Skip to content

feat(controls): manage controls from the CLI#986

Merged
pbeckham merged 13 commits into
mainfrom
5742-manage-controls
Jul 1, 2026
Merged

feat(controls): manage controls from the CLI#986
pbeckham merged 13 commits into
mainfrom
5742-manage-controls

Conversation

@pbeckham

@pbeckham pbeckham commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

What

Adds CLI parity with the released controls API so controls can be created, inspected, tagged and archived without the UI. Implements the command surface from the Definition of Ready in kosli-dev/server#5742.

Commands

Command Endpoint
kosli create control <id> --name <name> [--description] POST /api/v2/controls/{org}
kosli list controls [--search] [--tag] [--archived] [--page/--page-limit] [--output] GET /api/v2/controls/{org}
kosli get control <id> [--output] GET /api/v2/controls/{org}/{id}
kosli archive control <id> POST /api/v2/controls/{org}/{id}/archive
kosli unarchive control <id> (new unarchive command group) POST /api/v2/controls/{org}/{id}/unarchive
kosli tag control <id> --set/--unset PATCH /api/v2/tags/{org}/control/{id}

Notable changes

  • 409-retry fix (internal/requests): the shared client retries 409 (transient lock conflicts), which turned a duplicate-identifier 409 on control-create into a confusing "giving up after N attempts". Added a per-request DisableConflictRetry (threaded via request context) so control-create surfaces the real error immediately; lock-conflict retries are preserved everywhere else.
  • kosli tag now accepts control/controls as a resource type (the server already supported it).
  • Registry-driven OpenAPI contract test: fetches the live schema from the test server and asserts each CLI payload struct matches its schema component (every field it sends exists; every required property is sent). Seeded with control + environment; adding an entity is a one-line registry change. This is the "keep the CLI in sync with the schema" mechanism.

Testing

Every command has happy- and unhappy-path coverage: argument validation, required flags, duplicate-409, not-found, invalid-identifier (server validation surfaced verbatim), pagination bounds, filter correctness (search/tag/archived), and archived/active state verified via get. All green against the local server from a clean DB.

Out of scope (noted for follow-up)

  • The generic : map[...] suffix on API error messages is the shared client's formatting for any message+errors response — cosmetic, repo-wide, not controls-specific.
  • --link/links on create control and policies on environment are not exposed by the CLI yet; the contract test logs these as coverage gaps (not failures).

Refs kosli-dev/server#5742

🤖 Generated with Claude Code

pbeckham and others added 8 commits June 30, 2026 13:07
Add `kosli create control <identifier> --name <name> [--description]`
which POSTs to /api/v2/controls/{org}, the first slice of CLI parity for
the controls API (#5742).

Also fix 409 handling in the shared HTTP client: 409 is retried by
default (transient lock conflicts), but control creation uses 409 for a
duplicate identifier, a permanent error. Add a per-request
RequestParams.DisableConflictRetry (threaded via request context) so
control-create surfaces the duplicate error immediately instead of
retrying and reporting "giving up after N attempts". Lock-conflict
retries are preserved for all other requests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add `kosli list controls` which GETs /api/v2/controls/{org} and renders
the HAL list response as a table or JSON (--output). Supports pagination
via --page and --page-limit, with an empty-state message and a page
footer (#5742).

Tests cover the populated/empty cases (table and JSON), page-limit
capping, the echoed pagination params, a page beyond the data, and the
positive-integer validation for --page and --page-limit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ontrols'

Wire the list controls command to the API's filtering query params:
--search (name/identifier substring), --tag (repeatable, 'key' or
'key:value') and --archived (list archived instead of active) (#5742).

Add ArchiveControl and TagControl test helpers and cover each filter:
search by identifier, tag-only matches, archived excluded by default,
and --archived returning archived controls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add "control"/"controls" to the tag command's accepted resource types
(the server already supports tagging controls via
/api/v2/tags/{org}/control/{id}) and document it with an example.

Tests cover adding, removing and the plural resource-type alias on a
control (#5742).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add `kosli get control <identifier>` which GETs
/api/v2/controls/{org}/{identifier} and renders the control as a table
(identifier, name, description, version, archived, created by/at, tags,
links, referencing policies) or JSON via --output (#5742).

Tests cover argument validation, fetching an existing control as table
and JSON, and the clear error for a non-existing control.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rol'

Add `kosli archive control <identifier>` (POST .../archive) and a new
`unarchive` command group with `kosli unarchive control <identifier>`
(POST .../unarchive) (#5742).

Tests cover argument validation, the success messages, the resulting
archived/active state (verified via get control), and the clear error
when the control does not exist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hive

Cover the remaining unhappy paths for control commands (#5742):
- create control with an invalid identifier surfaces the server's
  validation error verbatim
- tagging a non-existing control gives a clear "does not exist" error
- unarchiving a non-existing control gives a clear "does not exist" error

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guard against drift between the CLI's hand-written request payload structs
and the Kosli API by fetching the live OpenAPI schema from the test server
and asserting, per registered case, that every json field the CLI sends
exists in the schema component and every required schema property is sent
by the CLI. Optional schema properties the CLI does not expose are logged,
not failed.

Seeded with create control (ControlPostInput) and create environment
(CreateEnvironmentPutInput) to prove the registry generalises beyond
controls; adding an entity is a one-line registry change (#5742).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished @pbeckham's task in 3m 31s —— View job


Code review — feat(controls): manage controls from the CLI

  • Static analysis (go build, go vet, go mod tidy)
  • Review command code (create/list/get/archive/unarchive/tag)
  • Review internal/requests 409-retry change
  • Review tests + OpenAPI contract test
  • Post review

Static analysis ✅

go build ./..., go vet ./... both clean. go mod tidy leaves go.mod/go.sum unchanged. No compile/vet/dependency issues.

Overall

This is a clean, well-structured PR. The command files follow the established new<Command>Cmd() factory + PreRunE/RunE pattern consistently, flag help lives in root.go with the rest, and every command has happy- and unhappy-path coverage. Nicely done.

Highlights worth calling out

  • DisableConflictRetry (internal/requests) is the right fix, done well: a private contextKey type (no collision risk), the retry opt-out threaded through customCheckRetry, lock-conflict retries preserved everywhere else, and a dedicated test (/conflict/ with DisableConflictRetry: true) that pins the surfaced-immediately behavior. The earlier review's determinism fixes in getControl.go (sorted tags/links, != nil guards) are also in and look correct.
  • Registry-driven OpenAPI contract test is a genuinely good mechanism for keeping CLI payloads in sync with the schema, and the "log-don't-fail" treatment of unexposed optional properties is a sensible default.

Suggestions (all minor / non-blocking)

  1. Doc inconsistency in list controls example — flagged inline (listControls.go:41): the comment uses framework=finos-sdlc while the command and flag help use the colon form.

  2. Contract test doesn't recurse into embedded structsjsonFieldNames (openapiContract_test.go:113) iterates only top-level NumField(), so an anonymous/embedded payload struct (like the CommonAttestationPayload pattern used elsewhere) would silently contribute no field coverage. Both current cases (ControlPayload, CreateEnvironmentPayload) are flat so it's fine today — worth a one-line comment noting the limitation so a future registry addition doesn't get a false sense of coverage.

  3. On the "isn't this a server-side test?" threads (@FayeSGW) — a fair point on the assertions that match the exact server-generated wording via goldenRegex (invalid-identifier, duplicate-409, not-found). The high-value CLI-side guarantee is that the command surfaces the error (non-zero exit + printed message) rather than swallowing/masking it — most importantly that create's DisableConflictRetry turns the duplicate 409 into a real error instead of "giving up after N attempts". Matching the server's literal phrasing ("A control with identifier ... already exists", "contains invalid characters") couples these CLI tests to server copy, so they'll break if the server rewords. If you want to keep them CLI-side, consider loosening those regexes to the CLI-owned part of the behavior; if the intent is purely to prove surfacing, a shorter anchor (e.g. just ^Error: plus already exists) is less brittle. Not blocking — just the trade-off behind Faye's question.

Nothing here blocks merge. 👍
· 5742-manage-controls

Comment thread cmd/kosli/getControl.go
Satisfy the errcheck linter by handling the deferred Body.Close return
value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread cmd/kosli/getControl.go
Comment thread cmd/kosli/getControl.go Outdated
pbeckham and others added 2 commits July 1, 2026 11:55
…ol table

Address PR review on the get control table printer:
- sort tags and links keys before printing so output is deterministic
  (matching tag.go), instead of relying on random map iteration order
- guard version, archived and created_by with the same != nil check used
  for description and created_at, so an explicit null never renders as a
  Go error verb

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The controls feature is gated server-side behind the is-controls-enabled
flag and published as beta in the API schema, so mark the create, list,
get, archive and unarchive control commands beta (visible, with the beta
banner in --help), consistent with other beta CLI commands. Add a
lifecycle test asserting all five stay beta while flag-gated (#5742).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread cmd/kosli/listControls.go
Drop Hidden and the docHidden annotation from attest decision so it
appears in --help and the generated docs, keeping the betaCLIAnnotation
so it still shows the beta banner. Update the lifecycle test to assert
the command is beta and visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread cmd/kosli/archiveControl_test.go Outdated
Comment thread cmd/kosli/listControls.go
Comment thread cmd/kosli/listControls.go Outdated
Comment thread cmd/kosli/listControls_test.go
Comment thread cmd/kosli/unarchiveControl_test.go Outdated
- drop the "control is archived/active afterwards" cases from the
  archive/unarchive tests: verifying persisted state is the server's
  responsibility, and the existing archiveFlow/archiveEnvironment tests
  assert only the success message and error cases
- add --search, --tag and --archived examples to list controls help
- move listControlsResponse up with the other type declarations

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread cmd/kosli/listControls.go
@pbeckham pbeckham merged commit 51b04a6 into main Jul 1, 2026
20 checks passed
@pbeckham pbeckham deleted the 5742-manage-controls branch July 1, 2026 14:29
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.

2 participants