diff --git a/data/k8s/dev/devteam/bigquerydataset.yaml b/data/k8s/dev/devteam/bigquerydataset.yaml index 8622b7454..804f37545 100644 --- a/data/k8s/dev/devteam/bigquerydataset.yaml +++ b/data/k8s/dev/devteam/bigquerydataset.yaml @@ -6,7 +6,7 @@ metadata: - bqrator.nais.io/finalizer generation: 1 labels: - labels.nais.io/key: value + key: value app: app-w-all-storage team: devteam name: app-w-all-storage-deleteme_bq-23424asf324 diff --git a/data/k8s/dev/devteam/cm.yaml b/data/k8s/dev/devteam/cm.yaml index 8738da2db..ec1ef5f49 100644 --- a/data/k8s/dev/devteam/cm.yaml +++ b/data/k8s/dev/devteam/cm.yaml @@ -8,7 +8,7 @@ metadata: console.nais.io/last-modified-by: bob.ross@nais.io reloader.stakater.com/match: "true" labels: - labels.nais.io/key: value + key: value app.kubernetes.io/managed-by: console nais.io/managed-by: console name: asdadasd diff --git a/data/k8s/dev/devteam/deploy-canary.yaml b/data/k8s/dev/devteam/deploy-canary.yaml index 559d20464..4791259c2 100644 --- a/data/k8s/dev/devteam/deploy-canary.yaml +++ b/data/k8s/dev/devteam/deploy-canary.yaml @@ -2,7 +2,7 @@ apiVersion: nais.io/v1alpha1 kind: Application metadata: labels: - labels.nais.io/key: value + key: value annotations: deploy.nais.io/client-version: 2023-01-23-7071cd7 nais.io/deploymentCorrelationID: f8c04f82-6a84-4a8e-9f8b-563b5894d0cf diff --git a/data/k8s/dev/devteam/job_running.yaml b/data/k8s/dev/devteam/job_running.yaml index aaccefc4a..4dd427331 100644 --- a/data/k8s/dev/devteam/job_running.yaml +++ b/data/k8s/dev/devteam/job_running.yaml @@ -3,7 +3,7 @@ apiVersion: nais.io/v1 kind: Naisjob metadata: labels: - labels.nais.io/key: value + key: value annotations: deploy.nais.io/client-version: 2023-12-19-bbbb39b deploy.nais.io/github-actor: jhrv diff --git a/data/k8s/dev/devteam/opensearch.yaml b/data/k8s/dev/devteam/opensearch.yaml index 901549c22..8c3fd294b 100644 --- a/data/k8s/dev/devteam/opensearch.yaml +++ b/data/k8s/dev/devteam/opensearch.yaml @@ -17,7 +17,7 @@ metadata: name: app-w-all-storage uid: 5d684281-5621-4c87-aee7-fca257550790 labels: - labels.nais.io/key: value + key: value team: devteam app: app-w-all-storage "nais.io/managed-by": console diff --git a/data/k8s/dev/devteam/postgres.yaml b/data/k8s/dev/devteam/postgres.yaml index 496a448bb..20069a296 100644 --- a/data/k8s/dev/devteam/postgres.yaml +++ b/data/k8s/dev/devteam/postgres.yaml @@ -4,7 +4,7 @@ metadata: name: postgres-1 namespace: devteam labels: - labels.nais.io/key: value + key: value team: devteam spec: cluster: diff --git a/data/k8s/dev/devteam/secrets.yaml b/data/k8s/dev/devteam/secrets.yaml index 2d299743e..43e7e114e 100644 --- a/data/k8s/dev/devteam/secrets.yaml +++ b/data/k8s/dev/devteam/secrets.yaml @@ -12,7 +12,7 @@ metadata: console.nais.io/last-modified-at: "2021-01-01T00:00:00Z" console.nais.io/last-modified-by: "dev.usersen@example.com" labels: - labels.nais.io/key: value + key: value nais.io/managed-by: console foo: bar bar: baz diff --git a/data/k8s/dev/devteam/sqlinstance.yaml b/data/k8s/dev/devteam/sqlinstance.yaml index 27b8c85c8..5b793ff55 100644 --- a/data/k8s/dev/devteam/sqlinstance.yaml +++ b/data/k8s/dev/devteam/sqlinstance.yaml @@ -10,7 +10,7 @@ metadata: cnrm.cloud.google.com/state-into-spec: merge nais.io/deploymentCorrelationID: c53778c4-f03f-41a5-ae2f-0ca6a833019e labels: - labels.nais.io/key: value + key: value app: app-w-all-storage app.kubernetes.io/instance: app-w-all-storage app.kubernetes.io/managed-by: Helm diff --git a/data/k8s/dev/devteam/storagebucket.yaml b/data/k8s/dev/devteam/storagebucket.yaml index 3a97dcc27..920721a15 100644 --- a/data/k8s/dev/devteam/storagebucket.yaml +++ b/data/k8s/dev/devteam/storagebucket.yaml @@ -15,7 +15,7 @@ metadata: labels: app: app-w-all-storage team: devteam - labels.nais.io/key: value + key: value name: uniquebucketname-bucket namespace: dev spec: diff --git a/data/k8s/dev/devteam/topic.yaml b/data/k8s/dev/devteam/topic.yaml index 3e85fe132..69de3f26e 100644 --- a/data/k8s/dev/devteam/topic.yaml +++ b/data/k8s/dev/devteam/topic.yaml @@ -19,7 +19,7 @@ metadata: generation: 215 labels: team: devteam - labels.nais.io/key: value + key: value name: rapido.the.first namespace: devteam resourceVersion: "4941946441" diff --git a/data/k8s/dev/devteam/valkey.yaml b/data/k8s/dev/devteam/valkey.yaml index de4edb1a8..c57eec938 100644 --- a/data/k8s/dev/devteam/valkey.yaml +++ b/data/k8s/dev/devteam/valkey.yaml @@ -7,7 +7,7 @@ metadata: controllers.aiven.io/instance-is-running: "true" nais.io/deploymentCorrelationID: 61ad0b73-e30f-401a-949c-b8810c2b509a labels: - labels.nais.io/key: value + key: value app: app-w-all-storage team: devteam name: valkey-devteam-contests diff --git a/integration_tests/configs.lua b/integration_tests/configs.lua index efec73b3a..f71117af0 100644 --- a/integration_tests/configs.lua +++ b/integration_tests/configs.lua @@ -1513,7 +1513,7 @@ Test.gql("Update config labels", function(t) }, } - -- Update labels successfully (fully qualified) + -- Update labels successfully with a custom label (non-reserved) t.query [[ mutation { updateConfig(input: { @@ -1521,7 +1521,7 @@ Test.gql("Update config labels", function(t) environmentName: "dev" teamSlug: "myteam" labels: [ - { key: "labels.nais.io/tag", value: "testing" } + { key: "my-custom-key", value: "testing" } ] }) { config { name labels { key value } } @@ -1535,14 +1535,14 @@ Test.gql("Update config labels", function(t) config = { name = "labels-test-config", labels = { - { key = "labels.nais.io/tag", value = "testing" }, + { key = "my-custom-key", value = "testing" }, }, }, }, }, } - -- Try updating with an invalid key (no prefix) -> should fail validation + -- Try updating with a reserved key -> should fail validation t.query [[ mutation { updateConfig(input: { @@ -1550,7 +1550,7 @@ Test.gql("Update config labels", function(t) environmentName: "dev" teamSlug: "myteam" labels: [ - { key: "tag", value: "invalid" } + { key: "app", value: "invalid" } ] }) { config { name } @@ -1562,7 +1562,7 @@ Test.gql("Update config labels", function(t) errors = { { locations = NotNull(), - message = Contains("label key \"tag\" must be prefixed with \"labels.nais.io/\""), + message = Contains("is reserved"), path = { "updateConfig", }, @@ -1570,4 +1570,78 @@ Test.gql("Update config labels", function(t) }, data = Null, } + + -- Update labels and explicitly specify app.kubernetes.io/managed-by: Helm (value is not console, so it's a valid user label and gets set) + t.query [[ + mutation { + updateConfig(input: { + name: "labels-test-config" + environmentName: "dev" + teamSlug: "myteam" + labels: [ + { key: "app.kubernetes.io/managed-by", value: "Helm" } + ] + }) { + config { name labels { key value } } + } + } + ]] + + t.check { + data = { + updateConfig = { + config = { + name = "labels-test-config", + labels = { + { key = "app.kubernetes.io/managed-by", value = "Helm" }, + }, + }, + }, + }, + } + + -- Update labels again with another custom label -> app.kubernetes.io/managed-by: Helm must be removed because value is not console! + t.query [[ + mutation { + updateConfig(input: { + name: "labels-test-config" + environmentName: "dev" + teamSlug: "myteam" + labels: [ + { key: "my-custom-key", value: "second-test" } + ] + }) { + config { name labels { key value } } + } + } + ]] + + t.check { + data = { + updateConfig = { + config = { + name = "labels-test-config", + labels = { + { key = "my-custom-key", value = "second-test" }, + }, + }, + }, + }, + } +end) + +Test.k8s("Validate config labels after update", function(t) + t.check("v1", "configmaps", "dev", "myteam", "labels-test-config", { + apiVersion = Ignore(), + kind = Ignore(), + metadata = { + name = "labels-test-config", + namespace = "myteam", + annotations = Ignore(), + labels = { + ["nais.io/managed-by"] = "console", + ["my-custom-key"] = "second-test", + }, + }, + }) end) diff --git a/integration_tests/k8s_resources/label_selectors/dev/labelteam/applications.yaml b/integration_tests/k8s_resources/label_selectors/dev/labelteam/applications.yaml index 64744f3e5..542f5a359 100644 --- a/integration_tests/k8s_resources/label_selectors/dev/labelteam/applications.yaml +++ b/integration_tests/k8s_resources/label_selectors/dev/labelteam/applications.yaml @@ -5,8 +5,8 @@ metadata: name: app-one namespace: labelteam labels: - labels.nais.io/tag: target - labels.nais.io/priority: high + tag: target + priority: high spec: image: navikt/app-one:latest --- @@ -16,7 +16,7 @@ metadata: name: app-two namespace: labelteam labels: - labels.nais.io/tag: target + tag: target spec: image: navikt/app-two:latest --- @@ -26,6 +26,6 @@ metadata: name: app-three namespace: labelteam labels: - labels.nais.io/tag: other + tag: other spec: image: navikt/app-three:latest diff --git a/integration_tests/k8s_resources/label_selectors/dev/labelteam/jobs.yaml b/integration_tests/k8s_resources/label_selectors/dev/labelteam/jobs.yaml index 67328c2c5..cf70cd13f 100644 --- a/integration_tests/k8s_resources/label_selectors/dev/labelteam/jobs.yaml +++ b/integration_tests/k8s_resources/label_selectors/dev/labelteam/jobs.yaml @@ -5,8 +5,8 @@ metadata: name: job-one namespace: labelteam labels: - labels.nais.io/tag: target - labels.nais.io/priority: high + tag: target + priority: high spec: image: navikt/job-one:latest --- @@ -16,7 +16,7 @@ metadata: name: job-two namespace: labelteam labels: - labels.nais.io/tag: target + tag: target spec: image: navikt/job-two:latest --- @@ -26,6 +26,6 @@ metadata: name: job-three namespace: labelteam labels: - labels.nais.io/tag: other + tag: other spec: image: navikt/job-three:latest diff --git a/integration_tests/k8s_resources/label_selectors/dev/labelteam/postgres.yaml b/integration_tests/k8s_resources/label_selectors/dev/labelteam/postgres.yaml index 5d26c3442..7b92807cd 100644 --- a/integration_tests/k8s_resources/label_selectors/dev/labelteam/postgres.yaml +++ b/integration_tests/k8s_resources/label_selectors/dev/labelteam/postgres.yaml @@ -5,8 +5,8 @@ metadata: name: postgres-one namespace: labelteam labels: - labels.nais.io/tag: target - labels.nais.io/priority: high + tag: target + priority: high spec: cluster: majorVersion: "17" @@ -23,7 +23,7 @@ metadata: name: postgres-two namespace: labelteam labels: - labels.nais.io/tag: target + tag: target spec: cluster: majorVersion: "17" @@ -40,7 +40,7 @@ metadata: name: postgres-three namespace: labelteam labels: - labels.nais.io/tag: other + tag: other spec: cluster: majorVersion: "17" diff --git a/integration_tests/k8s_resources/label_selectors/dev/labelteam/valkeys.yaml b/integration_tests/k8s_resources/label_selectors/dev/labelteam/valkeys.yaml index cd461edf5..8b4ed23d3 100644 --- a/integration_tests/k8s_resources/label_selectors/dev/labelteam/valkeys.yaml +++ b/integration_tests/k8s_resources/label_selectors/dev/labelteam/valkeys.yaml @@ -5,8 +5,8 @@ metadata: name: valkey-one namespace: labelteam labels: - labels.nais.io/tag: target - labels.nais.io/priority: high + tag: target + priority: high spec: plan: startup-4 --- @@ -16,7 +16,7 @@ metadata: name: valkey-two namespace: labelteam labels: - labels.nais.io/tag: target + tag: target spec: plan: startup-4 --- @@ -26,6 +26,6 @@ metadata: name: valkey-three namespace: labelteam labels: - labels.nais.io/tag: other + tag: other spec: plan: startup-4 diff --git a/integration_tests/label_selectors.lua b/integration_tests/label_selectors.lua index fa8a39336..20a087769 100644 --- a/integration_tests/label_selectors.lua +++ b/integration_tests/label_selectors.lua @@ -38,20 +38,20 @@ Test.gql("Check all Valkey instances (no filter)", function(t) { name = "valkey-one", labels = { - { key = "labels.nais.io/priority", value = "high" }, - { key = "labels.nais.io/tag", value = "target" }, + { key = "priority", value = "high" }, + { key = "tag", value = "target" }, }, }, { name = "valkey-three", labels = { - { key = "labels.nais.io/tag", value = "other" }, + { key = "tag", value = "other" }, }, }, { name = "valkey-two", labels = { - { key = "labels.nais.io/tag", value = "target" }, + { key = "tag", value = "target" }, }, }, }, @@ -66,7 +66,7 @@ Test.gql("Valkey filter by tag=target", function(t) t.query [[ { team(slug: "labelteam") { - valkeys(filter: { labels: [{ key: "labels.nais.io/tag", value: "target" }] }) { + valkeys(filter: { labels: [{ key: "tag", value: "target" }] }) { pageInfo { totalCount } @@ -102,8 +102,8 @@ Test.gql("Valkey filter by tag=target and priority=high", function(t) team(slug: "labelteam") { valkeys(filter: { labels: [ - { key: "labels.nais.io/tag", value: "target" }, - { key: "labels.nais.io/priority", value: "high" } + { key: "tag", value: "target" }, + { key: "priority", value: "high" } ] }) { pageInfo { @@ -167,20 +167,20 @@ Test.gql("Check all Postgres instances (no filter)", function(t) { name = "postgres-one", labels = { - { key = "labels.nais.io/priority", value = "high" }, - { key = "labels.nais.io/tag", value = "target" }, + { key = "priority", value = "high" }, + { key = "tag", value = "target" }, }, }, { name = "postgres-three", labels = { - { key = "labels.nais.io/tag", value = "other" }, + { key = "tag", value = "other" }, }, }, { name = "postgres-two", labels = { - { key = "labels.nais.io/tag", value = "target" }, + { key = "tag", value = "target" }, }, }, }, @@ -195,7 +195,7 @@ Test.gql("Postgres filter by tag=target", function(t) t.query [[ { team(slug: "labelteam") { - postgresInstances(filter: { labels: [{ key: "labels.nais.io/tag", value: "target" }] }) { + postgresInstances(filter: { labels: [{ key: "tag", value: "target" }] }) { pageInfo { totalCount } @@ -231,8 +231,8 @@ Test.gql("Postgres filter by tag=target and priority=high", function(t) team(slug: "labelteam") { postgresInstances(filter: { labels: [ - { key: "labels.nais.io/tag", value: "target" }, - { key: "labels.nais.io/priority", value: "high" } + { key: "tag", value: "target" }, + { key: "priority", value: "high" } ] }) { pageInfo { @@ -296,20 +296,20 @@ Test.gql("Check all applications (no filter)", function(t) { name = "app-one", labels = { - { key = "labels.nais.io/priority", value = "high" }, - { key = "labels.nais.io/tag", value = "target" }, + { key = "priority", value = "high" }, + { key = "tag", value = "target" }, }, }, { name = "app-three", labels = { - { key = "labels.nais.io/tag", value = "other" }, + { key = "tag", value = "other" }, }, }, { name = "app-two", labels = { - { key = "labels.nais.io/tag", value = "target" }, + { key = "tag", value = "target" }, }, }, }, @@ -324,7 +324,7 @@ Test.gql("Application filter by tag=target", function(t) t.query [[ { team(slug: "labelteam") { - applications(filter: { labels: [{ key: "labels.nais.io/tag", value: "target" }] }) { + applications(filter: { labels: [{ key: "tag", value: "target" }] }) { pageInfo { totalCount } @@ -360,8 +360,8 @@ Test.gql("Application filter by tag=target and priority=high", function(t) team(slug: "labelteam") { applications(filter: { labels: [ - { key: "labels.nais.io/tag", value: "target" }, - { key: "labels.nais.io/priority", value: "high" } + { key: "tag", value: "target" }, + { key: "priority", value: "high" } ] }) { pageInfo { @@ -425,20 +425,20 @@ Test.gql("Check all jobs (no filter)", function(t) { name = "job-one", labels = { - { key = "labels.nais.io/priority", value = "high" }, - { key = "labels.nais.io/tag", value = "target" }, + { key = "priority", value = "high" }, + { key = "tag", value = "target" }, }, }, { name = "job-three", labels = { - { key = "labels.nais.io/tag", value = "other" }, + { key = "tag", value = "other" }, }, }, { name = "job-two", labels = { - { key = "labels.nais.io/tag", value = "target" }, + { key = "tag", value = "target" }, }, }, }, @@ -453,7 +453,7 @@ Test.gql("Job filter by tag=target", function(t) t.query [[ { team(slug: "labelteam") { - jobs(filter: { name: "", labels: [{ key: "labels.nais.io/tag", value: "target" }] }) { + jobs(filter: { name: "", labels: [{ key: "tag", value: "target" }] }) { pageInfo { totalCount } @@ -490,8 +490,8 @@ Test.gql("Job filter by tag=target and priority=high", function(t) jobs(filter: { name: "" labels: [ - { key: "labels.nais.io/tag", value: "target" }, - { key: "labels.nais.io/priority", value: "high" } + { key: "tag", value: "target" }, + { key: "priority", value: "high" } ] }) { pageInfo { @@ -521,12 +521,12 @@ Test.gql("Job filter by tag=target and priority=high", function(t) } end) -Test.gql("Valkey filter with invalid label prefix", function(t) +Test.gql("Valkey filter with reserved label key", function(t) t.addHeader("x-user-email", user:email()) t.query [[ { team(slug: "labelteam") { - valkeys(filter: { labels: [{ key: "tag", value: "target" }] }) { + valkeys(filter: { labels: [{ key: "app", value: "target" }] }) { pageInfo { totalCount } @@ -538,7 +538,7 @@ Test.gql("Valkey filter with invalid label prefix", function(t) t.check { errors = { { - message = Contains("label key \"tag\" must be prefixed with \"labels.nais.io/\""), + message = Contains("Label key \"app\" is reserved and cannot be used"), path = { "team", "valkeys", @@ -575,9 +575,9 @@ Test.gql("Check labels facets on Valkey connection", function(t) valkeys = { facets = { labels = { - { key = "labels.nais.io/priority", value = "high", count = 1 }, - { key = "labels.nais.io/tag", value = "other", count = 1 }, - { key = "labels.nais.io/tag", value = "target", count = 2 }, + { key = "priority", value = "high", count = 1 }, + { key = "tag", value = "other", count = 1 }, + { key = "tag", value = "target", count = 2 }, }, }, }, diff --git a/integration_tests/secrets.lua b/integration_tests/secrets.lua index e7e776ef0..6ea9a351d 100644 --- a/integration_tests/secrets.lua +++ b/integration_tests/secrets.lua @@ -1008,7 +1008,7 @@ Test.gql("Update secret labels", function(t) environmentName: "dev" teamSlug: "myteam" labels: [ - { key: "labels.nais.io/tag", value: "testing" } + { key: "tag", value: "testing" } ] }) { secret { name labels { key value } } @@ -1022,14 +1022,14 @@ Test.gql("Update secret labels", function(t) secret = { name = "labels-test-secret", labels = { - { key = "labels.nais.io/tag", value = "testing" }, + { key = "tag", value = "testing" }, }, }, }, }, } - -- Try updating with an invalid key (no prefix) -> should fail validation + -- Try updating with a reserved key -> should fail validation t.query [[ mutation { updateSecret(input: { @@ -1037,7 +1037,7 @@ Test.gql("Update secret labels", function(t) environmentName: "dev" teamSlug: "myteam" labels: [ - { key: "tag", value: "invalid" } + { key: "app", value: "invalid" } ] }) { secret { name } @@ -1049,7 +1049,7 @@ Test.gql("Update secret labels", function(t) errors = { { locations = NotNull(), - message = Contains("label key \"tag\" must be prefixed with \"labels.nais.io/\""), + message = Contains("is reserved"), path = { "updateSecret", }, diff --git a/integration_tests/valkey_crud.lua b/integration_tests/valkey_crud.lua index 4f9aabc80..312706067 100644 --- a/integration_tests/valkey_crud.lua +++ b/integration_tests/valkey_crud.lua @@ -1071,3 +1071,247 @@ Test.gql("Verify otherTeam activity log is isolated from mainTeam", function(t) }, } end) + +Test.gql("Create Valkey to test labels", function(t) + t.addHeader("x-user-email", user:email()) + t.query [[ + mutation CreateValkey { + createValkey( + input: { + name: "labels-valkey" + environmentName: "dev" + teamSlug: "someteamname" + tier: SINGLE_NODE + memory: GB_14 + } + ) { + valkey { + name + labels { + key + value + } + } + } + } + ]] + + t.check { + data = { + createValkey = { + valkey = { + name = "labels-valkey", + labels = {}, + }, + }, + }, + } +end) + +Test.gql("Update Valkey labels successfully", function(t) + t.addHeader("x-user-email", user:email()) + t.query [[ + mutation UpdateValkey { + updateValkey( + input: { + name: "labels-valkey" + environmentName: "dev" + teamSlug: "someteamname" + tier: SINGLE_NODE + memory: GB_14 + labels: [ + { key: "my-custom-key", value: "testing" } + ] + } + ) { + valkey { + name + labels { + key + value + } + } + } + } + ]] + + t.check { + data = { + updateValkey = { + valkey = { + name = "labels-valkey", + labels = { + { key = "my-custom-key", value = "testing" }, + }, + }, + }, + }, + } +end) + +Test.gql("Update Valkey labels with reserved key -> should fail validation", function(t) + t.addHeader("x-user-email", user:email()) + t.query [[ + mutation UpdateValkey { + updateValkey( + input: { + name: "labels-valkey" + environmentName: "dev" + teamSlug: "someteamname" + tier: SINGLE_NODE + memory: GB_14 + labels: [ + { key: "app", value: "invalid" } + ] + } + ) { + valkey { + name + } + } + } + ]] + + t.check { + errors = { + { + message = Contains("is reserved"), + path = { + "updateValkey", + }, + extensions = { + field = "labels", + }, + }, + }, + data = Null, + } +end) + +Test.gql("Update Valkey labels to specify app.kubernetes.io/managed-by: Helm", function(t) + t.addHeader("x-user-email", user:email()) + t.query [[ + mutation UpdateValkey { + updateValkey( + input: { + name: "labels-valkey" + environmentName: "dev" + teamSlug: "someteamname" + tier: SINGLE_NODE + memory: GB_14 + labels: [ + { key: "app.kubernetes.io/managed-by", value: "Helm" } + ] + } + ) { + valkey { + name + labels { + key + value + } + } + } + } + ]] + + t.check { + data = { + updateValkey = { + valkey = { + name = "labels-valkey", + labels = { + { key = "app.kubernetes.io/managed-by", value = "Helm" }, + }, + }, + }, + }, + } +end) + +Test.gql( + "Update Valkey labels again -> app.kubernetes.io/managed-by: Helm must be removed because value is not console!", + function(t) + t.addHeader("x-user-email", user:email()) + t.query [[ + mutation UpdateValkey { + updateValkey( + input: { + name: "labels-valkey" + environmentName: "dev" + teamSlug: "someteamname" + tier: SINGLE_NODE + memory: GB_14 + labels: [ + { key: "my-custom-key", value: "second-test" } + ] + } + ) { + valkey { + name + labels { + key + value + } + } + } + } + ]] + + t.check { + data = { + updateValkey = { + valkey = { + name = "labels-valkey", + labels = { + { key = "my-custom-key", value = "second-test" }, + }, + }, + }, + }, + } + end) + +Test.k8s("Validate Valkey labels after update", function(t) + local resourceName = string.format("valkey-%s-labels-valkey", mainTeam:slug()) + + t.check("aiven.io/v1alpha1", "valkeys", "dev", mainTeam:slug(), resourceName, { + apiVersion = "aiven.io/v1alpha1", + kind = "Valkey", + metadata = { + name = resourceName, + namespace = mainTeam:slug(), + annotations = Ignore(), + labels = { + ["nais.io/managed-by"] = "console", + ["my-custom-key"] = "second-test", + }, + }, + spec = Ignore(), + }) +end) + +Test.gql("Clean up labels Valkey", function(t) + t.addHeader("x-user-email", user:email()) + t.query [[ + mutation DeleteValkey { + deleteValkey( + input: { + name: "labels-valkey" + environmentName: "dev" + teamSlug: "someteamname" + } + ) { + valkeyDeleted + } + } + ]] + + t.check { + data = { + deleteValkey = { + valkeyDeleted = true, + }, + }, + } +end) diff --git a/internal/graph/gengql/root_.generated.go b/internal/graph/gengql/root_.generated.go index e3a77e5c9..3261a9c2a 100644 --- a/internal/graph/gengql/root_.generated.go +++ b/internal/graph/gengql/root_.generated.go @@ -24343,8 +24343,6 @@ A user-defined label attached to a resource. Labels are key-value pairs that teams can use to organize and filter their resources. Both the key and the value may contain letters, numbers, hyphens, underscores and dots, and may be at most 63 characters long. - -Keys must be prefixed with labels.nais.io/. """ type ResourceLabel { "The label key." diff --git a/internal/graph/model/labels.go b/internal/graph/model/labels.go index 9394234ed..1fc5a7325 100644 --- a/internal/graph/model/labels.go +++ b/internal/graph/model/labels.go @@ -12,11 +12,6 @@ import ( "k8s.io/apimachinery/pkg/util/validation" ) -// UserLabelPrefix is the namespace under which user-defined labels are stored on -// Nais resources. Only labels with this prefix are user-editable and exposed -// through the API; all other labels are considered platform-internal. -const UserLabelPrefix = "labels.nais.io/" - type ResourceLabel struct { Key string `json:"key"` Value string `json:"value"` @@ -74,8 +69,8 @@ func ValidateUserLabels(labels []*ResourceLabel) error { } seen[l.Key] = struct{}{} - if !strings.HasPrefix(l.Key, UserLabelPrefix) { - return LabelValidationError{Message: fmt.Sprintf("label key %q must be prefixed with %q", l.Key, UserLabelPrefix)} + if HiddenLabelKey(l.Key, l.Value) { + return LabelValidationError{Message: fmt.Sprintf("Label key %q is reserved", l.Key)} } for _, msg := range validation.IsQualifiedName(l.Key) { @@ -103,14 +98,16 @@ func (l *LabelFilters) UnmarshalGQL(v any) error { key, _ := itemMap["key"].(string) - if !strings.HasPrefix(key, UserLabelPrefix) { - return LabelValidationError{Message: fmt.Sprintf("label key %q must be prefixed with %q", key, UserLabelPrefix)} - } - + var value string var valuePtr *string if val, ok := itemMap["value"]; ok && val != nil { valStr, _ := val.(string) valuePtr = &valStr + value = valStr + } + + if HiddenLabelKey(key, value) { + return LabelValidationError{Message: fmt.Sprintf("Label key %q is reserved and cannot be used", key)} } filters = append(filters, &LabelFilter{ @@ -144,7 +141,7 @@ func (l LabelFilters) MarshalGQL(w io.Writer) { func UserLabels(labels map[string]string) []*ResourceLabel { out := make([]*ResourceLabel, 0, len(labels)) for k, v := range labels { - if !strings.HasPrefix(k, UserLabelPrefix) && k != UserLabelPrefix { + if HiddenLabelKey(k, v) { continue } out = append(out, &ResourceLabel{Key: k, Value: v}) @@ -208,7 +205,7 @@ func MatchesLabelFilters(labels []*ResourceLabel, filters []*LabelFilter) bool { func MergeUserLabels(existing map[string]string, desired []*ResourceLabel) map[string]string { out := make(map[string]string, len(existing)+len(desired)) for k, v := range existing { - if strings.HasPrefix(k, UserLabelPrefix) { + if !HiddenLabelKey(k, v) { continue } out[k] = v @@ -223,3 +220,15 @@ func MergeUserLabels(existing map[string]string, desired []*ResourceLabel) map[s return out } + +// HiddenLabelKey return true if the label key is considered managed by the platform +func HiddenLabelKey(key, value string) bool { + switch key { + case "app", "team", "euthanaisa.nais.io/kill-after": + return true + case "app.kubernetes.io/managed-by": + return value == "console" + } + + return strings.Contains(key, "nais.io/") +} diff --git a/internal/graph/model/labels_test.go b/internal/graph/model/labels_test.go new file mode 100644 index 000000000..4cd566e2a --- /dev/null +++ b/internal/graph/model/labels_test.go @@ -0,0 +1,89 @@ +package model_test + +import ( + "testing" + + "github.com/nais/api/internal/graph/model" +) + +func TestHiddenLabelKey(t *testing.T) { + tests := []struct { + key string + val string + want bool + }{ + {"app", "foo", true}, + {"team", "bar", true}, + {"euthanaisa.nais.io/kill-after", "12345", true}, + {"app.kubernetes.io/managed-by", "console", true}, + {"app.kubernetes.io/managed-by", "Helm", false}, + {"my-custom-label", "whatever", false}, + {"nais.io/something", "any", true}, + } + + for _, tt := range tests { + got := model.HiddenLabelKey(tt.key, tt.val) + if got != tt.want { + t.Errorf("HiddenLabelKey(%q, %q) = %v; want %v", tt.key, tt.val, got, tt.want) + } + } +} + +func TestMergeUserLabels(t *testing.T) { + existing := map[string]string{ + "app.kubernetes.io/managed-by": "Helm", + "nais.io/managed-by": "console", + "my-custom-key": "testing", + } + + desired := []*model.ResourceLabel{ + {Key: "another-key", Value: "val"}, + } + + got := model.MergeUserLabels(existing, desired) + + // Since app.kubernetes.io/managed-by was "Helm" (not "console"), it should be removed! + if _, ok := got["app.kubernetes.io/managed-by"]; ok { + t.Errorf("expected app.kubernetes.io/managed-by: Helm to be removed, but it was kept") + } + + // nais.io/managed-by is reserved/hidden, so it must be kept + if val := got["nais.io/managed-by"]; val != "console" { + t.Errorf("expected nais.io/managed-by: console to be kept, but got %q", val) + } + + // my-custom-key is a user label and not in desired, so it should be removed + if _, ok := got["my-custom-key"]; ok { + t.Errorf("expected my-custom-key to be removed, but it was kept") + } + + // another-key was in desired, so it must be added + if val := got["another-key"]; val != "val" { + t.Errorf("expected another-key: val, but got %q", val) + } +} + +func TestUserLabels(t *testing.T) { + labels := map[string]string{ + "app.kubernetes.io/managed-by": "Helm", + "nais.io/managed-by": "console", + "my-custom-key": "testing", + } + + got := model.UserLabels(labels) + + if len(got) != 2 { + t.Errorf("expected 2 user labels, got %d", len(got)) + } + + // app.kubernetes.io/managed-by: Helm should be exposed to user + foundHelm := false + for _, l := range got { + if l.Key == "app.kubernetes.io/managed-by" && l.Value == "Helm" { + foundHelm = true + } + } + if !foundHelm { + t.Errorf("expected app.kubernetes.io/managed-by: Helm to be returned as user label") + } +} diff --git a/internal/graph/schema/labels.graphqls b/internal/graph/schema/labels.graphqls index a837d679d..1efc192c0 100644 --- a/internal/graph/schema/labels.graphqls +++ b/internal/graph/schema/labels.graphqls @@ -4,8 +4,6 @@ A user-defined label attached to a resource. Labels are key-value pairs that teams can use to organize and filter their resources. Both the key and the value may contain letters, numbers, hyphens, underscores and dots, and may be at most 63 characters long. - -Keys must be prefixed with labels.nais.io/. """ type ResourceLabel { "The label key."