From 15a995744810e930dee1a27e40131a160f9ebebf Mon Sep 17 00:00:00 2001 From: Amit Kumar Das Date: Wed, 18 Nov 2020 21:32:06 +0530 Subject: [PATCH] feat(recipe): add labeling as an operation (#181) This commit adds labeling as an operation. In other words, this lets one to labels against one or more resources. This also has tunable to unset labels against the resources that was applied previously. Signed-off-by: AmitKumarDas --- .github/workflows/test-release.yaml | 12 +- pkg/recipe/labeling.go | 199 ++++++++++++++++++++++++++++ types/recipe/label.go | 98 ++++++++++++++ 3 files changed, 299 insertions(+), 10 deletions(-) create mode 100644 pkg/recipe/labeling.go create mode 100644 types/recipe/label.go diff --git a/.github/workflows/test-release.yaml b/.github/workflows/test-release.yaml index 95e7be3..1f4aa33 100644 --- a/.github/workflows/test-release.yaml +++ b/.github/workflows/test-release.yaml @@ -11,12 +11,8 @@ jobs: steps: - name: Checkout Code uses: actions/checkout@v2 - - name: Setup GOPATH - run: | - echo "::set-env name=GOPATH::$(go env GOPATH)" - echo "::add-path::$(go env GOPATH)/bin" - name: Setup Golang - uses: actions/setup-go@v1 + uses: actions/setup-go@v2 with: go-version: 1.13.5 - run: make ${{ matrix.test }} @@ -29,12 +25,8 @@ jobs: steps: - name: Checkout Code uses: actions/checkout@v2 - - name: Setup GOPATH - run: | - echo "::set-env name=GOPATH::$(go env GOPATH)" - echo "::add-path::$(go env GOPATH)/bin" - name: Setup Golang - uses: actions/setup-go@v1 + uses: actions/setup-go@v2 with: go-version: 1.13.5 - run: sudo make ${{ matrix.test }} diff --git a/pkg/recipe/labeling.go b/pkg/recipe/labeling.go new file mode 100644 index 0000000..2cd2e3c --- /dev/null +++ b/pkg/recipe/labeling.go @@ -0,0 +1,199 @@ +/* +Copyright 2020 The MayaData Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package recipe + +import ( + "fmt" + + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + types "mayadata.io/d-operators/types/recipe" + "openebs.io/metac/dynamic/clientset" +) + +// Labeling helps applying desired labels(s) against the resource +type Labeling struct { + BaseRunner + Label *types.Label + + result *types.LabelResult + err error +} + +// LabelingConfig helps in creating new instance of Labeling +type LabelingConfig struct { + BaseRunner + Label *types.Label +} + +// NewLabeler returns a new instance of Labeling +func NewLabeler(config LabelingConfig) *Labeling { + return &Labeling{ + BaseRunner: config.BaseRunner, + Label: config.Label, + result: &types.LabelResult{}, + } +} + +func (l *Labeling) unset( + client *clientset.ResourceClient, + obj *unstructured.Unstructured, +) error { + var currentLbls = obj.GetLabels() + if len(currentLbls) == 0 || + len(currentLbls) < len(l.Label.ApplyLabels) { + // given object is not eligible to be + // unset, since all the desired labels + // are not present + return nil + } + for key, val := range l.Label.ApplyLabels { + if currentLbls[key] != val { + // given object is not eligible to be + // unset, since it does not match the desired labels + return nil + } + } + var newLbls = map[string]string{} + var isUnset bool + for key, val := range currentLbls { + isUnset = false + for applyKey := range l.Label.ApplyLabels { + if key == applyKey { + // do not add this key & value + // In other words unset this label + isUnset = true + break + } + } + if !isUnset { + // add existing key value pair since + // this is not to be unset + newLbls[key] = val + } + } + // update the resource by removing desired labels + obj.SetLabels(newLbls) + // update the object against the cluster + _, err := client. + Namespace(obj.GetNamespace()). + Update( + obj, + metav1.UpdateOptions{}, + ) + return err +} + +func (l *Labeling) label( + client *clientset.ResourceClient, + obj *unstructured.Unstructured, +) error { + var newLbls = map[string]string{} + for key, val := range obj.GetLabels() { + newLbls[key] = val + } + for nkey, nval := range l.Label.ApplyLabels { + newLbls[nkey] = nval + } + // update the resource with new labels + obj.SetLabels(newLbls) + // update the object against the cluster + _, err := client. + Namespace(obj.GetNamespace()). + Update( + obj, + metav1.UpdateOptions{}, + ) + return err +} + +func (l *Labeling) labelOrUnset( + client *clientset.ResourceClient, + obj *unstructured.Unstructured, +) error { + var isInclude bool + for _, name := range l.Label.FilterByNames { + if name == obj.GetName() { + isInclude = true + break + } + } + if !isInclude && l.Label.AutoUnset { + return l.unset(client, obj) + } + return l.label(client, obj) +} + +func (l *Labeling) labelAll() (*types.LabelResult, error) { + var message = fmt.Sprintf( + "Label resource %s %s: GVK %s", + l.Label.State.GetNamespace(), + l.Label.State.GetName(), + l.Label.State.GroupVersionKind(), + ) + err := l.Retry.Waitf( + func() (bool, error) { + // get appropriate dynamic client + client, err := l.GetClientForAPIVersionAndKind( + l.Label.State.GetAPIVersion(), + l.Label.State.GetKind(), + ) + if err != nil { + return false, errors.Wrapf( + err, + "Failed to get resource client", + ) + } + items, err := client. + Namespace(l.Label.State.GetNamespace()). + List(metav1.ListOptions{ + LabelSelector: labels.Set( + l.Label.State.GetLabels(), + ).String(), + }) + if err != nil { + return false, errors.Wrapf( + err, + "Failed to list resources", + ) + } + for _, obj := range items.Items { + err := l.labelOrUnset(client, &obj) + if err != nil { + return false, err + } + } + return true, nil + }, + message, + ) + if err != nil { + return nil, err + } + return &types.LabelResult{ + Phase: types.LabelStatusPassed, + Message: message, + }, nil +} + +// Run applyies the desired labels or unsets them +// against the resource(s) +func (l *Labeling) Run() (*types.LabelResult, error) { + return l.labelAll() +} diff --git a/types/recipe/label.go b/types/recipe/label.go new file mode 100644 index 0000000..e1ed9ed --- /dev/null +++ b/types/recipe/label.go @@ -0,0 +1,98 @@ +/* +Copyright 2020 The MayaData Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +import ( + "encoding/json" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// Label represents the label apply operation against +// one or more desired resources +type Label struct { + // Desired state i.e. resources that needs to be + // labeled + State *unstructured.Unstructured `json:"state"` + + // Filter the resources by these names + // + // Optional + FilterByNames []string `json:"filterByNames,omitempty"` + + // ApplyLabels represents the labels that need to be + // applied + ApplyLabels map[string]string `json:"applyLabels"` + + // AutoUnset removes the labels from the resources if + // they were applied earlier and these resources are + // no longer elgible to be applied with these labels + // + // Defaults to false + AutoUnset bool `json:"autoUnset"` +} + +// String implements the Stringer interface +func (l Label) String() string { + raw, err := json.MarshalIndent( + l, + " ", + ".", + ) + if err != nil { + panic(err) + } + return string(raw) +} + +// LabelStatusPhase is a typed definition to determine the +// result of executing the label operation +type LabelStatusPhase string + +const ( + // LabelStatusPassed defines a successful labeling + LabelStatusPassed LabelStatusPhase = "Passed" + + // LabelStatusWarning defines the label operation + // that resulted in warnings + LabelStatusWarning LabelStatusPhase = "Warning" + + // LabelStatusFailed defines a failed labeling + LabelStatusFailed LabelStatusPhase = "Failed" +) + +// ToTaskStatusPhase transforms ApplyStatusPhase to TestResultPhase +func (phase LabelStatusPhase) ToTaskStatusPhase() TaskStatusPhase { + switch phase { + case LabelStatusPassed: + return TaskStatusPassed + case LabelStatusFailed: + return TaskStatusFailed + case LabelStatusWarning: + return TaskStatusWarning + default: + return "" + } +} + +// LabelResult holds the result of labeling operation +type LabelResult struct { + Phase LabelStatusPhase `json:"phase"` + Message string `json:"message,omitempty"` + Verbose string `json:"verbose,omitempty"` + Warning string `json:"warning,omitempty"` +}