snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
196
server/pkg/label/label.go
Normal file
196
server/pkg/label/label.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package label
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/label/types"
|
||||
"github.com/cortezaproject/corteza/server/pkg/str"
|
||||
"github.com/cortezaproject/corteza/server/store"
|
||||
)
|
||||
|
||||
type (
|
||||
Labels map[string]types.LabelValue
|
||||
|
||||
LabeledResource interface {
|
||||
GetLabels() map[string]types.LabelValue
|
||||
SetLabel(key string, value types.LabelValue)
|
||||
LabelResourceKind() string
|
||||
LabelResourceID() uint64
|
||||
}
|
||||
)
|
||||
|
||||
// Changed checks if label maps are same or different
|
||||
func Changed(old, new map[string]types.LabelValue) bool {
|
||||
for k := range old {
|
||||
if _, has := new[k]; !has {
|
||||
return true
|
||||
} else if !new[k].Equal(old[k]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for k := range new {
|
||||
if _, has := old[k]; !has {
|
||||
return true
|
||||
} else if !new[k].Equal(old[k]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ParseStrings converts slice of strings with "key=val" format into
|
||||
func ParseStrings(ss []string) (m map[string]types.LabelValue, err error) {
|
||||
h, err := str.ParseStrings(ss)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m = make(map[string]types.LabelValue, len(h))
|
||||
for k, v := range h {
|
||||
if strings.HasPrefix(v, "[") && strings.HasSuffix(v, "]") {
|
||||
var arr []string
|
||||
if err := json.Unmarshal([]byte(v), &arr); err == nil {
|
||||
m[k] = types.LabelValue{Values: arr}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(v, "\"") && strings.HasSuffix(v, "\"") && len(v) > 1 {
|
||||
v = v[1 : len(v)-1]
|
||||
}
|
||||
m[k] = types.LabelValue{Val: v}
|
||||
}
|
||||
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]types.LabelValue, base ...uint64) ([]uint64, error) {
|
||||
// label filter not set,
|
||||
// return base resource IDs as-is
|
||||
if len(f) == 0 {
|
||||
return base, nil
|
||||
}
|
||||
var result []uint64 = base
|
||||
// search for filters
|
||||
for k, v := range f {
|
||||
var values []string
|
||||
if v.Val != "" {
|
||||
values = []string{v.Val}
|
||||
} else if len(v.Values) > 0 {
|
||||
values = v.Values
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
set, _, err := store.SearchLabels(ctx, s, types.LabelFilter{Kind: kind, Filter: map[string][]string{k: values}, ResourceID: result})
|
||||
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
|
||||
result = set.ResourceIDs()
|
||||
|
||||
if len(result) == 0 {
|
||||
return []uint64{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return result, 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
|
||||
}
|
||||
156
server/pkg/label/label_test.go
Normal file
156
server/pkg/label/label_test.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package label
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/label/types"
|
||||
)
|
||||
|
||||
func TestChanged(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
old map[string]types.LabelValue
|
||||
new map[string]types.LabelValue
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
"2x nil",
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"2x empty",
|
||||
map[string]types.LabelValue{},
|
||||
map[string]types.LabelValue{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"nil & empty",
|
||||
nil,
|
||||
map[string]types.LabelValue{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"same single value",
|
||||
map[string]types.LabelValue{"a": {Val: "a"}},
|
||||
map[string]types.LabelValue{"a": {Val: "a"}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"same multi value",
|
||||
map[string]types.LabelValue{"a": {Values: []string{"1", "2"}}},
|
||||
map[string]types.LabelValue{"a": {Values: []string{"1", "2"}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"diff single value",
|
||||
map[string]types.LabelValue{"a": {Val: "a"}},
|
||||
map[string]types.LabelValue{"a": {Val: "b"}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"diff multi value",
|
||||
map[string]types.LabelValue{"a": {Values: []string{"1", "2"}}},
|
||||
map[string]types.LabelValue{"a": {Values: []string{"1", "3"}}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"diff key removed",
|
||||
map[string]types.LabelValue{"a": {Val: "b"}},
|
||||
map[string]types.LabelValue{},
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStrings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
labels []string
|
||||
want map[string]types.LabelValue
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"set of pairs",
|
||||
[]string{"aa=b"},
|
||||
map[string]types.LabelValue{"aa": {Val: "b"}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"empty json",
|
||||
[]string{`{}`},
|
||||
map[string]types.LabelValue{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"json",
|
||||
[]string{`{"aa":"b"}`},
|
||||
map[string]types.LabelValue{"aa": {Val: "b"}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"value with colons and slashes (single)",
|
||||
[]string{"ref_namespace=corteza::compose:namespace/123"},
|
||||
map[string]types.LabelValue{"ref_namespace": {Val: "corteza::compose:namespace/123"}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"value with colons and slashes (array)",
|
||||
[]string{`ref_namespace=["corteza::compose:namespace/123","corteza::compose:namespace/456"]`},
|
||||
map[string]types.LabelValue{"ref_namespace": {Values: []string{"corteza::compose:namespace/123", "corteza::compose:namespace/456"}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"multiple reference labels",
|
||||
[]string{
|
||||
"ref_namespace=corteza::compose:namespace/123",
|
||||
"ref_module=corteza::compose:module/123/456",
|
||||
},
|
||||
map[string]types.LabelValue{
|
||||
"ref_namespace": {Val: "corteza::compose:namespace/123"},
|
||||
"ref_module": {Val: "corteza::compose:module/123/456"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"quoted string value (strips quotes)",
|
||||
[]string{`ref_module="corteza::compose:module/1239888/408"`},
|
||||
map[string]types.LabelValue{"ref_module": {Val: "corteza::compose:module/1239888/408"}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"mixed: array and quoted string",
|
||||
[]string{
|
||||
`ref_namespace=["corteza::compose:namespace/123"]`,
|
||||
`ref_module="corteza::compose:module/1239888/408"`,
|
||||
},
|
||||
map[string]types.LabelValue{
|
||||
"ref_namespace": {Values: []string{"corteza::compose:namespace/123"}},
|
||||
"ref_module": {Val: "corteza::compose:module/1239888/408"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseStrings(tt.labels)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseStrings() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ParseStrings() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
127
server/pkg/label/types/type.go
Normal file
127
server/pkg/label/types/type.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/sql"
|
||||
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type (
|
||||
LabelValue struct {
|
||||
Val string `json:"value,omitempty"`
|
||||
Values []string `json:"values,omitempty"`
|
||||
}
|
||||
Label struct {
|
||||
// Kind of the labeled resource
|
||||
Kind string
|
||||
|
||||
// ID of the labeled resource
|
||||
ResourceID uint64
|
||||
|
||||
Name string
|
||||
Value LabelValue
|
||||
}
|
||||
|
||||
LabelFilter struct {
|
||||
Kind string
|
||||
ResourceID []uint64
|
||||
Filter map[string][]string
|
||||
Limit uint
|
||||
Name string
|
||||
Value []string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
LabelResourceType = "corteza::generic:label"
|
||||
)
|
||||
|
||||
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]LabelValue {
|
||||
var kv = make(map[string]LabelValue)
|
||||
for _, label := range set {
|
||||
if kind == label.Kind && ID == label.ResourceID {
|
||||
kv[label.Name] = label.Value
|
||||
}
|
||||
}
|
||||
|
||||
return kv
|
||||
}
|
||||
func (lv *LabelValue) Scan(src any) error { return sql.ParseJSON(src, lv) }
|
||||
|
||||
// Value implements driver.Value for database storage
|
||||
// Uses explicit object format {"value":...} or {"values":...} for DB storage
|
||||
// (different from MarshalJSON which outputs simplified format for API)
|
||||
func (lv LabelValue) Value() (driver.Value, error) {
|
||||
if len(lv.Values) > 0 {
|
||||
return json.Marshal(struct {
|
||||
Values []string `json:"values"`
|
||||
}{Values: lv.Values})
|
||||
}
|
||||
return json.Marshal(struct {
|
||||
Val string `json:"value"`
|
||||
}{Val: lv.Val})
|
||||
}
|
||||
|
||||
func (lv LabelValue) MarshalJSON() ([]byte, error) {
|
||||
if len(lv.Values) > 0 {
|
||||
return json.Marshal(lv.Values)
|
||||
}
|
||||
return json.Marshal(lv.Val)
|
||||
}
|
||||
|
||||
func (lv *LabelValue) UnmarshalJSON(data []byte) error {
|
||||
var strVal string
|
||||
if err := json.Unmarshal(data, &strVal); err == nil {
|
||||
lv.Val = strVal
|
||||
lv.Values = nil
|
||||
return nil
|
||||
}
|
||||
var arrVal []string
|
||||
if err := json.Unmarshal(data, &arrVal); err == nil {
|
||||
lv.Values = arrVal
|
||||
lv.Val = ""
|
||||
return nil
|
||||
}
|
||||
var obj struct {
|
||||
Val string `json:"value,omitempty"`
|
||||
Values []string `json:"values,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &obj); err == nil {
|
||||
lv.Val = obj.Val
|
||||
lv.Values = obj.Values
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lv LabelValue) Equal(other LabelValue) bool {
|
||||
if lv.Val != "" && other.Val != "" {
|
||||
return lv.Val == other.Val
|
||||
}
|
||||
|
||||
if len(lv.Values) > 0 && len(other.Values) > 0 {
|
||||
if len(lv.Values) != len(other.Values) {
|
||||
return false
|
||||
}
|
||||
for i := range lv.Values {
|
||||
if lv.Values[i] != other.Values[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return lv.Val == "" && len(lv.Values) == 0 && other.Val == "" && len(other.Values) == 0
|
||||
}
|
||||
47
server/pkg/label/types/type_set.gen.go
generated
Normal file
47
server/pkg/label/types/type_set.gen.go
generated
Normal file
@@ -0,0 +1,47 @@
|
||||
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/types.yaml
|
||||
|
||||
type (
|
||||
|
||||
// LabelSet slice of Label
|
||||
//
|
||||
// This type is auto-generated.
|
||||
LabelSet []*Label
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Filter 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
server/pkg/label/types/type_set.gen_test.go
generated
Normal file
71
server/pkg/label/types/type_set.gen_test.go
generated
Normal 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/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
server/pkg/label/types/types.yaml
Normal file
4
server/pkg/label/types/types.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
package: types
|
||||
types:
|
||||
Label:
|
||||
noIdField: true
|
||||
Reference in New Issue
Block a user