Skip to content

Commit

Permalink
feat(recipe): add labeling as an operation (#181)
Browse files Browse the repository at this point in the history
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 <amit.das@mayadata.io>
  • Loading branch information
Amit Kumar Das authored Nov 18, 2020
1 parent f01b6bd commit 15a9957
Show file tree
Hide file tree
Showing 3 changed files with 299 additions and 10 deletions.
12 changes: 2 additions & 10 deletions .github/workflows/test-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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 }}
Expand Down
199 changes: 199 additions & 0 deletions pkg/recipe/labeling.go
Original file line number Diff line number Diff line change
@@ -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()
}
98 changes: 98 additions & 0 deletions types/recipe/label.go
Original file line number Diff line number Diff line change
@@ -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"`
}

0 comments on commit 15a9957

Please sign in to comment.