|
|
@@ -0,0 +1,424 @@
|
|
|
+package external
|
|
|
+
|
|
|
+import (
|
|
|
+ "fmt"
|
|
|
+ "testing"
|
|
|
+
|
|
|
+ "github.com/stretchr/testify/assert"
|
|
|
+ "github.com/stretchr/testify/require"
|
|
|
+)
|
|
|
+
|
|
|
+// newSource returns a ConfigMapSource for the given config.
|
|
|
+func newSource(cfg *Config) *ConfigMapSource {
|
|
|
+ return &ConfigMapSource{cfg: cfg}
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_NilConfig(t *testing.T) {
|
|
|
+ s := newSource(nil)
|
|
|
+
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{})
|
|
|
+
|
|
|
+ require.Error(t, err)
|
|
|
+ assert.Nil(t, res)
|
|
|
+ assert.EqualError(t, err, "nil config")
|
|
|
+}
|
|
|
+
|
|
|
+// --- traditional ConfigMap (Key and Route both empty) ---
|
|
|
+
|
|
|
+func TestConfigMapSource_Traditional_PassesDataDirectly(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "", "")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{"env": "prod", "region": "us-east-1"})
|
|
|
+ require.NoError(t, err)
|
|
|
+ assert.Equal(t, map[string]string{"env": "prod", "region": "us-east-1"}, res)
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_Traditional_EmptyData(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "", "")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{})
|
|
|
+ require.NoError(t, err)
|
|
|
+ assert.Empty(t, res)
|
|
|
+}
|
|
|
+
|
|
|
+// --- block-scalar ConfigMap ---
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_EmptyLabelsMap(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ yamlData := `
|
|
|
+labels: {}
|
|
|
+`
|
|
|
+
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{
|
|
|
+ "config.yaml": yamlData,
|
|
|
+ })
|
|
|
+
|
|
|
+ require.NoError(t, err)
|
|
|
+ assert.Empty(t, res)
|
|
|
+ assert.NotNil(t, res)
|
|
|
+}
|
|
|
+
|
|
|
+const prometheusConfig = `
|
|
|
+prometheusK8s:
|
|
|
+ externalLabels:
|
|
|
+ cluster: prod-cluster
|
|
|
+ region: eu-west-1
|
|
|
+`
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_SingleLevel(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("prometheus-cm", "", "config.yaml", "prometheusK8s.externalLabels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{"config.yaml": prometheusConfig})
|
|
|
+ require.NoError(t, err)
|
|
|
+ assert.Equal(t, map[string]string{
|
|
|
+ "cluster": "prod-cluster",
|
|
|
+ "region": "eu-west-1",
|
|
|
+ }, res)
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_TopLevelRoute(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "data", "labels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ yaml := `
|
|
|
+labels:
|
|
|
+ team: platform
|
|
|
+ env: staging
|
|
|
+`
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{"data": yaml})
|
|
|
+ require.NoError(t, err)
|
|
|
+ assert.Equal(t, map[string]string{"team": "platform", "env": "staging"}, res)
|
|
|
+}
|
|
|
+
|
|
|
+// --- error cases ---
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_EmptyMapDataInConfigYaml(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{
|
|
|
+ "config.yaml": "",
|
|
|
+ })
|
|
|
+
|
|
|
+ require.Error(t, err)
|
|
|
+ assert.Nil(t, res)
|
|
|
+ assert.Contains(t, err.Error(), "failed to locate route")
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_MissingKey(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "externalLabels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ _, err := s.ExtractNodeLabels(map[string]string{"other-key": "value"})
|
|
|
+ require.Error(t, err)
|
|
|
+ assert.Contains(t, err.Error(), `key "config.yaml" not found`)
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_InvalidYAML(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ _, err := s.ExtractNodeLabels(map[string]string{"config.yaml": ":\tinvalid: yaml: {"})
|
|
|
+ require.Error(t, err)
|
|
|
+ assert.Contains(t, err.Error(), "found character that cannot start any token")
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_RouteSegmentNotFound(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "does.not.exist")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ _, err := s.ExtractNodeLabels(map[string]string{"config.yaml": "foo: bar\n"})
|
|
|
+ require.Error(t, err)
|
|
|
+ assert.Contains(t, err.Error(), `failed to locate route`)
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_RouteSegmentNotANodeSequenceType(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels.nested")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ // labels is a string, not a map — traversing into it should fail
|
|
|
+ _, err := s.ExtractNodeLabels(map[string]string{"config.yaml": "labels: just-a-string\n"})
|
|
|
+ require.Error(t, err)
|
|
|
+ assert.Contains(t, err.Error(), "is not a map")
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_RoutePointsToNodeScalarType(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ // labels is a string, not a map — final conversion should fail
|
|
|
+ _, err := s.ExtractNodeLabels(map[string]string{"config.yaml": "labels: just-a-string\n"})
|
|
|
+ require.Error(t, err)
|
|
|
+ assert.Contains(t, err.Error(), "is not a map")
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_BoolValuesConvertedToString(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ yaml := `
|
|
|
+labels:
|
|
|
+ active: true
|
|
|
+ deprecated: false
|
|
|
+`
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{"config.yaml": yaml})
|
|
|
+ require.NoError(t, err)
|
|
|
+ assert.Equal(t, "true", res["active"])
|
|
|
+ assert.Equal(t, "false", res["deprecated"])
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_IntValuesConvertedToString(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ yaml := `
|
|
|
+labels:
|
|
|
+ priority: 42
|
|
|
+ replicas: 3
|
|
|
+`
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{"config.yaml": yaml})
|
|
|
+ require.NoError(t, err)
|
|
|
+ assert.Equal(t, "42", res["priority"])
|
|
|
+ assert.Equal(t, "3", res["replicas"])
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_SequenceValueRejected(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ yamlData := `
|
|
|
+labels:
|
|
|
+ environments:
|
|
|
+ - dev
|
|
|
+ - staging
|
|
|
+ - prod
|
|
|
+`
|
|
|
+
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{
|
|
|
+ "config.yaml": yamlData,
|
|
|
+ })
|
|
|
+
|
|
|
+ require.Error(t, err)
|
|
|
+ assert.Nil(t, res)
|
|
|
+ assert.Contains(t, err.Error(), `cannot unmarshal !!seq into string`)
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_NestedMapValueRejected(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ yamlData := `
|
|
|
+labels:
|
|
|
+ inner-map:
|
|
|
+ my-key: value
|
|
|
+ other-key: value
|
|
|
+`
|
|
|
+
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{
|
|
|
+ "config.yaml": yamlData,
|
|
|
+ })
|
|
|
+
|
|
|
+ require.Error(t, err)
|
|
|
+ assert.Nil(t, res)
|
|
|
+ assert.Contains(t, err.Error(), `unmarshal !!map into string`)
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_NullValues(t *testing.T) {
|
|
|
+ tests := []struct {
|
|
|
+ name string
|
|
|
+ nullValue string
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "explicit null",
|
|
|
+ nullValue: "null",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "tilde",
|
|
|
+ nullValue: "~",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "empty value",
|
|
|
+ nullValue: "",
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tt := range tests {
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+ yamlData := fmt.Sprintf(`
|
|
|
+labels:
|
|
|
+ priority: %s
|
|
|
+`, tt.nullValue)
|
|
|
+
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{
|
|
|
+ "config.yaml": yamlData,
|
|
|
+ })
|
|
|
+
|
|
|
+ require.NoError(t, err)
|
|
|
+ assert.Equal(t, "", res["priority"])
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_AliasValue(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ yamlData := `
|
|
|
+defaultEnvironment: &defaultEnvironment production
|
|
|
+
|
|
|
+labels:
|
|
|
+ environment: *defaultEnvironment
|
|
|
+`
|
|
|
+
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{
|
|
|
+ "config.yaml": yamlData,
|
|
|
+ })
|
|
|
+
|
|
|
+ require.NoError(t, err)
|
|
|
+ assert.Equal(t, "production", res["environment"])
|
|
|
+}
|
|
|
+
|
|
|
+func TestConfigMapSource_BlockScalar_ComplexYamlWithTwoUnsupportedNodeType(t *testing.T) {
|
|
|
+ nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
|
|
|
+ cfg := NewConfig(nlCfg)
|
|
|
+ s := newSource(cfg)
|
|
|
+
|
|
|
+ yaml := `
|
|
|
+labels:
|
|
|
+ name: complex
|
|
|
+ priority: 42
|
|
|
+ replicas: 3
|
|
|
+ other:
|
|
|
+ - "hello"
|
|
|
+ - "this"
|
|
|
+ - "is"
|
|
|
+ - "a"
|
|
|
+ - "test"
|
|
|
+ inner-map:
|
|
|
+ my-key: "value"
|
|
|
+ other-key: "value"
|
|
|
+`
|
|
|
+
|
|
|
+ res, err := s.ExtractNodeLabels(map[string]string{"config.yaml": yaml})
|
|
|
+
|
|
|
+ require.Error(t, err)
|
|
|
+ assert.Nil(t, res)
|
|
|
+}
|
|
|
+
|
|
|
+// --- filterValidLabels ---
|
|
|
+
|
|
|
+func TestFilterValidLabels_AllValid_ReturnedUnchanged(t *testing.T) {
|
|
|
+ in := map[string]string{
|
|
|
+ "env": "prod",
|
|
|
+ "region": "us-east-1",
|
|
|
+ "app.kubernetes.io/name": "opencost",
|
|
|
+ "my-key": "my-value",
|
|
|
+ "a": "b",
|
|
|
+ }
|
|
|
+ out := filterValidLabels(in)
|
|
|
+ assert.Equal(t, in, out)
|
|
|
+}
|
|
|
+
|
|
|
+func TestFilterValidLabels_EmptyInput_ReturnsEmptyMap(t *testing.T) {
|
|
|
+ out := filterValidLabels(map[string]string{})
|
|
|
+ assert.NotNil(t, out)
|
|
|
+ assert.Empty(t, out)
|
|
|
+}
|
|
|
+
|
|
|
+func TestFilterValidLabels_InvalidKey_EntryDropped(t *testing.T) {
|
|
|
+ in := map[string]string{
|
|
|
+ "valid-key": "value",
|
|
|
+ "": "empty-key-dropped",
|
|
|
+ "-bad-start": "dropped",
|
|
|
+ }
|
|
|
+ out := filterValidLabels(in)
|
|
|
+ assert.Equal(t, map[string]string{"valid-key": "value"}, out)
|
|
|
+}
|
|
|
+
|
|
|
+func TestFilterValidLabels_InvalidValue_EntryDropped(t *testing.T) {
|
|
|
+ in := map[string]string{
|
|
|
+ "good-key": "good-value",
|
|
|
+ "bad-value": "-starts-with-dash",
|
|
|
+ }
|
|
|
+ out := filterValidLabels(in)
|
|
|
+ assert.Equal(t, map[string]string{"good-key": "good-value"}, out)
|
|
|
+}
|
|
|
+
|
|
|
+func TestFilterValidLabels_ValueTooLong_EntryDropped(t *testing.T) {
|
|
|
+ longValue := string(make([]byte, 64))
|
|
|
+ for i := range longValue {
|
|
|
+ longValue = longValue[:i] + "a" + longValue[i+1:]
|
|
|
+ }
|
|
|
+ in := map[string]string{
|
|
|
+ "ok": "fine",
|
|
|
+ "toolong": longValue,
|
|
|
+ }
|
|
|
+ out := filterValidLabels(in)
|
|
|
+ assert.Equal(t, map[string]string{"ok": "fine"}, out)
|
|
|
+}
|
|
|
+
|
|
|
+func TestFilterValidLabels_KeyTooLong_EntryDropped(t *testing.T) {
|
|
|
+ longKey := string(make([]byte, 64))
|
|
|
+ for i := range longKey {
|
|
|
+ longKey = longKey[:i] + "a" + longKey[i+1:]
|
|
|
+ }
|
|
|
+ in := map[string]string{
|
|
|
+ "ok": "fine",
|
|
|
+ longKey: "value",
|
|
|
+ }
|
|
|
+ out := filterValidLabels(in)
|
|
|
+ assert.Equal(t, map[string]string{"ok": "fine"}, out)
|
|
|
+}
|
|
|
+
|
|
|
+func TestFilterValidLabels_PrefixedKey_Valid(t *testing.T) {
|
|
|
+ in := map[string]string{
|
|
|
+ "app.kubernetes.io/name": "opencost",
|
|
|
+ "example.com/env": "staging",
|
|
|
+ }
|
|
|
+ out := filterValidLabels(in)
|
|
|
+ assert.Equal(t, in, out)
|
|
|
+}
|
|
|
+
|
|
|
+func TestFilterValidLabels_PrefixedKey_InvalidPrefix_EntryDropped(t *testing.T) {
|
|
|
+ in := map[string]string{
|
|
|
+ "valid": "kept",
|
|
|
+ "-bad.prefix/k": "dropped",
|
|
|
+ }
|
|
|
+ out := filterValidLabels(in)
|
|
|
+ assert.Equal(t, map[string]string{"valid": "kept"}, out)
|
|
|
+}
|
|
|
+
|
|
|
+func TestFilterValidLabels_MixedValidAndInvalid_OnlyValidReturned(t *testing.T) {
|
|
|
+ in := map[string]string{
|
|
|
+ "cluster": "prod",
|
|
|
+ "": "no-key",
|
|
|
+ "bad-value": "-oops",
|
|
|
+ "region": "eu-west-1",
|
|
|
+ }
|
|
|
+ out := filterValidLabels(in)
|
|
|
+ assert.Equal(t, map[string]string{"cluster": "prod", "region": "eu-west-1"}, out)
|
|
|
+}
|