Files
dokku/plugins/scheduler-k3s/metadata_test.go
Jose Diaz-Gonzalez c37a71b90b feat: add a whole-set form for scheduler-k3s metadata
The `scheduler-k3s` metadata commands were all per-key, so matching an app's stored annotations, labels or node sysctls to a declared set meant reading the report, computing the difference in both directions and issuing one command per added, changed and removed key, with a failure partway through leaving a mixture of the two sets. A `--replace` flag on `annotations:set`, `labels:set`, `node-sysctls:set` and `autoscaling-auth:set` now writes the declared map in a single call, taking trailing `key=value` pairs everywhere except `autoscaling-auth:set`, which keeps its repeatable `--metadata` flag. Every pair is parsed before anything is written, so a rejected pair leaves the stored map untouched, and a pair ending in `=` stores an empty value, which the per-key form cannot express. An empty pair list is rejected rather than treated as a request to remove everything, so a generated list that expands to nothing cannot silently drop an app's metadata; `annotations:clear`, `labels:clear` and `node-sysctls:clear` handle that case, taking the same optional filters as their report commands. The `--resource-type` flag and the app name are now validated on the set commands as well, where a typo previously exited zero and wrote a property that no report and no deploy ever read.
2026-09-18 05:14:51 -04:00

189 lines
5.2 KiB
Go

package scheduler_k3s
import (
"reflect"
"strings"
"testing"
)
func TestParseMetadataPairs(t *testing.T) {
tests := []struct {
name string
pairs []string
want map[string]string
}{
{
name: "empty list",
pairs: []string{},
want: map[string]string{},
},
{
name: "multiple pairs",
pairs: []string{"foo=bar", "baz=qux"},
want: map[string]string{"foo": "bar", "baz": "qux"},
},
{
name: "trailing equals stores an empty value",
pairs: []string{"foo="},
want: map[string]string{"foo": ""},
},
{
name: "value containing equals is kept whole",
pairs: []string{"foo=a=b"},
want: map[string]string{"foo": "a=b"},
},
{
name: "kubernetes style key",
pairs: []string{"prometheus.io/scrape=true"},
want: map[string]string{"prometheus.io/scrape": "true"},
},
{
name: "multi-line value",
pairs: []string{"foo=line one\nline two"},
want: map[string]string{"foo": "line one\nline two"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := parseMetadataPairs(test.pairs)
if err != nil {
t.Fatalf("parseMetadataPairs(%q) returned error: %s", test.pairs, err)
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("parseMetadataPairs(%q) = %v, want %v", test.pairs, got, test.want)
}
})
}
}
func TestParseMetadataPairsErrors(t *testing.T) {
tests := []struct {
name string
pairs []string
want string
}{
{
name: "missing equals",
pairs: []string{"foo"},
want: "Invalid key=value pair: foo",
},
{
name: "empty key",
pairs: []string{"=bar"},
want: "Invalid key=value pair: =bar",
},
{
name: "empty pair",
pairs: []string{""},
want: "Invalid key=value pair: ",
},
{
name: "duplicate key",
pairs: []string{"foo=bar", "foo=qux"},
want: "Duplicate key specified: foo",
},
{
name: "duplicate key with identical value",
pairs: []string{"foo=bar", "foo=bar"},
want: "Duplicate key specified: foo",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := parseMetadataPairs(test.pairs)
if err == nil {
t.Fatalf("parseMetadataPairs(%q) = %v, want error", test.pairs, got)
}
if err.Error() != test.want {
t.Errorf("parseMetadataPairs(%q) error = %q, want %q", test.pairs, err.Error(), test.want)
}
})
}
}
func TestValidateResourceType(t *testing.T) {
tests := []struct {
name string
field metadataField
resourceType string
wantErr bool
}{
{
name: "valid annotation resource type",
field: annotationsField,
resourceType: "deployment",
},
{
name: "valid label resource type",
field: labelsField,
resourceType: "deployment",
},
{
// An empty resource type is an absent filter for the clear commands.
// The set commands reject it separately.
name: "empty resource type passes",
field: annotationsField,
resourceType: "",
},
{
name: "unknown resource type",
field: annotationsField,
resourceType: "deploymnet",
wantErr: true,
},
{
// Keda resources do not flow user labels through, so they are
// annotation-only resource types.
name: "keda resource type is valid for annotations",
field: annotationsField,
resourceType: "keda_scaled_object",
},
{
name: "keda resource type is invalid for labels",
field: labelsField,
resourceType: "keda_scaled_object",
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := validateResourceType(test.field, test.resourceType)
if test.wantErr {
if err == nil {
t.Fatalf("validateResourceType(%s, %q) = nil, want error", test.field.Name, test.resourceType)
}
if !strings.Contains(err.Error(), "Invalid resource-type specified, valid resource types include: ") {
t.Errorf("validateResourceType(%s, %q) error = %q, want the valid type list", test.field.Name, test.resourceType, err.Error())
}
for _, valid := range test.field.ResourceTypes {
if !strings.Contains(err.Error(), valid) {
t.Errorf("validateResourceType(%s, %q) error = %q, missing valid type %s", test.field.Name, test.resourceType, err.Error(), valid)
}
}
return
}
if err != nil {
t.Errorf("validateResourceType(%s, %q) returned error: %s", test.field.Name, test.resourceType, err)
}
})
}
}
func TestMetadataPropertyName(t *testing.T) {
// Annotations are stored unprefixed and labels under a labels. prefix, which is
// what getAnnotation and getLabel already build.
if got := metadataPropertyName(annotationsField, "web", "deployment"); got != "web.deployment" {
t.Errorf("metadataPropertyName(annotations, web, deployment) = %q, want web.deployment", got)
}
if got := metadataPropertyName(labelsField, "web", "deployment"); got != "labels.web.deployment" {
t.Errorf("metadataPropertyName(labels, web, deployment) = %q, want labels.web.deployment", got)
}
if got := metadataPropertyName(annotationsField, GlobalProcessType, "pod"); got != "--global.pod" {
t.Errorf("metadataPropertyName(annotations, --global, pod) = %q, want --global.pod", got)
}
}