3
0

Implement labels through pkg/label

This commit is contained in:
Denis Arh
2020-10-26 06:57:17 +01:00
parent 9b3ba3169c
commit ae4a07a3f7
6 changed files with 396 additions and 0 deletions
+176
View File
@@ -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
}
+56
View File
@@ -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)
}
})
}
}
+40
View File
@@ -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
}
+49
View File
@@ -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
}
+71
View File
@@ -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)
}
}
+4
View File
@@ -0,0 +1,4 @@
package: types
types:
Label:
noIdField: true