Skip to content

feat: add dash0 slos command group (SLO IaC support) - #207

Open
nicolastakashi wants to merge 17 commits into
mainfrom
slos
Open

feat: add dash0 slos command group (SLO IaC support)#207
nicolastakashi wants to merge 17 commits into
mainfrom
slos

Conversation

@nicolastakashi

@nicolastakashi nicolastakashi commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a dash0 slos command group (create / get / list / update / delete) for managing service level objectives (SLOs) as code, backed by the /api/slos endpoint using OpenSLO documents (apiVersion: openslo.com/v1, kind: SLO).

  • Dataset-scoped via --dataset; ID at metadata.labels."dash0.com/id"; deep-link URL support (/goto/alerting/slos/details).
  • Wires SLO into apply (create-or-replace by ID).
  • Integration tests against the mock server, plus a roundtrip test (test/roundtrip/test_slo_roundtrip.sh, registered in run_all.sh) — validated end-to-end against dev: create → list → get → export → apply → delete.
  • Docs (docs/commands.md, docs/cli-naming-conventions.md) and a .chloggen/ entry.

Part of cross-facility SLO IaC support (API client, CLI, Terraform provider, Kubernetes operator).

Dependency

Builds against the published github.com/dash0hq/dash0-api-client-go v1.18.0 (SLO client support) — no local replace. The openslo.com/v1 apiVersion is the domain-qualified form the backend dual-accepts on write and canonicalizes on read (INS-551), so the same document is installable as the operator's Kubernetes CRD.

Status

Rebased on main. build / test-unit / test-integration / lint pass. test_slo_roundtrip and test_apply_slo_idempotency pass against dev on the static-token shard. The oauth shard reports green but its steps are skippedDASH0_OAUTH_REFRESH_TOKEN is not provisioned, so that column carries no signal (see #236 discussion). Any red on unrelated test_log_roundtrip / test_span_roundtrip / test_apply_check_rule_idempotency is dev read-lag, not from this change.

Closes #206

@nicolastakashi nicolastakashi left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: dash0 slos command group

Overall this is a faithful clone of the existing views / synthetic-checks shape — flag registration, the asset.Import* helper, error contexts, table columns, roundtrip script structure, and fixture layout all match. No new patterns introduced for problems the repo already solves, and apply.go touches exactly the three kind switches it needs plus KindDisplayName (I checked; none missed).

Two places diverge from the established pattern in ways that matter, plus one open question and one test gap. Details inline; comments use conventional comment labels.

Blocking: slos delete --force is missing the client.IsAlreadyDeleted branch that all 8 other delete commands have — that regresses the invariant shipped in 1.16.2 (#217).

Verified while reviewing (no action needed): SloTimeWindow.Duration resolves to type Duration = string in api-client v1.18.0, so the sigsyaml.Unmarshal in parseDocumentHeader is safe — no time.Duration field, unlike the prometheusrule case that carries the warning comment. AssetType: "SLO" in uppercase is the right deviation from the lowercase convention: it renders as SLO "x" not found and capitalizeFirst is a no-op on it. SloSpec has no unstably-ordered list field, so nothing needs the SortViewPermissions treatment from #231.

Comment thread internal/slos/delete.go
Comment thread internal/slos/update.go
Comment thread internal/asset/slo.go Outdated
Comment thread test/roundtrip/run_all.sh
Comment thread internal/slos/integration_test.go
Comment thread internal/slos/list.go
Comment thread internal/slos/list.go Outdated
Comment thread docs/commands.md Outdated
nicolastakashi added a commit that referenced this pull request Jul 28, 2026
- delete: add IsAlreadyDeleted idempotency branch so `slos delete --force`
  on an already-deleted SLO exits 0 (matches the 8 other delete commands,
  CHANGELOG 1.16.2 / #217); cover with an integration test.
- update: StripSLOServerFields before the dry-run diff and PUT so
  `get -o yaml | update -f` no longer sends server-managed labels/timestamps
  or shows a spurious diff (mirrors views/update.go).
- asset/ImportSLO: mirror ImportTeam upsert-key selection — prefer
  dash0.com/id, fall back to dash0.com/origin (read before strip), POST only
  when neither is present, so an origin-only document upserts via PUT instead
  of duplicating on every apply (#227 shape).
- list: replace interface{} with any on the column closures (matches
  views/list.go).
- integration tests: assert the update wire body strips dash0.com/version,
  dash0.com/origin, and created-at; add create/update --dry-run tests; add a
  second element to list_success.json and exercise --limit truncation.
- roundtrip: add test_apply_slo_idempotency.sh (double-apply, no duplicate /
  stable id) and register it in run_all.sh.
- docs: add teams to the command-group enumerations in commands.md and
  cli-naming-conventions.md and adjust the count wording.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nicolastakashi
nicolastakashi marked this pull request as ready for review July 28, 2026 09:42
@nicolastakashi
nicolastakashi requested a review from a team as a code owner July 28, 2026 09:42
Comment thread test/roundtrip/fixtures/slo.yaml Outdated
Comment thread test/roundtrip/test_apply_slo_idempotency.sh
Comment thread test/roundtrip/test_apply_slo_idempotency.sh
Comment thread test/roundtrip/test_slo_roundtrip.sh Outdated
nicolastakashi added a commit that referenced this pull request Jul 28, 2026
…ST body

Addresses six pre-merge review findings on #207.

1. The docs taught `dash0.com/id` as the SLO upsert key while `ImportSLO` is
   origin-first, and SLO ids are server-assigned (`slo_<ulid>`) so the pinned
   example id could never exist. The identifier table, the SLO YAML example,
   the `apply` section, and the "notification channels and spam filters are the
   two exceptions" sentence (in both docs/commands.md and the skill's SKILL.md)
   now say origin — modeled on the existing `Dash0Team` row, which describes
   the same routing.
2. `apply --dry-run` printed no identifier for an origin-only SLO document (the
   recommended form). Added the origin fallback both origin-keyed baselines
   already have.
3. `slos get <origin>` built the deep link and the id label from the raw
   argument, producing a URL that does not resolve. Both are now read off the
   response, mirroring `notificationchannels/get.go`.
4. `StripSLOServerFields` does not clear `dash0.com/id`, so the cross-org POST
   fallback (id branch, preflight 404) sent the source org's id. `ImportSLO`
   now calls the previously-unused `dash0api.ClearSLOID` on that path, and the
   regression test asserts the POST body carries no id. Also deleted the
   `ImportSLO` doc comment sentence claiming the strip clears the id.
5. There was no `slos` topic in the Agent Skill bundle: added the
   `ManifestEntry`, the generator `topicSpec`, the SKILL.md topic-index row,
   and regenerated, so `dash0 skill show slos` resolves.
6. `slos list -o wide`/`-o csv` were missing the `ORIGIN` column the shared
   format contract promises, and `slos get` printed no `Origin:` line. Since
   origin is the SLO upsert key, it is the field users script against.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread .agents/skills/dash0-cli/references/slos.md Outdated
Comment thread .agents/skills/dash0-cli/SKILL.md Outdated
Comment thread .agents/skills/dash0-cli/SKILL.md Outdated
Comment thread .claude/skills/dash0-cli/references/slos.md Outdated
Comment thread .claude/skills/dash0-cli/references/slos.md Outdated
nicolastakashi added a commit that referenced this pull request Jul 28, 2026
… skill

Second review round on #207, all on the embedded Agent Skill content.

- Add an "SLO documents and OpenSLO v1 compatibility" section to
  docs/commands.md and wire it into the `slos` skill topic. It replaces the
  flatly wrong "SLO documents use the OpenSLO v1 format" claim: the
  `apiVersion` is domain-qualified (`openslo.com/v1`, dual-accepted with the
  upstream bare `openslo/v1` on write, canonicalized on read), only a subset
  is supported (one objective, inline `ratioMetric`, `Occurrences`, rolling
  `4w`/`28d`), a table lists what is rejected with a 400, `alertPolicies` is
  called out as accepted-but-silently-ignored, and queries must be bare
  PromQL vector selectors. Verified field-by-field against `SloDefinition`
  and `SloSpec` in the API OpenAPI spec.
- Note that the same document body is what the operator's `openslo.com/v1`
  `SLO` CRD accepts (group/kind/version and every spec field the example
  uses match; `alertPolicies` and `indicatorRef` are deliberately absent),
  phrased conservatively because that CRD is still unmerged
  (dash0hq/dash0-operator#1247).
- Expand "SLOs" to "service level objectives (SLOs)" on first use in the
  SKILL.md frontmatter description.
- Reframe the origin-vs-ID prose in SKILL.md and docs/commands.md. Origin
  being the upsert key is no longer an exception list: it is the rule for
  the API-managed kinds (notification channels, spam filters, SLOs, teams),
  while views, check rules, recording rules, and synthetic checks are the
  kinds where the server treats origin as provenance and the CLI strips it.
  Dashboards are called out separately, since their `dash0Extensions.origin`
  is deprecated in favor of `dash0Extensions.id`.
- Align the skill's SLO example with test/roundtrip/fixtures/slo.yaml so
  both use the Dash0 synthetic-check metrics (`dash0.synthetic_check.runs`
  split on `dash0_synthetic_check_outcome="Healthy"`) instead of
  `http_server_request_duration_seconds_count`.

Deduplicating the two published skill copies is tracked separately in #236.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread .agents/skills/dash0-cli/references/slos.md Outdated
@nicolastakashi
nicolastakashi marked this pull request as draft July 31, 2026 10:31
nicolastakashi added a commit that referenced this pull request Aug 4, 2026
- delete: add IsAlreadyDeleted idempotency branch so `slos delete --force`
  on an already-deleted SLO exits 0 (matches the 8 other delete commands,
  CHANGELOG 1.16.2 / #217); cover with an integration test.
- update: StripSLOServerFields before the dry-run diff and PUT so
  `get -o yaml | update -f` no longer sends server-managed labels/timestamps
  or shows a spurious diff (mirrors views/update.go).
- asset/ImportSLO: mirror ImportTeam upsert-key selection — prefer
  dash0.com/id, fall back to dash0.com/origin (read before strip), POST only
  when neither is present, so an origin-only document upserts via PUT instead
  of duplicating on every apply (#227 shape).
- list: replace interface{} with any on the column closures (matches
  views/list.go).
- integration tests: assert the update wire body strips dash0.com/version,
  dash0.com/origin, and created-at; add create/update --dry-run tests; add a
  second element to list_success.json and exercise --limit truncation.
- roundtrip: add test_apply_slo_idempotency.sh (double-apply, no duplicate /
  stable id) and register it in run_all.sh.
- docs: add teams to the command-group enumerations in commands.md and
  cli-naming-conventions.md and adjust the count wording.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nicolastakashi added a commit that referenced this pull request Aug 4, 2026
…ST body

Addresses six pre-merge review findings on #207.

1. The docs taught `dash0.com/id` as the SLO upsert key while `ImportSLO` is
   origin-first, and SLO ids are server-assigned (`slo_<ulid>`) so the pinned
   example id could never exist. The identifier table, the SLO YAML example,
   the `apply` section, and the "notification channels and spam filters are the
   two exceptions" sentence (in both docs/commands.md and the skill's SKILL.md)
   now say origin — modeled on the existing `Dash0Team` row, which describes
   the same routing.
2. `apply --dry-run` printed no identifier for an origin-only SLO document (the
   recommended form). Added the origin fallback both origin-keyed baselines
   already have.
3. `slos get <origin>` built the deep link and the id label from the raw
   argument, producing a URL that does not resolve. Both are now read off the
   response, mirroring `notificationchannels/get.go`.
4. `StripSLOServerFields` does not clear `dash0.com/id`, so the cross-org POST
   fallback (id branch, preflight 404) sent the source org's id. `ImportSLO`
   now calls the previously-unused `dash0api.ClearSLOID` on that path, and the
   regression test asserts the POST body carries no id. Also deleted the
   `ImportSLO` doc comment sentence claiming the strip clears the id.
5. There was no `slos` topic in the Agent Skill bundle: added the
   `ManifestEntry`, the generator `topicSpec`, the SKILL.md topic-index row,
   and regenerated, so `dash0 skill show slos` resolves.
6. `slos list -o wide`/`-o csv` were missing the `ORIGIN` column the shared
   format contract promises, and `slos get` printed no `Origin:` line. Since
   origin is the SLO upsert key, it is the field users script against.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nicolastakashi added a commit that referenced this pull request Aug 4, 2026
… skill

Second review round on #207, all on the embedded Agent Skill content.

- Add an "SLO documents and OpenSLO v1 compatibility" section to
  docs/commands.md and wire it into the `slos` skill topic. It replaces the
  flatly wrong "SLO documents use the OpenSLO v1 format" claim: the
  `apiVersion` is domain-qualified (`openslo.com/v1`, dual-accepted with the
  upstream bare `openslo/v1` on write, canonicalized on read), only a subset
  is supported (one objective, inline `ratioMetric`, `Occurrences`, rolling
  `4w`/`28d`), a table lists what is rejected with a 400, `alertPolicies` is
  called out as accepted-but-silently-ignored, and queries must be bare
  PromQL vector selectors. Verified field-by-field against `SloDefinition`
  and `SloSpec` in the API OpenAPI spec.
- Note that the same document body is what the operator's `openslo.com/v1`
  `SLO` CRD accepts (group/kind/version and every spec field the example
  uses match; `alertPolicies` and `indicatorRef` are deliberately absent),
  phrased conservatively because that CRD is still unmerged
  (dash0hq/dash0-operator#1247).
- Expand "SLOs" to "service level objectives (SLOs)" on first use in the
  SKILL.md frontmatter description.
- Reframe the origin-vs-ID prose in SKILL.md and docs/commands.md. Origin
  being the upsert key is no longer an exception list: it is the rule for
  the API-managed kinds (notification channels, spam filters, SLOs, teams),
  while views, check rules, recording rules, and synthetic checks are the
  kinds where the server treats origin as provenance and the CLI strips it.
  Dashboards are called out separately, since their `dash0Extensions.origin`
  is deprecated in favor of `dash0Extensions.id`.
- Align the skill's SLO example with test/roundtrip/fixtures/slo.yaml so
  both use the Dash0 synthetic-check metrics (`dash0.synthetic_check.runs`
  split on `dash0_synthetic_check_outcome="Healthy"`) instead of
  `http_server_request_duration_seconds_count`.

Deduplicating the two published skill copies is tracked separately in #236.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nicolastakashi added a commit that referenced this pull request Aug 4, 2026
- delete: add IsAlreadyDeleted idempotency branch so `slos delete --force`
  on an already-deleted SLO exits 0 (matches the 8 other delete commands,
  CHANGELOG 1.16.2 / #217); cover with an integration test.
- update: StripSLOServerFields before the dry-run diff and PUT so
  `get -o yaml | update -f` no longer sends server-managed labels/timestamps
  or shows a spurious diff (mirrors views/update.go).
- asset/ImportSLO: mirror ImportTeam upsert-key selection — prefer
  dash0.com/id, fall back to dash0.com/origin (read before strip), POST only
  when neither is present, so an origin-only document upserts via PUT instead
  of duplicating on every apply (#227 shape).
- list: replace interface{} with any on the column closures (matches
  views/list.go).
- integration tests: assert the update wire body strips dash0.com/version,
  dash0.com/origin, and created-at; add create/update --dry-run tests; add a
  second element to list_success.json and exercise --limit truncation.
- roundtrip: add test_apply_slo_idempotency.sh (double-apply, no duplicate /
  stable id) and register it in run_all.sh.
- docs: add teams to the command-group enumerations in commands.md and
  cli-naming-conventions.md and adjust the count wording.
nicolastakashi added a commit that referenced this pull request Aug 4, 2026
…ST body

Addresses six pre-merge review findings on #207.

1. The docs taught `dash0.com/id` as the SLO upsert key while `ImportSLO` is
   origin-first, and SLO ids are server-assigned (`slo_<ulid>`) so the pinned
   example id could never exist. The identifier table, the SLO YAML example,
   the `apply` section, and the "notification channels and spam filters are the
   two exceptions" sentence (in both docs/commands.md and the skill's SKILL.md)
   now say origin — modeled on the existing `Dash0Team` row, which describes
   the same routing.
2. `apply --dry-run` printed no identifier for an origin-only SLO document (the
   recommended form). Added the origin fallback both origin-keyed baselines
   already have.
3. `slos get <origin>` built the deep link and the id label from the raw
   argument, producing a URL that does not resolve. Both are now read off the
   response, mirroring `notificationchannels/get.go`.
4. `StripSLOServerFields` does not clear `dash0.com/id`, so the cross-org POST
   fallback (id branch, preflight 404) sent the source org's id. `ImportSLO`
   now calls the previously-unused `dash0api.ClearSLOID` on that path, and the
   regression test asserts the POST body carries no id. Also deleted the
   `ImportSLO` doc comment sentence claiming the strip clears the id.
5. There was no `slos` topic in the Agent Skill bundle: added the
   `ManifestEntry`, the generator `topicSpec`, the SKILL.md topic-index row,
   and regenerated, so `dash0 skill show slos` resolves.
6. `slos list -o wide`/`-o csv` were missing the `ORIGIN` column the shared
   format contract promises, and `slos get` printed no `Origin:` line. Since
   origin is the SLO upsert key, it is the field users script against.
nicolastakashi added a commit that referenced this pull request Aug 4, 2026
… skill

Second review round on #207, all on the embedded Agent Skill content.

- Add an "SLO documents and OpenSLO v1 compatibility" section to
  docs/commands.md and wire it into the `slos` skill topic. It replaces the
  flatly wrong "SLO documents use the OpenSLO v1 format" claim: the
  `apiVersion` is domain-qualified (`openslo.com/v1`, dual-accepted with the
  upstream bare `openslo/v1` on write, canonicalized on read), only a subset
  is supported (one objective, inline `ratioMetric`, `Occurrences`, rolling
  `4w`/`28d`), a table lists what is rejected with a 400, `alertPolicies` is
  called out as accepted-but-silently-ignored, and queries must be bare
  PromQL vector selectors. Verified field-by-field against `SloDefinition`
  and `SloSpec` in the API OpenAPI spec.
- Note that the same document body is what the operator's `openslo.com/v1`
  `SLO` CRD accepts (group/kind/version and every spec field the example
  uses match; `alertPolicies` and `indicatorRef` are deliberately absent),
  phrased conservatively because that CRD is still unmerged
  (dash0hq/dash0-operator#1247).
- Expand "SLOs" to "service level objectives (SLOs)" on first use in the
  SKILL.md frontmatter description.
- Reframe the origin-vs-ID prose in SKILL.md and docs/commands.md. Origin
  being the upsert key is no longer an exception list: it is the rule for
  the API-managed kinds (notification channels, spam filters, SLOs, teams),
  while views, check rules, recording rules, and synthetic checks are the
  kinds where the server treats origin as provenance and the CLI strips it.
  Dashboards are called out separately, since their `dash0Extensions.origin`
  is deprecated in favor of `dash0Extensions.id`.
- Align the skill's SLO example with test/roundtrip/fixtures/slo.yaml so
  both use the Dash0 synthetic-check metrics (`dash0.synthetic_check.runs`
  split on `dash0_synthetic_check_outcome="Healthy"`) instead of
  `http_server_request_duration_seconds_count`.

Deduplicating the two published skill copies is tracked separately in #236.
@nicolastakashi
nicolastakashi marked this pull request as ready for review August 4, 2026 19:50
Comment thread internal/apply/apply.go Outdated
Comment thread internal/asset/diff_test.go Outdated
Comment thread internal/skill/content/references/apply.md Outdated
Comment thread internal/testutil/fixtures/slos/create_success.json
Add create/get/list/update/delete subcommands for service level
objectives (SLOs), backed by the /api/slos endpoint using OpenSLO v1
documents (apiVersion: openslo/v1, kind: SLO). Dataset-scoped; ID at
metadata.labels."dash0.com/id"; deep-link URL support.

Also wires SLO into `apply`, adds integration tests against the mock
server, a roundtrip test, docs, and a changelog entry.

Part of cross-facility SLO IaC support (API client, CLI, Terraform
provider, Kubernetes operator).

Note: builds against the local dash0-api-client-go worktree via a
go.mod replace directive while the API client change is unpublished.
The replace must be removed and the require version upgraded once the
API client is released.

Closes #206
The Dash0 SLO API document version changed from openslo/v1 to
openslo.com/v1. Update the CLI's SLO fixtures, tests, help text, docs,
and changelog entry to use the new apiVersion token.
- delete: add IsAlreadyDeleted idempotency branch so `slos delete --force`
  on an already-deleted SLO exits 0 (matches the 8 other delete commands,
  CHANGELOG 1.16.2 / #217); cover with an integration test.
- update: StripSLOServerFields before the dry-run diff and PUT so
  `get -o yaml | update -f` no longer sends server-managed labels/timestamps
  or shows a spurious diff (mirrors views/update.go).
- asset/ImportSLO: mirror ImportTeam upsert-key selection — prefer
  dash0.com/id, fall back to dash0.com/origin (read before strip), POST only
  when neither is present, so an origin-only document upserts via PUT instead
  of duplicating on every apply (#227 shape).
- list: replace interface{} with any on the column closures (matches
  views/list.go).
- integration tests: assert the update wire body strips dash0.com/version,
  dash0.com/origin, and created-at; add create/update --dry-run tests; add a
  second element to list_success.json and exercise --limit truncation.
- roundtrip: add test_apply_slo_idempotency.sh (double-apply, no duplicate /
  stable id) and register it in run_all.sh.
- docs: add teams to the command-group enumerations in commands.md and
  cli-naming-conventions.md and adjust the count wording.
SLO dash0.com/id is server-assigned (e.g. "slo_01k..."); a client cannot
choose it. The apply idempotency test injected a UUID as dash0.com/id and
asserted it echoed back — but the server assigns its own id and treats the
supplied value as the origin. Rework the test to inject dash0.com/origin
(the client-settable upsert key ImportSLO uses) and assert idempotency by
origin: single SLO, reachable by origin, no duplicate on re-apply.
The second apply correctly updates in place (same origin, only updated-at
changes), but the check `grep -q "created"` matched the
`dash0.com/created-at:` line in the update diff and reported a false
duplicate. Anchor the created/deleted matches to end-of-line so they hit
the action word only, not the *-at annotations. The exactly-one-per-origin
count assertion remains the authoritative duplicate check.
…tests

ImportSLO now faithfully mirrors ImportTeam's canonical upsert logic:

- Origin-first precedence. When dash0.com/origin is present it wins over
  dash0.com/id and is used as the upsert key (PUT is create-or-replace).
  Previously ImportSLO was id-first, which is the opposite of Teams.
- Preflight-driven id path with a genuine-404 POST fallback. On the id path
  the preflight GET routes the write: a 200 upserts via PUT, a 404 falls
  through to POST (cross-environment apply, where the id belongs to another
  org), and any other error is surfaced instead of silently POSTing — so a
  transient 5xx/auth/network blip never spawns a duplicate. Previously
  ImportSLO always PUT whenever any key was present.

Added the upsert-routing integration tests the SLO suite was missing,
driven through the real `dash0 slos create -f <file>` path and asserting
the HTTP method + path the mock received via server.Requests() (not
LastRequest, since the preflight GET precedes the write):

- TestCreateSLOFromFile_UpsertByOrigin
- TestCreateSLOFromFile_UpsertByID
- TestCreateSLOFromFile_UpsertByID_FallsBackToPOSTWhenNotFound
- TestCreateSLOFromFile_OriginWinsOverID
Michele is right that the previous idempotency test proved only that a second
apply created no duplicate — nothing in it tested idempotency. A re-apply of an
unchanged document should be a no-op, and `apply` expresses that by rendering an
empty before/after diff and printing "no changes". That is the contract every
sibling asset already meets (see TestApply_Dashboard_* and friends), so SLO now
meets it too. No new ImportAction state was introduced: ImportSLO still reports
`updated` for an upsert, and the "no changes" text comes from the diff being
empty.

The reason it was not empty: asset.marshalForDiff normalizes each asset through
its Strip*ServerFields helper before diffing, and *dash0api.SloDefinition was the
only asset type with no case in that switch. It therefore fell through to the
default branch and neither side was stripped. Because the server bumps
dash0.com/updated-at and dash0.com/version on every PUT — even for a
byte-identical body — every re-apply rendered a spurious two-line
version/updated-at diff. CI showed exactly that diff and nothing else. Adding the
missing case fixes `apply` and `slos update` together and changes nothing for any
other asset.

Assertions added:

- test_apply_slo_idempotency.sh: the second apply must state "no changes", and
  must report neither `created` nor `updated`. Both action-word greps stay
  anchored to end-of-line, because "dash0.com/updated-at" appears in diff text
  and would false-match an unanchored grep.
- test_slo_roundtrip.sh: re-applying the just-exported YAML must state
  "no changes", which is what closes the round-trip.
- TestApply_SLO_SecondApplyReportsNoChanges (internal/apply) reproduces the
  server behavior from the CI log against the mock server: the PUT response is
  the GET response with only version and updated-at bumped. It also pins that the
  second apply still upserts by origin and never POSTs.
- TestUpdateSLO_UnchangedDocumentReportsNoChanges (internal/slos) covers the same
  no-op through `slos update`.
- TestPrintDiff_SLO_ServerBumpedMetadataIsNotAChange and
  TestPrintDiff_SLO_RealSpecChangeStillDiffs unit-pin the normalization and guard
  that it does not swallow a genuine spec change.

These run on the mock server, so the contract is pinned without the live API. That
matters because only the `static` roundtrip shard actually executes: the oauth
matrix jobs are skipped-as-pass while DASH0_OAUTH_REFRESH_TOKEN is unset.

Also fixes a read-lag flake that failed the static roundtrip at Step 6 with
"found 0": `slos get "$ORIGIN"` succeeded while `slos list --all` still returned 0
for the same origin under a second after the restore PUT, and two unrelated
static jobs failed in the same minute with ingestion-lag symptoms. The
list-count assertions in both scripts now poll with backoff, as the log and span
roundtrips already do, instead of asserting once.

Finally, the roundtrip fixture's SLI now uses the Dash0 synthetic-check counter
dash0.synthetic_check.runs split on dash0.synthetic_check.outcome, which is a more
instructive availability example than a generic HTTP-duration counter. Metric name
and the "Healthy" outcome value were confirmed against the live metric catalog,
not guessed. Both queries remain bare vector selectors, as the API requires, and
counter: true still holds because the metric is a Sum.
…ST body

Addresses six pre-merge review findings on #207.

1. The docs taught `dash0.com/id` as the SLO upsert key while `ImportSLO` is
   origin-first, and SLO ids are server-assigned (`slo_<ulid>`) so the pinned
   example id could never exist. The identifier table, the SLO YAML example,
   the `apply` section, and the "notification channels and spam filters are the
   two exceptions" sentence (in both docs/commands.md and the skill's SKILL.md)
   now say origin — modeled on the existing `Dash0Team` row, which describes
   the same routing.
2. `apply --dry-run` printed no identifier for an origin-only SLO document (the
   recommended form). Added the origin fallback both origin-keyed baselines
   already have.
3. `slos get <origin>` built the deep link and the id label from the raw
   argument, producing a URL that does not resolve. Both are now read off the
   response, mirroring `notificationchannels/get.go`.
4. `StripSLOServerFields` does not clear `dash0.com/id`, so the cross-org POST
   fallback (id branch, preflight 404) sent the source org's id. `ImportSLO`
   now calls the previously-unused `dash0api.ClearSLOID` on that path, and the
   regression test asserts the POST body carries no id. Also deleted the
   `ImportSLO` doc comment sentence claiming the strip clears the id.
5. There was no `slos` topic in the Agent Skill bundle: added the
   `ManifestEntry`, the generator `topicSpec`, the SKILL.md topic-index row,
   and regenerated, so `dash0 skill show slos` resolves.
6. `slos list -o wide`/`-o csv` were missing the `ORIGIN` column the shared
   format contract promises, and `slos get` printed no `Origin:` line. Since
   origin is the SLO upsert key, it is the field users script against.
… skill

Second review round on #207, all on the embedded Agent Skill content.

- Add an "SLO documents and OpenSLO v1 compatibility" section to
  docs/commands.md and wire it into the `slos` skill topic. It replaces the
  flatly wrong "SLO documents use the OpenSLO v1 format" claim: the
  `apiVersion` is domain-qualified (`openslo.com/v1`, dual-accepted with the
  upstream bare `openslo/v1` on write, canonicalized on read), only a subset
  is supported (one objective, inline `ratioMetric`, `Occurrences`, rolling
  `4w`/`28d`), a table lists what is rejected with a 400, `alertPolicies` is
  called out as accepted-but-silently-ignored, and queries must be bare
  PromQL vector selectors. Verified field-by-field against `SloDefinition`
  and `SloSpec` in the API OpenAPI spec.
- Note that the same document body is what the operator's `openslo.com/v1`
  `SLO` CRD accepts (group/kind/version and every spec field the example
  uses match; `alertPolicies` and `indicatorRef` are deliberately absent),
  phrased conservatively because that CRD is still unmerged
  (dash0hq/dash0-operator#1247).
- Expand "SLOs" to "service level objectives (SLOs)" on first use in the
  SKILL.md frontmatter description.
- Reframe the origin-vs-ID prose in SKILL.md and docs/commands.md. Origin
  being the upsert key is no longer an exception list: it is the rule for
  the API-managed kinds (notification channels, spam filters, SLOs, teams),
  while views, check rules, recording rules, and synthetic checks are the
  kinds where the server treats origin as provenance and the CLI strips it.
  Dashboards are called out separately, since their `dash0Extensions.origin`
  is deprecated in favor of `dash0Extensions.id`.
- Align the skill's SLO example with test/roundtrip/fixtures/slo.yaml so
  both use the Dash0 synthetic-check metrics (`dash0.synthetic_check.runs`
  split on `dash0_synthetic_check_outcome="Healthy"`) instead of
  `http_server_request_duration_seconds_count`.

Deduplicating the two published skill copies is tracked separately in #236.
Upgrade to dash0-api-client-go v1.18.2, which clears dash0.com/id in
StripSLOServerFields (dash0hq/dash0-api-client-go#24). This removes the
local ClearSLOID workarounds that stood in for the missing behaviour.

Also adds the SLO case to marshalForDiff. SLO was the only asset type
absent from the type switch, so neither side of a diff was normalized and
re-applying an unchanged document rendered a spurious hunk from the
server-bumped dash0.com/updated-at and dash0.com/version instead of
reporting "no changes".

slos update -f now falls back to dash0.com/origin when no id is present,
matching notification channels and teams. SLO ids are server-assigned, so
a hand-authored document can only pin an origin — the flow the command's
own help advertises was previously impossible.
Three fixtures claimed to represent a Terraform-managed SLO but carried
`dash0.com/origin: terraform`, which is not a shape any tool produces. The
provider mints `"tf_" + uuid.New().String()` (slo_resource.go), so the
fixtures now use a `tf_<uuid>` value.

The all-zeros `dash0.com/id` is left as-is: it is the repo-wide placeholder
convention shared with the spam-filter tests, and reads as a deliberate
placeholder rather than a wrong format.
The SLO paragraph in `apply` defined its upsert-key selection as "the same as
Dash0Team", but Dash0Team is documented after SLO, so a reader going top-down
met the rule before its definition. The four lines that follow already spell
the selection out in full, so the cross-reference only added the dependency.
It now opens with the constraint that explains why origin is the key.

Also signposts the direction of the OpenSLO-compatibility pointer, which
refers to a section roughly 200 lines further down.

Edited docs/commands.md (the source) and regenerated the skill bundle; the
reference files are generated and carry a do-not-edit-by-hand header.
spec.service is not a plain service name: the API splits on the first `/`, so
`payments/eu` resolves to service.namespace=payments and service.name=eu, and a
leading `/` escapes a name that itself contains a slash (`/payments/eu` is the
literal name `payments/eu`). Every fixture used a bare name, so none of that
was exercised.

The CLI is a passthrough here, and that is the property worth pinning: a
well-meaning "normalize the path" change would silently retarget an SLO at a
different service. The new table test asserts all three forms reach the wire
byte-for-byte. Verified it fails when the value is mangled, not just that it
passes today.

Also documents the format, which was absent from the CLI docs entirely even
though the field is impossible to use correctly without it.
The comment claimed the fallback was about the upsert key, but parseDocumentHeader
feeds display output only: doc.id reaches nothing but formatNameAndId. Upsert
routing happens in ImportSLO, which keys on originOrId. The fallback now reads
like its notification-channel and team siblings.
`DELETE /api/slos/{originOrId}` has always accepted an origin, and the PR's own
roundtrip test deletes by one, but the cobra help claimed ID only. The help now
matches `slos get`, whose long description already documented both.

Also records that the omitted-timeWindow default is not observable on read: the
API returns `timeWindow: null` rather than an explicit 28d window, so a stored
document cannot distinguish "defaulted" from "unset". The existing sentence
described the behaviour correctly but implied a populated value.

Amended the existing changelog entry instead of adding one, since the feature is
unreleased and its subtext already claimed origin support for `get` alone.
Upgrades to dash0-api-client-go v1.19.0, which adds GetSLOOrigin
(dash0hq/dash0-api-client-go#28), and replaces the four open-coded reads of
the dash0.com/origin label with it:

- internal/asset/slo.go (ImportSLO upsert routing)
- internal/slos/update.go (`update -f` origin fallback)
- internal/apply/apply.go (parseDocumentHeader)
- internal/slos/slos_cmd.go (local sloOrigin helper, now deleted; list and get
  call the API-client helper directly)

Each carried its own nil-check for Labels and the pointer, and two carried
comments explaining that no such helper existed. Both comments are gone with
the code they described.

No behaviour change: GetSLOOrigin has the same nil-handling as the code it
replaces, matching GetTeamOrigin and GetNotificationChannelOrigin.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add dash0 slos command group (SLO IaC support)

2 participants