configmapsource_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. package external
  2. import (
  3. "fmt"
  4. "testing"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/stretchr/testify/require"
  7. )
  8. // newSource returns a ConfigMapSource for the given config.
  9. func newSource(cfg *Config) *ConfigMapSource {
  10. return &ConfigMapSource{cfg: cfg}
  11. }
  12. func TestConfigMapSource_NilConfig(t *testing.T) {
  13. s := newSource(nil)
  14. res, err := s.ExtractNodeLabels(map[string]string{})
  15. require.Error(t, err)
  16. assert.Nil(t, res)
  17. assert.EqualError(t, err, "nil config")
  18. }
  19. // --- traditional ConfigMap (Key and Route both empty) ---
  20. func TestConfigMapSource_Traditional_PassesDataDirectly(t *testing.T) {
  21. nlCfg := NewNodeLabelConfig("my-cm", "", "", "")
  22. cfg := NewConfig(nlCfg)
  23. s := newSource(cfg)
  24. res, err := s.ExtractNodeLabels(map[string]string{"env": "prod", "region": "us-east-1"})
  25. require.NoError(t, err)
  26. assert.Equal(t, map[string]string{"env": "prod", "region": "us-east-1"}, res)
  27. }
  28. func TestConfigMapSource_Traditional_EmptyData(t *testing.T) {
  29. nlCfg := NewNodeLabelConfig("my-cm", "", "", "")
  30. cfg := NewConfig(nlCfg)
  31. s := newSource(cfg)
  32. res, err := s.ExtractNodeLabels(map[string]string{})
  33. require.NoError(t, err)
  34. assert.Empty(t, res)
  35. }
  36. // --- block-scalar ConfigMap ---
  37. func TestConfigMapSource_BlockScalar_EmptyLabelsMap(t *testing.T) {
  38. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
  39. cfg := NewConfig(nlCfg)
  40. s := newSource(cfg)
  41. yamlData := `
  42. labels: {}
  43. `
  44. res, err := s.ExtractNodeLabels(map[string]string{
  45. "config.yaml": yamlData,
  46. })
  47. require.NoError(t, err)
  48. assert.Empty(t, res)
  49. assert.NotNil(t, res)
  50. }
  51. const prometheusConfig = `
  52. prometheusK8s:
  53. externalLabels:
  54. cluster: prod-cluster
  55. region: eu-west-1
  56. `
  57. func TestConfigMapSource_BlockScalar_SingleLevel(t *testing.T) {
  58. nlCfg := NewNodeLabelConfig("prometheus-cm", "", "config.yaml", "prometheusK8s.externalLabels")
  59. cfg := NewConfig(nlCfg)
  60. s := newSource(cfg)
  61. res, err := s.ExtractNodeLabels(map[string]string{"config.yaml": prometheusConfig})
  62. require.NoError(t, err)
  63. assert.Equal(t, map[string]string{
  64. "cluster": "prod-cluster",
  65. "region": "eu-west-1",
  66. }, res)
  67. }
  68. func TestConfigMapSource_BlockScalar_TopLevelRoute(t *testing.T) {
  69. nlCfg := NewNodeLabelConfig("my-cm", "", "data", "labels")
  70. cfg := NewConfig(nlCfg)
  71. s := newSource(cfg)
  72. yaml := `
  73. labels:
  74. team: platform
  75. env: staging
  76. `
  77. res, err := s.ExtractNodeLabels(map[string]string{"data": yaml})
  78. require.NoError(t, err)
  79. assert.Equal(t, map[string]string{"team": "platform", "env": "staging"}, res)
  80. }
  81. // --- error cases ---
  82. func TestConfigMapSource_BlockScalar_EmptyMapDataInConfigYaml(t *testing.T) {
  83. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
  84. cfg := NewConfig(nlCfg)
  85. s := newSource(cfg)
  86. res, err := s.ExtractNodeLabels(map[string]string{
  87. "config.yaml": "",
  88. })
  89. require.Error(t, err)
  90. assert.Nil(t, res)
  91. assert.Contains(t, err.Error(), "failed to locate route")
  92. }
  93. func TestConfigMapSource_BlockScalar_MissingKey(t *testing.T) {
  94. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "externalLabels")
  95. cfg := NewConfig(nlCfg)
  96. s := newSource(cfg)
  97. _, err := s.ExtractNodeLabels(map[string]string{"other-key": "value"})
  98. require.Error(t, err)
  99. assert.Contains(t, err.Error(), `key "config.yaml" not found`)
  100. }
  101. func TestConfigMapSource_BlockScalar_InvalidYAML(t *testing.T) {
  102. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
  103. cfg := NewConfig(nlCfg)
  104. s := newSource(cfg)
  105. _, err := s.ExtractNodeLabels(map[string]string{"config.yaml": ":\tinvalid: yaml: {"})
  106. require.Error(t, err)
  107. assert.Contains(t, err.Error(), "found character that cannot start any token")
  108. }
  109. func TestConfigMapSource_BlockScalar_RouteSegmentNotFound(t *testing.T) {
  110. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "does.not.exist")
  111. cfg := NewConfig(nlCfg)
  112. s := newSource(cfg)
  113. _, err := s.ExtractNodeLabels(map[string]string{"config.yaml": "foo: bar\n"})
  114. require.Error(t, err)
  115. assert.Contains(t, err.Error(), `failed to locate route`)
  116. }
  117. func TestConfigMapSource_BlockScalar_RouteSegmentNotANodeSequenceType(t *testing.T) {
  118. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels.nested")
  119. cfg := NewConfig(nlCfg)
  120. s := newSource(cfg)
  121. // labels is a string, not a map — traversing into it should fail
  122. _, err := s.ExtractNodeLabels(map[string]string{"config.yaml": "labels: just-a-string\n"})
  123. require.Error(t, err)
  124. assert.Contains(t, err.Error(), "is not a map")
  125. }
  126. func TestConfigMapSource_BlockScalar_RoutePointsToNodeScalarType(t *testing.T) {
  127. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
  128. cfg := NewConfig(nlCfg)
  129. s := newSource(cfg)
  130. // labels is a string, not a map — final conversion should fail
  131. _, err := s.ExtractNodeLabels(map[string]string{"config.yaml": "labels: just-a-string\n"})
  132. require.Error(t, err)
  133. assert.Contains(t, err.Error(), "is not a map")
  134. }
  135. func TestConfigMapSource_BlockScalar_BoolValuesConvertedToString(t *testing.T) {
  136. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
  137. cfg := NewConfig(nlCfg)
  138. s := newSource(cfg)
  139. yaml := `
  140. labels:
  141. active: true
  142. deprecated: false
  143. `
  144. res, err := s.ExtractNodeLabels(map[string]string{"config.yaml": yaml})
  145. require.NoError(t, err)
  146. assert.Equal(t, "true", res["active"])
  147. assert.Equal(t, "false", res["deprecated"])
  148. }
  149. func TestConfigMapSource_BlockScalar_IntValuesConvertedToString(t *testing.T) {
  150. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
  151. cfg := NewConfig(nlCfg)
  152. s := newSource(cfg)
  153. yaml := `
  154. labels:
  155. priority: 42
  156. replicas: 3
  157. `
  158. res, err := s.ExtractNodeLabels(map[string]string{"config.yaml": yaml})
  159. require.NoError(t, err)
  160. assert.Equal(t, "42", res["priority"])
  161. assert.Equal(t, "3", res["replicas"])
  162. }
  163. func TestConfigMapSource_BlockScalar_SequenceValueRejected(t *testing.T) {
  164. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
  165. cfg := NewConfig(nlCfg)
  166. s := newSource(cfg)
  167. yamlData := `
  168. labels:
  169. environments:
  170. - dev
  171. - staging
  172. - prod
  173. `
  174. res, err := s.ExtractNodeLabels(map[string]string{
  175. "config.yaml": yamlData,
  176. })
  177. require.Error(t, err)
  178. assert.Nil(t, res)
  179. assert.Contains(t, err.Error(), `cannot unmarshal !!seq into string`)
  180. }
  181. func TestConfigMapSource_BlockScalar_NestedMapValueRejected(t *testing.T) {
  182. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
  183. cfg := NewConfig(nlCfg)
  184. s := newSource(cfg)
  185. yamlData := `
  186. labels:
  187. inner-map:
  188. my-key: value
  189. other-key: value
  190. `
  191. res, err := s.ExtractNodeLabels(map[string]string{
  192. "config.yaml": yamlData,
  193. })
  194. require.Error(t, err)
  195. assert.Nil(t, res)
  196. assert.Contains(t, err.Error(), `unmarshal !!map into string`)
  197. }
  198. func TestConfigMapSource_BlockScalar_NullValues(t *testing.T) {
  199. tests := []struct {
  200. name string
  201. nullValue string
  202. }{
  203. {
  204. name: "explicit null",
  205. nullValue: "null",
  206. },
  207. {
  208. name: "tilde",
  209. nullValue: "~",
  210. },
  211. {
  212. name: "empty value",
  213. nullValue: "",
  214. },
  215. }
  216. for _, tt := range tests {
  217. t.Run(tt.name, func(t *testing.T) {
  218. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
  219. cfg := NewConfig(nlCfg)
  220. s := newSource(cfg)
  221. yamlData := fmt.Sprintf(`
  222. labels:
  223. priority: %s
  224. `, tt.nullValue)
  225. res, err := s.ExtractNodeLabels(map[string]string{
  226. "config.yaml": yamlData,
  227. })
  228. require.NoError(t, err)
  229. assert.Equal(t, "", res["priority"])
  230. })
  231. }
  232. }
  233. func TestConfigMapSource_BlockScalar_AliasValue(t *testing.T) {
  234. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
  235. cfg := NewConfig(nlCfg)
  236. s := newSource(cfg)
  237. yamlData := `
  238. defaultEnvironment: &defaultEnvironment production
  239. labels:
  240. environment: *defaultEnvironment
  241. `
  242. res, err := s.ExtractNodeLabels(map[string]string{
  243. "config.yaml": yamlData,
  244. })
  245. require.NoError(t, err)
  246. assert.Equal(t, "production", res["environment"])
  247. }
  248. func TestConfigMapSource_BlockScalar_ComplexYamlWithTwoUnsupportedNodeType(t *testing.T) {
  249. nlCfg := NewNodeLabelConfig("my-cm", "", "config.yaml", "labels")
  250. cfg := NewConfig(nlCfg)
  251. s := newSource(cfg)
  252. yaml := `
  253. labels:
  254. name: complex
  255. priority: 42
  256. replicas: 3
  257. other:
  258. - "hello"
  259. - "this"
  260. - "is"
  261. - "a"
  262. - "test"
  263. inner-map:
  264. my-key: "value"
  265. other-key: "value"
  266. `
  267. res, err := s.ExtractNodeLabels(map[string]string{"config.yaml": yaml})
  268. require.Error(t, err)
  269. assert.Nil(t, res)
  270. }
  271. // --- filterValidLabels ---
  272. func TestFilterValidLabels_AllValid_ReturnedUnchanged(t *testing.T) {
  273. in := map[string]string{
  274. "env": "prod",
  275. "region": "us-east-1",
  276. "app.kubernetes.io/name": "opencost",
  277. "my-key": "my-value",
  278. "a": "b",
  279. }
  280. out := filterValidLabels(in)
  281. assert.Equal(t, in, out)
  282. }
  283. func TestFilterValidLabels_EmptyInput_ReturnsEmptyMap(t *testing.T) {
  284. out := filterValidLabels(map[string]string{})
  285. assert.NotNil(t, out)
  286. assert.Empty(t, out)
  287. }
  288. func TestFilterValidLabels_InvalidKey_EntryDropped(t *testing.T) {
  289. in := map[string]string{
  290. "valid-key": "value",
  291. "": "empty-key-dropped",
  292. "-bad-start": "dropped",
  293. }
  294. out := filterValidLabels(in)
  295. assert.Equal(t, map[string]string{"valid-key": "value"}, out)
  296. }
  297. func TestFilterValidLabels_InvalidValue_EntryDropped(t *testing.T) {
  298. in := map[string]string{
  299. "good-key": "good-value",
  300. "bad-value": "-starts-with-dash",
  301. }
  302. out := filterValidLabels(in)
  303. assert.Equal(t, map[string]string{"good-key": "good-value"}, out)
  304. }
  305. func TestFilterValidLabels_ValueTooLong_EntryDropped(t *testing.T) {
  306. longValue := string(make([]byte, 64))
  307. for i := range longValue {
  308. longValue = longValue[:i] + "a" + longValue[i+1:]
  309. }
  310. in := map[string]string{
  311. "ok": "fine",
  312. "toolong": longValue,
  313. }
  314. out := filterValidLabels(in)
  315. assert.Equal(t, map[string]string{"ok": "fine"}, out)
  316. }
  317. func TestFilterValidLabels_KeyTooLong_EntryDropped(t *testing.T) {
  318. longKey := string(make([]byte, 64))
  319. for i := range longKey {
  320. longKey = longKey[:i] + "a" + longKey[i+1:]
  321. }
  322. in := map[string]string{
  323. "ok": "fine",
  324. longKey: "value",
  325. }
  326. out := filterValidLabels(in)
  327. assert.Equal(t, map[string]string{"ok": "fine"}, out)
  328. }
  329. func TestFilterValidLabels_PrefixedKey_Valid(t *testing.T) {
  330. in := map[string]string{
  331. "app.kubernetes.io/name": "opencost",
  332. "example.com/env": "staging",
  333. }
  334. out := filterValidLabels(in)
  335. assert.Equal(t, in, out)
  336. }
  337. func TestFilterValidLabels_PrefixedKey_InvalidPrefix_EntryDropped(t *testing.T) {
  338. in := map[string]string{
  339. "valid": "kept",
  340. "-bad.prefix/k": "dropped",
  341. }
  342. out := filterValidLabels(in)
  343. assert.Equal(t, map[string]string{"valid": "kept"}, out)
  344. }
  345. func TestFilterValidLabels_MixedValidAndInvalid_OnlyValidReturned(t *testing.T) {
  346. in := map[string]string{
  347. "cluster": "prod",
  348. "": "no-key",
  349. "bad-value": "-oops",
  350. "region": "eu-west-1",
  351. }
  352. out := filterValidLabels(in)
  353. assert.Equal(t, map[string]string{"cluster": "prod", "region": "eu-west-1"}, out)
  354. }