Skip to content

feat(serviceactivation): shared, status-keyed service-enablement SDK#242

Draft
scotwells wants to merge 4 commits into
mainfrom
feat/service-activation-sdk
Draft

feat(serviceactivation): shared, status-keyed service-enablement SDK#242
scotwells wants to merge 4 commits into
mainfrom
feat/service-activation-sdk

Conversation

@scotwells

@scotwells scotwells commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What this enables

Datum services are enabled per project through a ServiceEntitlement, and for a gated service (like compute) that request awaits a human approval on the provider side. Every CLI plugin that gates on enablement has had to hand-roll the same flow — and has gotten it wrong the same way: request access, wait a fixed 15 seconds, then print Error: and "try again in a moment" no matter what actually happened, pointing at datumctl services … commands that don't exist.

This PR adds a shared, service-agnostic serviceactivation package so plugins stop forking that flow and instead get one honest experience. It re-keys everything off the entitlement's own status, so the CLI tells the truth in every state.

Before (what a user sees today, verbatim):

$ datumctl compute instances
Compute is not enabled for project "datum-cloud".
Would you like to request access? [y/N]: y
Requesting access to compute for project "datum-cloud"...

Access to compute for project "datum-cloud" has been requested.
Run your command again once it becomes active.

Check status with: datumctl services list
Error: compute access is not yet active — try again in a moment

After (compute, the first consumer):

$ datumctl compute instances
Compute is not enabled for project "datum-cloud".
Requesting access sends an enablement request to the service provider for approval.
Would you like to request access? [y/N]: y
Requesting access to compute for project "datum-cloud"...
⠋ Waiting for the platform to process the request... (4s)

Your request to enable compute for project "datum-cloud" has been submitted.

  Status:  Pending approval — Waiting for the service provider to approve this request.
           Approval is a manual step by the service provider and may take a while.

Check progress with:   datumctl compute access
Wait for activation:   datumctl compute access request --wait

The request no longer reads as an error, pending approval is a legible waiting state, and every printed command exists. Scripts get deterministic, documented exit codes instead of a blanket exit 1.

What's in the package

serviceactivation is configured per service (object name, canonical name, display noun, and the plugin's own access verb) and takes an injected client plus IO streams:

  • Pure state classifier — reduces an entitlement (or its absence) to one of eight states: not requested, processing, pending approval, active, denied, revoked, unavailable, catalog-unavailable. Denied vs. revoked splits on entitledAt; ServiceNotPublished maps to unavailable; transient relay reasons never drive control flow.
  • Preflight gate — prompts and submits on a TTY, never prompts or mutates without one; contains the bounded first-status wait.
  • Request/renew/wait primitives — create with an admission-error mapping, renew as delete + tombstone-aware recreate, and an opt-in --wait that states up front that approval is a manual provider step with no time bound.
  • Shared status renderer and a documented exit-code contract (10–13 for not-enabled / denied-or-revoked / pending / unavailable; 0/1/9 unchanged).

The dependency-free plugin package is deliberately left untouched; this package is its sibling and takes the go.miloapis.com/service-catalog dependency on its own.

Follow-ups (not in this PR)

  • The state model mirrors two things service-catalog keeps internal today — the Ready condition type and reason constants, and a canonical status.serviceName for lookup. The SDK carries local copies with a one-line seam to switch over once those are promoted to api/v1alpha1.
  • The RFC's phase 2/3 (a datumctl services command group on these same internals, and ipam adopting the shared exit codes) are separate proposals.

Design/rationale: the compute repo RFC docs/compute/development/rfcs/cli-service-activation.md.

Related

The compute plugin PR datum-cloud/compute#113 is the first consumer and depends on this PR. It currently pins this branch by pseudo-version for CI; it must be re-pinned to a tagged/merged datumctl version before merge.

Update — backed by the generated service-catalog clientset

The SDK's EntitlementClient is now implemented over the generated typed clientset from milo-os/service-catalog#50 (go.miloapis.com/service-catalog/pkg/generated) instead of a hand-built controller-runtime client. The public surface is unchanged (same EntitlementClient interface, Gate, Requester, typed errors), and the flow tests run against the generated fake clientset, exercising the real adapter path.

  • New dependency: go.miloapis.com/service-catalog is pinned by pseudo-version to that PR's branch commit; it must be re-pinned to a tagged/merged service-catalog release before this merges.
  • controller-runtime verdict: this package no longer imports controller-runtime. It remains a direct datumctl dependency only because core (internal/discovery, internal/client) still uses it — so nothing was dropped from the module.

Dependency chain: datum-cloud/compute#113 → this PR → milo-os/service-catalog#50 (merge in reverse order).

scotwells and others added 2 commits July 9, 2026 17:48
Turn a project's ServiceEntitlement into an honest CLI experience with a
service-agnostic package that plugins and datumctl core can adopt instead
of forking the flow. It provides a pure classifier for the eight
activation states (not requested, processing, pending approval, active,
denied, revoked, unavailable, catalog unavailable), an
interactive/non-interactive preflight gate, explicit request/renew/wait
primitives, a shared status renderer, and a documented exit-code contract
(10-13 for not-enabled / denied-or-revoked / pending / unavailable).

Consumers configure it per service (object name, canonical name, display
noun, and their own access verb) and inject a client and IO streams, so
every consumer gets consistent wording and exit codes. Adds the
go.miloapis.com/service-catalog dependency; the dependency-free plugin
package is left untouched.
scotwells and others added 2 commits July 9, 2026 19:31
…ed clientset

Swap the SDK's EntitlementClient implementation from a hand-built
controller-runtime client to the generated services clientset
(go.miloapis.com/service-catalog/pkg/generated). The public surface is
unchanged — same EntitlementClient interface, Gate, Requester, and typed
errors — so consumers are unaffected. The flow tests now run against the
generated fake clientset, exercising the real adapter path.

controller-runtime is no longer imported by this package; it remains a direct
datumctl dependency only because core (internal/discovery, internal/client)
still uses it.
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.

1 participant