From 5a93e862ee28b8804f681ae15997a00bdc45d1bf Mon Sep 17 00:00:00 2001 From: Luke Tornquist Date: Thu, 8 Oct 2026 15:12:00 -0400 Subject: [PATCH] fix(chart): own the Datadog version label on every workload Pod (#7876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Every chart-managed Pod now carries a chart-owned `tags.datadoghq.com/version` label derived from the deployed image, instead of a value each wrapper restates once per workload. Covered Pod templates: - relay Deployment (`templates/deployment.yaml`) - pairing relay (`templates/pairing-relay.yaml`) - storage-accounting CronJob (`templates/storage-accounting-cronjob.yaml`) - operator jobs, including the deletion drain (`templates/_operator-jobs.tpl`) **Source order:** `image.tag`, then `image.digest`, then `Chart.AppVersion` (`buzz.imageVersionSource`). Tag-first is deliberate: - The rendered value matches the `sha-` pins wrappers already carry, so upgrading to this chart does not roll any Pods. - An image promotion only has to write `image.tag` + `image.digest`. No per-workload label edit is needed. Exact runtime identity stays exact in two places. The image reference is `repository@digest` when a digest is pinned. `BUZZ_STORAGE_SNAPSHOT_CODE_SHA` (persisted as `code_sha`) is still digest-first. **Label safety:** `buzz.imageVersionLabel` maps any OCI tag or digest to a valid label value. - Valid values already in Datadog's normal form (lowercase, no `__`) pass through byte for byte, so `sha-` and `1.2.3-rc.4` are unchanged. Datadog lowercases tag values and collapses `__`, so any other value is hashed rather than allowed to report the same version as a different revision. - A digest keeps 63 hex characters. - Anything else is replaced by the first 63 hex characters of its SHA-256. - Exactly 63 lowercase hex characters is reserved for the hashed and digest forms, so a rendered label cannot be fed back as a tag to make two revisions collide. - When both tag and digest are set, the label names the tag even if they disagree; the digest still pins what runs. **Precedence:** a `tags.datadoghq.com/version` supplied through any workload's `podLabels` is ignored. Every other label, including `tags.datadoghq.com/env` and `/service`, passes through unchanged. Overriding instead of failing means a wrapper with a stale pin is corrected by the upgrade itself. The chart bump and the wrapper cleanup don't have to land together. **What got simpler:** one image identity instead of N. Wrappers delete their per-workload version pins. Image promotion tooling no longer needs a "mirror the tag into a label path" feature. One shared helper (`buzz.podLabelsWithVersion`) replaces four `podLabels` passthroughs. Chart `0.1.10 → 0.1.11`, with an `artifacthub.io/changes` entry. Documented in the chart README ("Datadog version label") and `docs/deployment-identity.md`. Note: a vendor-namespaced label now renders unconditionally in an OSS chart. This is in tension with [VISION_SOVEREIGN.md](../blob/main/VISION_SOVEREIGN.md). The label is inert without the operator-supplied `tags.datadoghq.com/service`/`env`, and the chart already ships vendor-specific defaults (e.g. `sidecar.istio.io/inject`). ### Testing Run on a Blox (Linux) workstation at tree `41634b7ca` (helm v3.16.4, helm-unittest 0.8.2, the CI versions): - `helm lint deploy/charts/buzz`: 0 failed - `helm unittest deploy/charts/buzz`: 12 suites, **85/85 passed** New `tests/version_label_test.yaml` checks the relay, pairing relay, and deletion drain: tag wins over a stale `podLabels` pin, the label is stamped with no `podLabels`, and digest-only images get a label-safe value. `tests/storage_accounting_test.yaml` covers the same cases for the CronJob, plus the `Chart.AppVersion` fallback. Mutation checks: - Reverting any one template to the plain `podLabels` passthrough fails tests. - Switching the source order to digest-first fails tests. - Restoring the case-insensitive passthrough fails the three Datadog-normalization tests (uppercase, `__`, uppercased 63-hex). No Rust, desktop, web, or mobile sources changed. ## Linear tracking Related work: [BUZZ-215](https://linear.app/squareup/issue/BUZZ-215). --------- Signed-off-by: tornquist Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5 Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> --- deploy/charts/buzz/Chart.yaml | 6 +- deploy/charts/buzz/README.md | 79 ++++ deploy/charts/buzz/templates/_helpers.tpl | 88 +++++ .../charts/buzz/templates/_operator-jobs.tpl | 4 +- deploy/charts/buzz/templates/deployment.yaml | 4 +- .../charts/buzz/templates/pairing-relay.yaml | 4 +- .../templates/storage-accounting-cronjob.yaml | 4 +- .../buzz/tests/storage_accounting_test.yaml | 362 +++++++++++++++++- .../charts/buzz/tests/version_label_test.yaml | 313 +++++++++++++++ deploy/charts/buzz/values.schema.json | 5 +- deploy/charts/buzz/values.yaml | 6 + docs/deployment-identity.md | 35 ++ 12 files changed, 893 insertions(+), 17 deletions(-) create mode 100644 deploy/charts/buzz/tests/version_label_test.yaml diff --git a/deploy/charts/buzz/Chart.yaml b/deploy/charts/buzz/Chart.yaml index 55d9db5488..ac3d871bc2 100644 --- a/deploy/charts/buzz/Chart.yaml +++ b/deploy/charts/buzz/Chart.yaml @@ -7,7 +7,7 @@ description: | PostgreSQL and Redis. Configurable for single-node evaluation (subcharts on) and HA production (external services, existingSecret). type: application -version: 0.1.10 +version: 0.1.11 appVersion: "0.1.0" home: https://github.com/block/buzz sources: @@ -23,8 +23,8 @@ maintainers: url: https://github.com/block annotations: artifacthub.io/changes: | - - kind: added - description: Optional typed deletion-drain operator CronJob using durable database leases. + - kind: changed + description: Chart-managed Pods derive tags.datadoghq.com/version from the deployed image identity instead of operator-supplied labels. artifacthub.io/license: Apache-2.0 # Optional eval-only subcharts. Production deploys disable both and point diff --git a/deploy/charts/buzz/README.md b/deploy/charts/buzz/README.md index 999a1522c2..eb9e3bdb32 100644 --- a/deploy/charts/buzz/README.md +++ b/deploy/charts/buzz/README.md @@ -460,6 +460,85 @@ volumes, and each init container must define an appropriate security context and resources. Empty `relay.command` and `relay.args` arrays preserve the image defaults; non-empty values override its entrypoint and arguments respectively. +## Storage accounting worker + +`storageAccounting.enabled=true` adds a CronJob that runs +`buzz-admin storage-snapshot` against the relay's S3 bucket and writes a +durable snapshot row, keeping the expensive object walk off the relay Pods. + +### Snapshot identity contract + +Every snapshot row records the code that produced it in `code_sha`, taken from +the `BUZZ_STORAGE_SNAPSHOT_CODE_SHA` environment variable the chart derives +from the deployed image identity (`image.digest` when set, otherwise +`image.tag`, otherwise `Chart.AppVersion`). The value is never hashed or +sanitized. + +## Datadog version label + +Every Pod the chart renders — relay, pairing relay, storage accounting, and the +deletion drain operator job — carries a chart-owned +`tags.datadoghq.com/version` label derived from the deployed image: +`image.tag` when set, otherwise `image.digest`, otherwise `Chart.AppVersion`. +The tag is preferred because it is the readable build name (`sha-`) +dashboards already use; promotion tooling writes it together with the digest +that actually pins the image. + +```yaml +relay: + podLabels: + tags.datadoghq.com/env: production + tags.datadoghq.com/service: buzz + # tags.datadoghq.com/version: NOT set here — the chart owns it. +``` + +Precedence is explicit: a `tags.datadoghq.com/version` supplied under any +workload's `podLabels` is ignored. Every other label — including +`tags.datadoghq.com/env` and `tags.datadoghq.com/service`, which describe the +deployment rather than the image — passes through unchanged. Remove any +wrapper-maintained version pins when upgrading; a pin equal to `image.tag` +renders identically, so the upgrade does not roll Pods for that label. + +A Kubernetes label value is capped at 63 bytes, must begin and end with an +alphanumeric, and may otherwise contain only `[-._a-zA-Z0-9]` — a far narrower +grammar than an OCI tag. `image.tag` is deliberately left unconstrained, so the +label is derived through a total mapping rather than a check that could reject +a tag the chart used to accept: + +| Revision | Label | +|---|---| +| `sha256:<64 hex>` digest | the hex without `sha256:`, first 63 characters | +| already a valid label value, lowercase, no `__`, and not exactly 63 lowercase hex characters | emitted byte for byte (e.g. `1.2.3-rc.4`, `sha-1a2b3c4`) | +| anything else | the first 63 hex characters of the revision's SHA-256 | + +The passthrough row requires Datadog's normal form because Datadog lowercases +tag values and collapses `__`: passing `ReleaseA` and `releasea` through +unchanged would report one version for two revisions. Every emitted label is +therefore already normalized, so distinct labels stay distinct in Datadog. + +When both `image.tag` and `image.digest` are set, the label names the tag — +even if the tag and digest disagree. The digest still pins what actually runs +(`repository@digest`), so keep the two in step when promoting. + +Exactly 63 lowercase hex characters is a **reserved shape** — it is what the +first and third rows emit, so the passthrough row must not be able to emit it +too. A tag of that shape is hashed rather than preserved. Shorter hex tags and +ordinary 40-character git SHAs are unaffected. + +Arbitrary OCI revisions outnumber 63-byte label values, so **no mapping onto +this grammar can be injective** and the chart does not claim one. What it does +claim is collision *resistance*: every case retains 252 bits of SHA-256, the +same margin the digest case has always relied on, and the reserved shape keeps +that margin across cases rather than only within one. Two earlier iterations +fell short of this — one kept a readable prefix with only 40 bits of hash (a +colliding tag pair was brute-forced in about a second), and one let a +passthrough tag reproduce a hashed label with no hash work at all, by copying a +rendered label into `image.tag`. + +The image reference in the Pod spec and `BUZZ_STORAGE_SNAPSHOT_CODE_SHA` always +keep the exact revision; the label is the joinable telemetry key. See +`docs/deployment-identity.md`. + ## Device pairing relay The chart can run Buzz's stateless pairing WebSocket relay as an independent diff --git a/deploy/charts/buzz/templates/_helpers.tpl b/deploy/charts/buzz/templates/_helpers.tpl index c00e643fdb..30f68e04bf 100644 --- a/deploy/charts/buzz/templates/_helpers.tpl +++ b/deploy/charts/buzz/templates/_helpers.tpl @@ -76,6 +76,94 @@ app.kubernetes.io/component: relay {{- end -}} {{- end -}} +{{/* +Datadog unified-service-tagging version for every chart-managed Pod (relay, +pairing relay, storage accounting, operator jobs). It names the deployed image: +image.tag when set — the readable build name operators and dashboards already +use, which promotion tooling writes together with image.digest — otherwise the +digest, otherwise Chart.AppVersion. The chart owns this label so a wrapper never +has to pin a second copy of the image identity that silently drifts; the exact +runtime identity stays in the image reference and in +BUZZ_STORAGE_SNAPSHOT_CODE_SHA ("buzz.imageRevision"). +*/}} +{{- define "buzz.imageVersionSource" -}} +{{- if .Values.image.tag -}} +{{- .Values.image.tag -}} +{{- else if .Values.image.digest -}} +{{- .Values.image.digest -}} +{{- else -}} +{{- .Chart.AppVersion -}} +{{- end -}} +{{- end -}} + +{{/* +Kubernetes-label-safe rendering of "buzz.imageVersionSource". + +A label value is at most 63 bytes, must begin and end with an alphanumeric, +and may otherwise contain only [-._a-zA-Z0-9]. image.tag is an unconstrained +string, so the domain is larger than the codomain and no mapping can be +injective; what the chart provides is a total mapping that is collision +*resistant*, in three cases: + + 1. A "sha256:<64 hex>" digest drops its algorithm prefix and keeps 63 hex + characters — the behaviour this chart has always had for digests. + 2. A revision that is already a valid label value *and* already in Datadog's + normal form — no uppercase, no "__" — and is not of the reserved shape + below, is emitted byte for byte, so ordinary tags like 1.2.3-rc.4 and + sha-1a2b3c4 are preserved exactly. + 3. Anything else — a leading "_", an uppercase letter, a "__" run, a byte + outside the label alphabet, or more than 63 bytes — is replaced by the + first 63 hex characters of its SHA-256, the same shape and the same 252 + retained bits as case 1. + +Case 2 is restricted to Datadog's normal form because the label's consumer is +Datadog, which lowercases tag values and collapses runs of "_". A passthrough +of "ReleaseA" would report the same version as "releasea", and "release__a" +the same as "release_a"; an uppercased 63-hex value would also slip past the +reserved-shape check below and then normalize onto a hashed or digest label. +Hashing everything not already in normal form means every value this helper +emits is a fixed point of Datadog's normalization, so distinct outputs stay +distinct where they are read. + +Exactly 63 lowercase hex characters is the reserved shape: it is what cases 1 +and 3 emit, so case 2 must not also be able to emit it. Without that exclusion +the two namespaces overlap and colliding two revisions costs no hash work at +all — render one, copy the label into image.tag, and the passthrough branch +echoes it back. Reserving the shape keeps every cross-case collision behind a +252-bit preimage, which is the same margin cases 1 and 3 already rely on. The +cost is that a tag which is itself 63 lowercase hex characters is hashed rather +than preserved; such a tag is not a form anything generates except this label. + +Case 3 deliberately keeps no readable prefix. An earlier revision of this +helper kept a sanitized prefix plus 10 hex characters of SHA-256, and that +40-bit namespace was small enough to brute-force: the tags +"b"x52 + "-collision-22356" and "b"x52 + "-collision-914141" both rendered +"b"x52 + "-430be57931" (see tests/storage_accounting_test.yaml). Readability +is not worth a forgeable telemetry identity when the exact revision is always +available in the image reference itself. +*/}} +{{- define "buzz.imageVersionLabel" -}} +{{- $revision := include "buzz.imageVersionSource" . -}} +{{- if regexMatch "^sha256:[0-9a-f]{64}$" $revision -}} +{{- $revision | trimPrefix "sha256:" | trunc 63 -}} +{{- else if and (le (len $revision) 63) (regexMatch "^[a-z0-9]([-._a-z0-9]*[a-z0-9])?$" $revision) (not (contains "__" $revision)) (not (regexMatch "^[0-9a-f]{63}$" $revision)) -}} +{{- $revision -}} +{{- else -}} +{{- sha256sum $revision | trunc 63 -}} +{{- end -}} +{{- end -}} + +{{/* +Pod-template labels for a chart-managed workload: the caller's podLabels with +the chart-owned Datadog version merged on top. Every other key, including +tags.datadoghq.com/env and tags.datadoghq.com/service, passes through. +Usage: include "buzz.podLabelsWithVersion" (dict "root" $ "labels" $podLabels) +*/}} +{{- define "buzz.podLabelsWithVersion" -}} +{{- $labels := merge (dict "tags.datadoghq.com/version" (include "buzz.imageVersionLabel" .root)) (.labels | default dict) -}} +{{- toYaml $labels -}} +{{- end -}} + {{/* Name of the chart-managed Secret holding relay-identity material and any chart-composed connection strings. diff --git a/deploy/charts/buzz/templates/_operator-jobs.tpl b/deploy/charts/buzz/templates/_operator-jobs.tpl index 49a204f9ec..5c54a9563d 100644 --- a/deploy/charts/buzz/templates/_operator-jobs.tpl +++ b/deploy/charts/buzz/templates/_operator-jobs.tpl @@ -30,9 +30,7 @@ spec: labels: {{- include "buzz.selectorLabels" $root | nindent 12 }} app.kubernetes.io/component: deletion-drain - {{- with $job.podLabels }} - {{- toYaml . | nindent 12 }} - {{- end }} + {{- include "buzz.podLabelsWithVersion" (dict "root" $root "labels" $job.podLabels) | nindent 12 }} annotations: {{- toYaml $job.podAnnotations | nindent 12 }} spec: diff --git a/deploy/charts/buzz/templates/deployment.yaml b/deploy/charts/buzz/templates/deployment.yaml index 59f52f759e..a47368eac7 100644 --- a/deploy/charts/buzz/templates/deployment.yaml +++ b/deploy/charts/buzz/templates/deployment.yaml @@ -21,9 +21,7 @@ spec: metadata: labels: {{- include "buzz.relaySelectorLabels" . | nindent 8 }} - {{- with .Values.relay.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} + {{- include "buzz.podLabelsWithVersion" (dict "root" . "labels" .Values.relay.podLabels) | nindent 8 }} annotations: # Roll pods when the chart-managed Secret changes. checksum/secret: {{ include (print $.Template.BasePath "/secret-chart.yaml") . | sha256sum }} diff --git a/deploy/charts/buzz/templates/pairing-relay.yaml b/deploy/charts/buzz/templates/pairing-relay.yaml index 86e362fe03..e1d9669969 100644 --- a/deploy/charts/buzz/templates/pairing-relay.yaml +++ b/deploy/charts/buzz/templates/pairing-relay.yaml @@ -16,9 +16,7 @@ spec: metadata: labels: {{- include "buzz.pairingRelaySelectorLabels" . | nindent 8 }} - {{- with .Values.pairingRelay.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} + {{- include "buzz.podLabelsWithVersion" (dict "root" . "labels" .Values.pairingRelay.podLabels) | nindent 8 }} {{- with .Values.pairingRelay.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} diff --git a/deploy/charts/buzz/templates/storage-accounting-cronjob.yaml b/deploy/charts/buzz/templates/storage-accounting-cronjob.yaml index f753bd3b41..2244f120a7 100644 --- a/deploy/charts/buzz/templates/storage-accounting-cronjob.yaml +++ b/deploy/charts/buzz/templates/storage-accounting-cronjob.yaml @@ -20,9 +20,7 @@ spec: labels: {{- include "buzz.selectorLabels" . | nindent 12 }} app.kubernetes.io/component: storage-accounting - {{- with .Values.storageAccounting.podLabels }} - {{- toYaml . | nindent 12 }} - {{- end }} + {{- include "buzz.podLabelsWithVersion" (dict "root" . "labels" .Values.storageAccounting.podLabels) | nindent 12 }} annotations: {{- toYaml .Values.storageAccounting.podAnnotations | nindent 12 }} spec: diff --git a/deploy/charts/buzz/tests/storage_accounting_test.yaml b/deploy/charts/buzz/tests/storage_accounting_test.yaml index 563e1ec228..bd3ffdc95a 100644 --- a/deploy/charts/buzz/tests/storage_accounting_test.yaml +++ b/deploy/charts/buzz/tests/storage_accounting_test.yaml @@ -44,6 +44,7 @@ tests: podLabels: tags.datadoghq.com/env: production tags.datadoghq.com/service: buzz-storage-accounting + # Stale operator pin: the chart owns this key and overrides it. tags.datadoghq.com/version: test-sha asserts: - hasDocuments: @@ -93,9 +94,11 @@ tests: path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/env"] value: production template: templates/storage-accounting-cronjob.yaml + # Image identity wins over the operator-supplied pin; default image.tag + # falls back to Chart.AppVersion, the same value as the env var below. - equal: path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] - value: test-sha + value: 0.1.0 template: templates/storage-accounting-cronjob.yaml - equal: path: spec.jobTemplate.spec.template.spec.containers[0].args @@ -165,3 +168,360 @@ tests: path: metadata.name value: storage-accounting-fullname-that-storage-accounting template: templates/storage-accounting-cronjob.yaml + # ── Datadog version tag is chart-owned, derived from image identity ──────── + # Regression: a wrapper chart pinned tags.datadoghq.com/version independently + # of image.tag/image.digest, so the storage-accounting Pod reported one + # version to Datadog while BUZZ_STORAGE_SNAPSHOT_CODE_SHA recorded another. + - it: overrides a stale operator-pinned Datadog version label with the image tag + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.accessKey: test + s3.secretKey: test + image.tag: 1.2.3-rc.4 + storageAccounting: + enabled: true + podLabels: + tags.datadoghq.com/env: staging + tags.datadoghq.com/service: buzz-storage-accounting + tags.datadoghq.com/version: stale-operator-sha + team: platform + asserts: + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: 1.2.3-rc.4 + template: templates/storage-accounting-cronjob.yaml + # The telemetry tag and the recorded snapshot identity cannot disagree. + - contains: + path: spec.jobTemplate.spec.template.spec.containers[0].env + content: + name: BUZZ_STORAGE_SNAPSHOT_CODE_SHA + value: 1.2.3-rc.4 + template: templates/storage-accounting-cronjob.yaml + # Every other operator label still passes through. + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/env"] + value: staging + template: templates/storage-accounting-cronjob.yaml + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/service"] + value: buzz-storage-accounting + template: templates/storage-accounting-cronjob.yaml + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["team"] + value: platform + template: templates/storage-accounting-cronjob.yaml + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["app.kubernetes.io/component"] + value: storage-accounting + template: templates/storage-accounting-cronjob.yaml + - it: stamps the version label with no operator podLabels at all + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.accessKey: test + s3.secretKey: test + image.tag: 2.0.0 + storageAccounting: + enabled: true + asserts: + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: 2.0.0 + template: templates/storage-accounting-cronjob.yaml + # Promotion writes image.tag and image.digest together; the tag is the + # readable build name Datadog already uses, the digest the runtime identity. + - it: names the image tag in the version label while the digest pins the image + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.accessKey: test + s3.secretKey: test + image.tag: sha-1234567 + image.digest: sha256:1111111111111111111111111111111111111111111111111111111111111111 + storageAccounting: + enabled: true + podLabels: + tags.datadoghq.com/version: stale-operator-sha + asserts: + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: sha-1234567 + template: templates/storage-accounting-cronjob.yaml + # The snapshot row still records the exact digest that ran. + - contains: + path: spec.jobTemplate.spec.template.spec.containers[0].env + content: + name: BUZZ_STORAGE_SNAPSHOT_CODE_SHA + value: sha256:1111111111111111111111111111111111111111111111111111111111111111 + template: templates/storage-accounting-cronjob.yaml + - it: renders a label-safe version from a digest when no tag is set + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.accessKey: test + s3.secretKey: test + image.digest: sha256:1111111111111111111111111111111111111111111111111111111111111111 + storageAccounting: + enabled: true + podLabels: + tags.datadoghq.com/version: stale-operator-sha + asserts: + # 63 chars, no "sha256:" prefix — a label value the API server accepts, + # still a unique prefix of the digest recorded in the env var below. + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "111111111111111111111111111111111111111111111111111111111111111" + template: templates/storage-accounting-cronjob.yaml + - matchRegex: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + pattern: ^[a-z0-9A-Z]([-_.a-zA-Z0-9]{0,61}[a-zA-Z0-9])?$ + template: templates/storage-accounting-cronjob.yaml + - contains: + path: spec.jobTemplate.spec.template.spec.containers[0].env + content: + name: BUZZ_STORAGE_SNAPSHOT_CODE_SHA + value: sha256:1111111111111111111111111111111111111111111111111111111111111111 + template: templates/storage-accounting-cronjob.yaml + # ── Label-safety of arbitrary OCI tags (PR #7876 review, P0) ─────────────── + # image.tag is an unconstrained string in values.schema.json, so any tag the + # OCI grammar allows must still yield a valid Kubernetes label value. The + # chart replaces any revision that is not already label-safe with the first + # 63 hex characters of its SHA-256, the same shape it uses for digests. + - it: sanitizes a leading underscore that OCI allows but Kubernetes labels do not + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.accessKey: test + s3.secretKey: test + image.tag: "_foo" + storageAccounting: + enabled: true + asserts: + # Leading '_' is a valid OCI tag byte but an invalid first label byte. + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "583da8894acbb7b121f9b85bc263859bd68c3a239f4934b62fc13f4f0beedc6" + template: templates/storage-accounting-cronjob.yaml + # The rendered label must satisfy the Kubernetes label-value grammar: + # <=63 bytes, alphanumeric first and last byte, [-._a-zA-Z0-9] inside. + - matchRegex: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + pattern: ^[a-zA-Z0-9]([-._a-zA-Z0-9]{0,61}[a-zA-Z0-9])?$ + template: templates/storage-accounting-cronjob.yaml + # The exact image revision still reaches the snapshot row unmodified. + - contains: + path: spec.jobTemplate.spec.template.spec.containers[0].env + content: + name: BUZZ_STORAGE_SNAPSHOT_CODE_SHA + value: "_foo" + template: templates/storage-accounting-cronjob.yaml + - it: keeps the label valid when truncation would land on mixed trailing delimiters + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.accessKey: test + s3.secretKey: test + image.tag: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-_.tail" + storageAccounting: + enabled: true + asserts: + # A one-pass '-'/'.'/'_' trim left a trailing '-' for a tag ending '-_.'; + # a hex label has no delimiters to trim. + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "e4fb46fc85bd57715b5a11055e17387d439f906e568445c24d27ba91fc4995f" + template: templates/storage-accounting-cronjob.yaml + # The rendered label must satisfy the Kubernetes label-value grammar: + # <=63 bytes, alphanumeric first and last byte, [-._a-zA-Z0-9] inside. + - matchRegex: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + pattern: ^[a-zA-Z0-9]([-._a-zA-Z0-9]{0,61}[a-zA-Z0-9])?$ + template: templates/storage-accounting-cronjob.yaml + # The exact image revision still reaches the snapshot row unmodified. + - contains: + path: spec.jobTemplate.spec.template.spec.containers[0].env + content: + name: BUZZ_STORAGE_SNAPSHOT_CODE_SHA + value: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-_.tail" + template: templates/storage-accounting-cronjob.yaml + - it: distinguishes the first of two >63-character tags sharing a 63-byte prefix + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.accessKey: test + s3.secretKey: test + image.tag: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-one" + storageAccounting: + enabled: true + asserts: + # Plain truncation collapses both tags to the same telemetry version. + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "2aad1d5b65b546c6b4a5f4d46845078c7dfb91302d9824c884fff9f45f6398c" + template: templates/storage-accounting-cronjob.yaml + # The rendered label must satisfy the Kubernetes label-value grammar: + # <=63 bytes, alphanumeric first and last byte, [-._a-zA-Z0-9] inside. + - matchRegex: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + pattern: ^[a-zA-Z0-9]([-._a-zA-Z0-9]{0,61}[a-zA-Z0-9])?$ + template: templates/storage-accounting-cronjob.yaml + # The exact image revision still reaches the snapshot row unmodified. + - contains: + path: spec.jobTemplate.spec.template.spec.containers[0].env + content: + name: BUZZ_STORAGE_SNAPSHOT_CODE_SHA + value: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-one" + template: templates/storage-accounting-cronjob.yaml + - it: distinguishes the second of two >63-character tags sharing a 63-byte prefix + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.accessKey: test + s3.secretKey: test + image.tag: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-two" + storageAccounting: + enabled: true + asserts: + # Same 63-byte tag prefix as the previous case, different SHA-256. + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "86dde532eb8217d9ab8648f38f12cd3568983910e0e668489a66c036734ae9a" + template: templates/storage-accounting-cronjob.yaml + # The rendered label must satisfy the Kubernetes label-value grammar: + # <=63 bytes, alphanumeric first and last byte, [-._a-zA-Z0-9] inside. + - matchRegex: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + pattern: ^[a-zA-Z0-9]([-._a-zA-Z0-9]{0,61}[a-zA-Z0-9])?$ + template: templates/storage-accounting-cronjob.yaml + # The exact image revision still reaches the snapshot row unmodified. + - contains: + path: spec.jobTemplate.spec.template.spec.containers[0].env + content: + name: BUZZ_STORAGE_SNAPSHOT_CODE_SHA + value: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-two" + template: templates/storage-accounting-cronjob.yaml + # ── Known 40-bit collision pair (PR #7876 second review, P0) ─────────────── + # These two tags were found by brute force against the previous fallback, + # which kept only 10 hex characters of SHA-256: both rendered + # b*52-430be57931. They share a 40-bit SHA-256 prefix and diverge at the + # 11th hex character, so they pin the width of the retained hash. + - it: keeps the first tag of the known 40-bit collision pair distinct + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.accessKey: test + s3.secretKey: test + image.tag: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-collision-22356" + storageAccounting: + enabled: true + asserts: + # Shares its first 10 SHA-256 hex characters with the next case. + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "430be579311332ea582db391704bd58601bcf0f3f5682d22bdabdac24a47169" + template: templates/storage-accounting-cronjob.yaml + - matchRegex: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + pattern: ^[a-zA-Z0-9]([-._a-zA-Z0-9]{0,61}[a-zA-Z0-9])?$ + template: templates/storage-accounting-cronjob.yaml + - contains: + path: spec.jobTemplate.spec.template.spec.containers[0].env + content: + name: BUZZ_STORAGE_SNAPSHOT_CODE_SHA + value: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-collision-22356" + template: templates/storage-accounting-cronjob.yaml + - it: keeps the second tag of the known 40-bit collision pair distinct + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.accessKey: test + s3.secretKey: test + image.tag: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-collision-914141" + storageAccounting: + enabled: true + asserts: + # Same 40-bit SHA-256 prefix as the previous case, different label. + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "430be5793136081a42faa8d72f746af34f0b3bad1a5f65f22bac19e22bd71c5" + template: templates/storage-accounting-cronjob.yaml + - matchRegex: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + pattern: ^[a-zA-Z0-9]([-._a-zA-Z0-9]{0,61}[a-zA-Z0-9])?$ + template: templates/storage-accounting-cronjob.yaml + - contains: + path: spec.jobTemplate.spec.template.spec.containers[0].env + content: + name: BUZZ_STORAGE_SNAPSHOT_CODE_SHA + value: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-collision-914141" + template: templates/storage-accounting-cronjob.yaml + # ── Reserved hash shape is not reachable by a passthrough tag ────────────── + # The hashed and digest branches both emit exactly 63 lowercase hex + # characters. If a label-safe tag of that same shape were passed through, an + # operator could collide two revisions with no hash work at all, by copying a + # rendered label into image.tag. The label below is exactly what + # "b"x52 + "-collision-22356" renders, so the chart must hash it rather than + # echo it back. + - it: hashes a tag that has the reserved 63-hex shape instead of passing it through + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.accessKey: test + s3.secretKey: test + image.tag: "430be579311332ea582db391704bd58601bcf0f3f5682d22bdabdac24a47169" + storageAccounting: + enabled: true + asserts: + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "9f615034d2fe14c5266fe5ca3eb7c26c4179eae8dce01f506def01a8cc6a77e" + template: templates/storage-accounting-cronjob.yaml + # Must not echo the tag back: that is the label of a different revision. + - notEqual: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "430be579311332ea582db391704bd58601bcf0f3f5682d22bdabdac24a47169" + template: templates/storage-accounting-cronjob.yaml + - matchRegex: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + pattern: ^[a-zA-Z0-9]([-._a-zA-Z0-9]{0,61}[a-zA-Z0-9])?$ + template: templates/storage-accounting-cronjob.yaml + - contains: + path: spec.jobTemplate.spec.template.spec.containers[0].env + content: + name: BUZZ_STORAGE_SNAPSHOT_CODE_SHA + value: "430be579311332ea582db391704bd58601bcf0f3f5682d22bdabdac24a47169" + template: templates/storage-accounting-cronjob.yaml diff --git a/deploy/charts/buzz/tests/version_label_test.yaml b/deploy/charts/buzz/tests/version_label_test.yaml new file mode 100644 index 0000000000..6071348679 --- /dev/null +++ b/deploy/charts/buzz/tests/version_label_test.yaml @@ -0,0 +1,313 @@ +suite: chart-owned Datadog version label on every workload +# Regression: wrappers pinned tags.datadoghq.com/version beside image.tag on +# each workload, so every image promotion had to edit several copies of one +# identity and a missed copy reported the wrong version to Datadog. The chart +# now owns the label everywhere it renders a Pod (storage accounting is covered +# in storage_accounting_test.yaml). +templates: + - templates/deployment.yaml + - templates/secret-chart.yaml + - templates/pairing-relay.yaml + - templates/deletion-drain-cronjob.yaml +tests: + - it: derives the relay version label from image.tag over a stale pin + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + image.tag: sha-abcdef1 + image.digest: sha256:2222222222222222222222222222222222222222222222222222222222222222 + relay.podLabels: + tags.datadoghq.com/env: staging + tags.datadoghq.com/service: buzz-example + tags.datadoghq.com/version: sha-stale00 + asserts: + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: sha-abcdef1 + template: templates/deployment.yaml + # Deployment-describing labels still pass through. + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/env"] + value: staging + template: templates/deployment.yaml + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/service"] + value: buzz-example + template: templates/deployment.yaml + - it: stamps the relay version label with no podLabels configured + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + image.tag: sha-abcdef1 + asserts: + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: sha-abcdef1 + template: templates/deployment.yaml + - it: renders a label-safe relay version from a digest-only image + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + image.digest: sha256:2222222222222222222222222222222222222222222222222222222222222222 + asserts: + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "222222222222222222222222222222222222222222222222222222222222222" + template: templates/deployment.yaml + - it: derives the pairing relay version label from image.tag over a stale pin + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + pairingRelay.enabled: true + pairingRelay.url: wss://pairing.buzz.xyz + image.tag: sha-abcdef1 + image.digest: sha256:2222222222222222222222222222222222222222222222222222222222222222 + pairingRelay.podLabels: + tags.datadoghq.com/env: staging + tags.datadoghq.com/service: buzz-example + tags.datadoghq.com/version: sha-stale00 + asserts: + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: sha-abcdef1 + template: templates/pairing-relay.yaml + documentIndex: 0 + # Deployment-describing labels still pass through. + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/env"] + value: staging + template: templates/pairing-relay.yaml + documentIndex: 0 + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/service"] + value: buzz-example + template: templates/pairing-relay.yaml + documentIndex: 0 + - it: stamps the pairing relay version label with no podLabels configured + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + pairingRelay.enabled: true + pairingRelay.url: wss://pairing.buzz.xyz + image.tag: sha-abcdef1 + asserts: + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: sha-abcdef1 + template: templates/pairing-relay.yaml + documentIndex: 0 + - it: renders a label-safe pairing relay version from a digest-only image + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + pairingRelay.enabled: true + pairingRelay.url: wss://pairing.buzz.xyz + image.digest: sha256:2222222222222222222222222222222222222222222222222222222222222222 + asserts: + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "222222222222222222222222222222222222222222222222222222222222222" + template: templates/pairing-relay.yaml + documentIndex: 0 + - it: derives the deletion drain version label from image.tag over a stale pin + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + operatorJobs.deletionDrain.enabled: true + image.tag: sha-abcdef1 + image.digest: sha256:2222222222222222222222222222222222222222222222222222222222222222 + operatorJobs.deletionDrain.podLabels: + tags.datadoghq.com/env: staging + tags.datadoghq.com/service: buzz-example + tags.datadoghq.com/version: sha-stale00 + asserts: + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: sha-abcdef1 + template: templates/deletion-drain-cronjob.yaml + # Deployment-describing labels still pass through. + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/env"] + value: staging + template: templates/deletion-drain-cronjob.yaml + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/service"] + value: buzz-example + template: templates/deletion-drain-cronjob.yaml + - it: stamps the deletion drain version label with no podLabels configured + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + operatorJobs.deletionDrain.enabled: true + image.tag: sha-abcdef1 + asserts: + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: sha-abcdef1 + template: templates/deletion-drain-cronjob.yaml + - it: renders a label-safe deletion drain version from a digest-only image + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + operatorJobs.deletionDrain.enabled: true + image.digest: sha256:2222222222222222222222222222222222222222222222222222222222222222 + asserts: + - equal: + path: spec.jobTemplate.spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "222222222222222222222222222222222222222222222222222222222222222" + template: templates/deletion-drain-cronjob.yaml + # ── Datadog normalization (PR #7876 review, P0) ─────────────────────────── + # Datadog lowercases tag values and collapses "__", so a passthrough of a + # non-normal value would merge two revisions into one reported version. Each + # pair below must render two labels that stay distinct after normalization. + - it: hashes a tag with uppercase so it cannot merge with its lowercase twin + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + image.tag: "ReleaseA" + asserts: + # Passed through, Datadog would read this as "releasea". + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "4592ae6466f715bbba35c9e1931fe5cdd548a4355947759b1fcf7f2b6f0e727" + template: templates/deployment.yaml + - notEqual: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "releasea" + template: templates/deployment.yaml + - it: passes through the lowercase twin of a hashed uppercase tag + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + image.tag: "releasea" + asserts: + # Already in Datadog normal form, so preserved byte for byte. + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "releasea" + template: templates/deployment.yaml + - it: hashes a tag containing a double underscore + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + image.tag: "release__a" + asserts: + # Passed through, Datadog would collapse this to "release_a". + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "ab213bb0d3409388c915a5d3ef384cd6d812237d4aa712081edfc1c1473daa1" + template: templates/deployment.yaml + - notEqual: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "release_a" + template: templates/deployment.yaml + - it: passes through the single-underscore twin of a hashed double-underscore tag + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + image.tag: "release_a" + asserts: + # Already in Datadog normal form, so preserved byte for byte. + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "release_a" + template: templates/deployment.yaml + - it: hashes an uppercased 63-hex tag instead of letting it alias the reserved shape + set: + relayUrl: wss://buzz.example.com + ownerPubkey: "0000000000000000000000000000000000000000000000000000000000000000" + externalPostgresql.url: postgres://u:p@h:5432/d + externalRedis.url: redis://h:6379 + s3.endpoint: https://s3.example.com + s3.bucket: buzz-media-example + s3.accessKey: test + s3.secretKey: test + image.tag: "ABCDEF000000000000000000000000000000000000000000000000000000000" + asserts: + # Passed through, Datadog would lowercase this onto the reserved 63-hex shape that digest-derived labels use. + - equal: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "2435b36c0cdc7f75334db8b9151d18738e4358cd0fac9b260e98a84f0207b41" + template: templates/deployment.yaml + - notEqual: + path: spec.template.metadata.labels["tags.datadoghq.com/version"] + value: "abcdef000000000000000000000000000000000000000000000000000000000" + template: templates/deployment.yaml diff --git a/deploy/charts/buzz/values.schema.json b/deploy/charts/buzz/values.schema.json index 309b2f5c37..8162ed7d0d 100644 --- a/deploy/charts/buzz/values.schema.json +++ b/deploy/charts/buzz/values.schema.json @@ -229,7 +229,10 @@ "failedJobsHistoryLimit": { "type": "integer", "minimum": 0 }, "relayMode": { "type": "string", "enum": ["inline", "external", "off"] }, "serviceAccountName": { "type": "string" }, - "podLabels": { "type": "object" }, + "podLabels": { + "type": "object", + "description": "Extra Pod labels. The chart owns tags.datadoghq.com/version and derives it from the deployed image identity, so a value supplied here for that one key is ignored." + }, "podAnnotations": { "type": "object" }, "resources": { "type": "object" } } diff --git a/deploy/charts/buzz/values.yaml b/deploy/charts/buzz/values.yaml index 41ddee9713..397f3015ba 100644 --- a/deploy/charts/buzz/values.yaml +++ b/deploy/charts/buzz/values.yaml @@ -172,6 +172,7 @@ relay: memory: "2Gi" podAnnotations: {} + # tags.datadoghq.com/version is chart-owned; see README "Datadog version label". podLabels: {} nodeSelector: {} tolerations: [] @@ -222,6 +223,7 @@ pairingRelay: port: 5000 annotations: {} podAnnotations: {} + # tags.datadoghq.com/version is chart-owned; see README "Datadog version label". podLabels: {} resources: requests: @@ -380,6 +382,9 @@ storageAccounting: failedJobsHistoryLimit: 3 relayMode: external # external | off; inline is a legacy alias serviceAccountName: "" # defaults to the main Buzz service account + # Extra Pod labels, e.g. Datadog unified service tagging. The chart owns + # `tags.datadoghq.com/version` on every workload (see README "Datadog version + # label"); a value supplied here for that one key is ignored. podLabels: {} podAnnotations: sidecar.istio.io/inject: "false" @@ -405,6 +410,7 @@ operatorJobs: # Empty inherits the relay service account, and with it any cloud IAM # binding on that account. Name a dedicated account to narrow the executor. serviceAccountName: "" + # tags.datadoghq.com/version is chart-owned; see README "Datadog version label". podLabels: {} podAnnotations: sidecar.istio.io/inject: "false" diff --git a/docs/deployment-identity.md b/docs/deployment-identity.md index f5bc8710d1..733db7aba8 100644 --- a/docs/deployment-identity.md +++ b/docs/deployment-identity.md @@ -85,3 +85,38 @@ image: When `image.digest` is set, the chart renders `repository@digest` and ignores `image.tag`. Existing tag-only values remain backwards compatible. + +## Derived deployment labels + +Every chart-managed Pod — the relay, the pairing relay, the storage-accounting +CronJob, and operator jobs such as the deletion drain — carries a +chart-owned `tags.datadoghq.com/version` label derived from the deployed image, +so a wrapper never restates the image identity once per workload. The label +names `image.tag` when it is set (the readable `sha-` build name that +promotion writes together with `image.digest`), otherwise `image.digest`, +otherwise `Chart.AppVersion`. + +The exact runtime identity stays exact elsewhere: the Pod's image reference is +`repository@digest` when a digest is pinned, and the storage-accounting +`BUZZ_STORAGE_SNAPSHOT_CODE_SHA` (persisted as `code_sha` on every snapshot +row) is the digest when set, otherwise the tag, otherwise `Chart.AppVersion`. + +The label value has to be sanitized: a +label value is capped at 63 bytes, must begin and end with an alphanumeric, and +may otherwise contain only `[-._a-zA-Z0-9]`, while `image.tag` accepts any OCI +tag. Because that domain is larger than the label codomain, no mapping onto it +is injective; the chart provides a deterministic, collision-resistant one. A +digest keeps the first 63 characters of its hex, a revision that is already a +valid label value in Datadog's normal form (lowercase, no `__`) is preserved +byte for byte, and anything else (a leading `_`, an uppercase letter, a `__` +run, a byte outside the label alphabet, more than 63 bytes) is replaced by the first +63 hex characters of its SHA-256 — 252 retained bits, the same margin as the +digest case, and no readable prefix. + +Exactly 63 lowercase hex characters is reserved for those hashed and digest +forms: a tag of that shape is hashed instead of preserved, so a rendered label +cannot be copied into `image.tag` to make two revisions report one version. +Shorter hex tags and 40-character git SHAs pass through unchanged. + +Any `tags.datadoghq.com/version` supplied through a workload's `podLabels` is +ignored; see the chart README's "Datadog version label" section.