Skip to content

Commit

Permalink
feat(flag): add Label type
Browse files Browse the repository at this point in the history
It allows for specifying label diff that should be used for labeling clusters/tasks.
It's important that sctool users are required to pass only the label diff,
so that they don't overwrite labels used by other users or operator.
  • Loading branch information
Michal-Leszczynski authored and karol-kokoszka committed Aug 9, 2024
1 parent 11ecedd commit f4dcb2f
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 0 deletions.
101 changes: 101 additions & 0 deletions pkg/command/flag/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
package flag

import (
"encoding/csv"
"fmt"
"maps"
"strconv"
"strings"
"time"
Expand All @@ -16,6 +18,105 @@ import (
flag "github.com/spf13/pflag"
)

// Label describes modification applied to object's labels.
// It takes a form of csv string slice, where values can have the following formats:
// k=v - format means that label should be added.
// k- - format means that label should be removed.
// Because of that, the '=' is reserved and cannot be a part of the label.
type Label struct {
add map[string]string
remove []string
}

var _ flag.Value = (*Label)(nil)

func (l *Label) String() string {
var parts []string
for k, v := range l.add {
parts = append(parts, k+"="+v)
}
for _, k := range l.remove {
parts = append(parts, k+"-")
}
return strings.Join(parts, ",")
}

// Set implements pflag.Value.
func (l *Label) Set(s string) error {
if s == "" {
return nil
}
stringReader := strings.NewReader(s)
csvReader := csv.NewReader(stringReader)
spec, err := csvReader.Read()
if err != nil {
return fmt.Errorf("parse csv label list: %w", err)
}
// Taken from https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/label/label.go#L411
add := map[string]string{}
var remove []string
for _, labelSpec := range spec {
switch {
case strings.Contains(labelSpec, "="):
parts := strings.Split(labelSpec, "=")
if len(parts) != 2 {
return fmt.Errorf("%w: %v", errUnknownLabelSpec, labelSpec)
}
add[parts[0]] = parts[1]
case strings.HasSuffix(labelSpec, "-"):
remove = append(remove, labelSpec[:len(labelSpec)-1])
default:
return fmt.Errorf("%w: %v", errUnknownLabelSpec, labelSpec)
}
}
for _, removeLabel := range remove {
if _, found := add[removeLabel]; found {
return errAddAndRemoveLabel
}
}
l.add = add
l.remove = remove
return nil
}

var (
errUnknownLabelSpec = errors.New("unknown label spec, label must be formatted as either key=value, or key-")
errAddAndRemoveLabel = errors.New("can not both modify and remove a label in the same command")
)

// Type implements pflag.Value.
func (l *Label) Type() string {
return "map"
}

// ApplyDiff adds and removes specified labels.
func (l *Label) ApplyDiff(labels map[string]string) map[string]string {
out := maps.Clone(labels)
if l == nil {
return out
}
if out == nil {
out = make(map[string]string)
}

for k, v := range l.add {
out[k] = v
}
for _, k := range l.remove {
delete(out, k)
}

return out
}

// NewLabels returns newly added labels.
func (l *Label) NewLabels() map[string]string {
if l.add == nil {
return map[string]string{}
}
return maps.Clone(l.add)
}

// Cron wraps string for early validation.
type Cron struct {
v string
Expand Down
84 changes: 84 additions & 0 deletions pkg/command/flag/type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
package flag

import (
"errors"
"maps"
"strings"
"testing"
"time"

"github.com/scylladb/go-set/strset"
"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
)

Expand Down Expand Up @@ -75,3 +78,84 @@ func TestParseTime(t *testing.T) {
}
}
}

func TestParseLabel(t *testing.T) {
testCases := []struct {
S string
T Label
E error
}{
{
S: "k1=v1",
T: Label{
add: map[string]string{
"k1": "v1",
},
},
},
{
S: "k1-",
T: Label{
remove: []string{"k1"},
},
},
{
S: "k1=v1,k2=v2,k3-,k4=v4",
T: Label{
add: map[string]string{
"k1": "v1",
"k2": "v2",
"k4": "v4",
},
remove: []string{"k3"},
},
},
{
S: "k1=v1,with space=with-dash_underscore666",
T: Label{
add: map[string]string{
"k1": "v1",
"with space": "with-dash_underscore666",
},
},
},
{
S: "k1=v1,k1-",
E: errAddAndRemoveLabel,
},
{
S: "k1=v1=v2",
E: errUnknownLabelSpec,
},
{
S: "k1",
E: errUnknownLabelSpec,
},
}

parse := func(s string) (Label, error) {
var l Label
err := l.Set(s)
return l, err
}

for i, tc := range testCases {
l, err := parse(tc.S)

if tc.E != nil {
if !errors.Is(err, tc.E) {
t.Fatalf("%d: expected error: %s, but got: %s", i, tc.E, err)
}
continue
} else if err != nil {
t.Fatalf("%d: expected no error, but got: %s", i, err)
}

if !maps.Equal(l.add, tc.T.add) {
t.Fatalf("%d: expected added labels: %v, but got: %v", i, l.add, tc.T.add)
}
if !strset.New(l.remove...).IsEqual(strset.New(tc.T.remove...)) {
t.Fatalf("%d: expected removed labels: %v, but got: %v", i, l.remove, tc.T.remove)
}
}
}

0 comments on commit f4dcb2f

Please sign in to comment.