diff --git a/pkg/label/label.go b/pkg/label/label.go index 651677a25..718a5bd0a 100644 --- a/pkg/label/label.go +++ b/pkg/label/label.go @@ -1 +1,177 @@ package label + +import ( + "context" + "fmt" + "github.com/cortezaproject/corteza-server/pkg/handle" + "github.com/cortezaproject/corteza-server/pkg/label/types" + "github.com/cortezaproject/corteza-server/store" + "strings" +) + +type ( + Labels map[string]string + + LabeledResource interface { + GetLabels() map[string]string + SetLabel(key string, value string) + LabelResourceKind() string + LabelResourceID() uint64 + } +) + +// Changed checks if label maps are same or different +func Changed(old, new map[string]string) bool { + for k := range old { + if _, has := new[k]; !has { + return true + } else if new[k] != old[k] { + return true + } + } + + for k := range new { + if _, has := old[k]; !has { + return true + } else if new[k] != old[k] { + return true + } + } + + return false +} + +// ParseStrings converts slice of strings with "key=val" format into +func ParseStrings(ss []string) (map[string]string, error) { + if len(ss) == 0 { + return nil, nil + } + + m := make(map[string]string) + + for _, s := range ss { + kv := strings.SplitN(s, "=", 2) + if len(kv) != 2 { + return nil, fmt.Errorf("invalid label format") + } + + if !handle.IsValid(kv[0]) { + return nil, fmt.Errorf("invalid label key format") + } + + m[kv[0]] = kv[1] + } + + return m, nil +} + +// Search queries all matching (by kind and key-value filter) labels +// +// In case when list of (base) resources is given, labels are also filtered by resource IDs +// to ensure only matching subset is returned +// +// 3 scenarios: +// - empty filter +// - filter set +// - filter & base set +func Search(ctx context.Context, s store.Labels, kind string, f map[string]string, base ...uint64) ([]uint64, error) { + // label filter not set, + // return base resource IDs as-is + if len(f) == 0 { + return base, nil + } + + // search for filters + set, _, err := store.SearchLabels(ctx, s, types.LabelFilter{Kind: kind, Filter: f, ResourceID: base}) + if err != nil { + return nil, err + } + + // If we have slice with base IDs, calculate intersection between it and fetched resourceIDs + // from the labels to ensure we only return results that satisfy BOTH conditions + return set.ResourceIDs(), nil +} + +// Load searches labels for all labeled resources +func Load(ctx context.Context, s store.Labels, rr ...LabeledResource) error { + if len(rr) == 0 { + return nil + } + + var ( + f = types.LabelFilter{ResourceID: make([]uint64, 0, len(rr))} + ) + + for _, r := range rr { + if f.Kind == "" { + f.Kind = r.LabelResourceKind() + } else if f.Kind != r.LabelResourceKind() { + return fmt.Errorf("expecting one label type, got two: %q, %q", f.Kind, r.LabelResourceKind()) + } + + f.ResourceID = append(f.ResourceID, r.LabelResourceID()) + } + + set, _, err := store.SearchLabels(ctx, s, f) + if err != nil { + return err + } + + for _, r := range rr { + for k, v := range set.FilterByResource(r.LabelResourceKind(), r.LabelResourceID()) { + r.SetLabel(k, v) + } + } + + return nil +} + +// Update updates/creates all labels on labeled resource and removes that are explicitly passed +func Create(ctx context.Context, s store.Labels, r LabeledResource) error { + var ( + err error + l = &types.Label{ + Kind: r.LabelResourceKind(), + ResourceID: r.LabelResourceID(), + } + ) + + for l.Name, l.Value = range r.GetLabels() { + if err = store.CreateLabel(ctx, s, l); err != nil { + return err + } + } + + return nil +} + +// Update updates or creates all labels on labeled resource and removes all non explicitly defined +func Update(ctx context.Context, s store.Labels, r LabeledResource) error { + var ( + err error + labels = r.GetLabels() + keys = make([]string, 0, len(labels)) + key string + + l = &types.Label{ + Kind: r.LabelResourceKind(), + ResourceID: r.LabelResourceID(), + } + ) + + for key = range labels { + keys = append(keys, key) + } + + if err = store.DeleteExtraLabels(ctx, s, r.LabelResourceKind(), r.LabelResourceID(), keys...); err != nil { + return err + } + + for l.Name, l.Value = range r.GetLabels() { + if err = store.UpsertLabel(ctx, s, l); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/label/label_test.go b/pkg/label/label_test.go index 651677a25..42e7ebe47 100644 --- a/pkg/label/label_test.go +++ b/pkg/label/label_test.go @@ -1 +1,57 @@ package label + +import "testing" + +func TestChanged(t *testing.T) { + tests := []struct { + name string + old map[string]string + new map[string]string + want bool + }{ + // TODO: Add test cases. + { + "2x nil", + nil, + nil, + false, + }, + { + "2x empty", + map[string]string{}, + map[string]string{}, + false, + }, + { + "nil & empty", + nil, + map[string]string{}, + false, + }, + { + "same", + map[string]string{"a": "a"}, + map[string]string{"a": "a"}, + false, + }, + { + "diff1", + map[string]string{"a": "a"}, + map[string]string{"a": "b"}, + true, + }, + { + "diff2", + map[string]string{"a": "b"}, + map[string]string{}, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Changed(tt.old, tt.new); got != tt.want { + t.Errorf("Changed() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/label/types/type.go b/pkg/label/types/type.go new file mode 100644 index 000000000..d6fceeb5c --- /dev/null +++ b/pkg/label/types/type.go @@ -0,0 +1,40 @@ +package types + +type ( + Label struct { + // Kind of the labeled resource + Kind string + + // ID of the labeled resource + ResourceID uint64 + + Name string + Value string + } + + LabelFilter struct { + Kind string + ResourceID []uint64 + Filter map[string]string + } +) + +func (set LabelSet) ResourceIDs() (rr []uint64) { + rr = make([]uint64, len(set)) + for r := range set { + rr[r] = set[r].ResourceID + } + + return +} + +func (set LabelSet) FilterByResource(kind string, ID uint64) map[string]string { + var kv = make(map[string]string) + for _, label := range set { + if kind == label.Kind && ID == label.ResourceID { + kv[label.Name] = label.Value + } + } + + return kv +} diff --git a/pkg/label/types/type_set.gen.go b/pkg/label/types/type_set.gen.go new file mode 100644 index 000000000..842e58bba --- /dev/null +++ b/pkg/label/types/type_set.gen.go @@ -0,0 +1,49 @@ +package types + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// pkg/label/types.yaml + +type ( + + // LabelSet slice of Label + // + // This type is auto-generated. + LabelSet []*Label + + Map map[string]string +) + +// Walk iterates through every slice item and calls w(Label) err +// +// This function is auto-generated. +func (set LabelSet) Walk(w func(*Label) error) (err error) { + for i := range set { + if err = w(set[i]); err != nil { + return + } + } + + return +} + +// LabelFilter iterates through every slice item, calls f(Label) (bool, err) and return filtered slice +// +// This function is auto-generated. +func (set LabelSet) Filter(f func(*Label) (bool, error)) (out LabelSet, err error) { + var ok bool + out = LabelSet{} + for i := range set { + if ok, err = f(set[i]); err != nil { + return + } else if ok { + out = append(out, set[i]) + } + } + + return +} diff --git a/pkg/label/types/type_set.gen_test.go b/pkg/label/types/type_set.gen_test.go new file mode 100644 index 000000000..5d240181f --- /dev/null +++ b/pkg/label/types/type_set.gen_test.go @@ -0,0 +1,71 @@ +package types + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// pkg/label/types.yaml + +import ( + "fmt" + "github.com/stretchr/testify/require" + "testing" +) + +func TestLabelSetWalk(t *testing.T) { + var ( + value = make(LabelSet, 3) + req = require.New(t) + ) + + // check walk with no errors + { + err := value.Walk(func(*Label) error { + return nil + }) + req.NoError(err) + } + + // check walk with error + req.Error(value.Walk(func(*Label) error { return fmt.Errorf("walk error") })) +} + +func TestLabelSetFilter(t *testing.T) { + var ( + value = make(LabelSet, 3) + req = require.New(t) + ) + + // filter nothing + { + set, err := value.Filter(func(*Label) (bool, error) { + return true, nil + }) + req.NoError(err) + req.Equal(len(set), len(value)) + } + + // filter one item + { + found := false + set, err := value.Filter(func(*Label) (bool, error) { + if !found { + found = true + return found, nil + } + return false, nil + }) + req.NoError(err) + req.Len(set, 1) + } + + // filter error + { + _, err := value.Filter(func(*Label) (bool, error) { + return false, fmt.Errorf("filter error") + }) + req.Error(err) + } +} diff --git a/pkg/label/types/types.yaml b/pkg/label/types/types.yaml new file mode 100644 index 000000000..137c59442 --- /dev/null +++ b/pkg/label/types/types.yaml @@ -0,0 +1,4 @@ +package: types +types: + Label: + noIdField: true