feat: add dash0 slos command group (SLO IaC support) - #207
feat: add dash0 slos command group (SLO IaC support)#207nicolastakashi wants to merge 17 commits into
dash0 slos command group (SLO IaC support)#207Conversation
nicolastakashi
left a comment
There was a problem hiding this comment.
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.
- 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>
…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>
… 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>
- 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>
…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>
… 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>
- 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.
…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.
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.
Summary
Adds a
dash0 sloscommand group (create/get/list/update/delete) for managing service level objectives (SLOs) as code, backed by the/api/slosendpoint using OpenSLO documents (apiVersion: openslo.com/v1,kind: SLO).--dataset; ID atmetadata.labels."dash0.com/id"; deep-link URL support (/goto/alerting/slos/details).SLOintoapply(create-or-replace by ID).test/roundtrip/test_slo_roundtrip.sh, registered inrun_all.sh) — validated end-to-end against dev: create → list → get → export → apply → delete.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-gov1.18.0 (SLO client support) — no localreplace. Theopenslo.com/v1apiVersion 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/lintpass.test_slo_roundtripandtest_apply_slo_idempotencypass against dev on the static-token shard. Theoauthshard reports green but its steps are skipped —DASH0_OAUTH_REFRESH_TOKENis not provisioned, so that column carries no signal (see #236 discussion). Any red on unrelatedtest_log_roundtrip/test_span_roundtrip/test_apply_check_rule_idempotencyis dev read-lag, not from this change.Closes #206