Skip to content

feat(oabctl): extract ProvisionDriver trait for the write path (K8s driver slice 3a) - #98

Merged
brettchien merged 2 commits into
mainfrom
feat/k8s-driver-slice3a-trait-extraction
Aug 23, 2026
Merged

feat(oabctl): extract ProvisionDriver trait for the write path (K8s driver slice 3a)#98
brettchien merged 2 commits into
mainfrom
feat/k8s-driver-slice3a-trait-extraction

Conversation

@brettchien

Copy link
Copy Markdown
Contributor

Summary

Why route through the trait now, with only one impl?

manifest::Runtime::Kubernetes has sat as a validated-but-rejected schema stub since slice-0 (both here and upstream in openab/operator) — precedent in this codebase for declaring a seam before the second side exists. Same reasoning here, but scoped tighter: EcsDriver is actually wired into studio_api's real call path (not dead code sitting unused), so slice 3b's K8sDriver has a genuine dispatch point to add itself to, instead of a speculative trait nothing calls.

Scope note

This covers the write path only (provision/scale/delete, the exact surface studio-cp depends on — verified via grep oabctl:: crates/studio-cp). Out of scope for 3a, tracked separately in #97:

  • status.rs/studio_api.rs read-model functions (service_status/instance_status) — no driver branching yet, needs a k8s-observe counterpart.
  • create.rs's interactive wizard — AWS-coupled from step 3 (Secrets Manager), before runtime is even chosen at step 4.

Testing

  • cargo build -p oabctl / cargo test -p oabctl (85 tests) / cargo clippy -p oabctl --all-targets — all clean.
  • Could not run the full cargo build --workspace --all-targets CI gate locally — this environment has a hard 21GB disk ceiling and the workspace's aws-sdk dependency tree exceeded it mid-build (No space left on device, not a code error — CI's own comment already documents "aws-sdk-ec2 exceeds small boxes' RAM" as a known constraint). My change is additive-only to oabctl's public API (new pub mod driver + two re-exports; no existing signature changed), so downstream crates (studio-cp, oab-mcp) shouldn't be affected, but flagging this so CI is the one that actually confirms it rather than my say-so.

Ref #97.

…river slice 3a)

studio_api's write path (provision/scale/delete) called ECS-specific code
(apply_manifests, ecsctl::scale_service, delete::run_with_bucket) directly,
with no trait boundary — ADR-2's "RuntimeDriver is the only layer with
vendor terms" only held for state classification, not CRUD. Introduce
ProvisionDriver and an EcsDriver impl that thin-wraps the existing free
functions unchanged, and route studio_api's write path through it.

No behavior change: EcsDriver's methods are pass-throughs, all 85 existing
oabctl tests stay green. This gives K8s driver (slice 3b, studio#97) a real
seam with an actual caller to dispatch through, instead of a speculative
trait with no consumer.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
…ename ecs_service_name

Found while starting K8sDriver (slice 3b): ProvisionDriver::apply took
apply::ApplyOptions directly (an ECS-shaped type — 'cluster' field documented
as "ECS cluster name or ARN"), and scale/delete took `cluster: &str` as a
per-call parameter. Neither generalizes to a k8s driver, which has no
"cluster" — it has a context+namespace bound at driver-construction time,
same as EcsDriver already binds aws_config.

- EcsDriver now holds `cluster` as an instance field alongside aws_config,
  matching how it already holds the AWS credential/region context.
- New ProvisionOptions carries only what's actually generic (bucket, wait);
  apply::ApplyOptions (still ECS-specific, used directly by apply_manifests)
  is unchanged.
- AppliedService.ecs_service_name -> resource_name: the only field crossing
  the trait boundary that still had a vendor-specific name. ServiceTarget
  (ECS-internal error detail, never crosses the trait) keeps its ECS name —
  it's genuinely ECS-only, not a leak.

No behavior change for the ECS path; 85/85 tests green. This was going to
bite immediately on K8sDriver's first method — fixing it now, before #98
merges, is cheaper than a second breaking change to studio_api after.
@brettchien

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit: found while starting 3b that ProvisionDriver's signature leaked ECS-specific shapes (ApplyOptions.cluster, per-call cluster: &str, AppliedService.ecs_service_name) — none of which generalize to a k8s driver (context+namespace, not "cluster"). Fixed now while #98 is still open rather than as a second breaking change later. No ECS behavior change, 85/85 tests + clippy clean. See commit message for detail.

@brettchien
brettchien merged commit 92443d5 into main Aug 23, 2026
2 checks passed
brettchien added a commit that referenced this pull request Aug 23, 2026
…yment (slice 3b)

Second ProvisionDriver impl, behind kube-rs. Scope deliberately narrow:

- apply(): builds a k8s Deployment from an OABServiceManifest (image,
  command = `openab run -c <configFrom>` — same convention EcsDriver uses,
  env NAMESPACE/NAME/BOOTSTRAP_FROM, cpu/memory as resource requests+limits,
  service account / node selector / tolerations from KubernetesRuntime),
  server-side-applies it. Deployment name is `oab-{name}` — OAB's own
  `namespace` maps directly onto the k8s namespace (already an isolation
  boundary, the same job it does for ECS's flat `oab-{ns}-{name}` naming),
  so unlike ECS there's no need to fold namespace into the resource name.
- scale(): patches replicas, same 0/1-only constraint as EcsDriver.
- delete(): deletes the Deployment, idempotent on 404.
- Kubeconfig context resolution: `K8sDriver::from_context(Option<&str>)` —
  `None` uses the kubeconfig's current context, same "ambient default,
  explicit override" shape aws_config uses. This is also how orbstack's
  local cluster gets targeted later (3f) — just another context, no
  special-casing.

Explicitly NOT handled yet, and apply() bails loudly rather than silently
mis-deploying if a manifest needs them:
- spec.bundleFrom (persona/skills bundle) — needs a ConfigMap/volume carrier,
  sub-slice 3c.
- spec.secrets — ECS resolves into Secret.valueFrom ARNs; k8s needs a
  different output shape (a Secret key selector), sub-slice 3d.

Also out of scope for this PR: observing k8s state into the canonical
6-state (status.rs's ECS service_status/instance_status has no k8s
counterpart yet — a new agent-lifecycle RuntimeDriver impl per ADR-2's
6-state<->k8s mapping table). Substantial enough to land as its own
follow-up rather than growing this one further.

Adds kube 0.99 + k8s-openapi 0.24 (rustls-tls, no openssl).

Stacked on #98 (3a) — this branch is 3a's branch + the K8sDriver commit;
GitHub will show 3a+3b combined in the diff until #98 merges, at which point
the diff narrows to just this. 7 new unit tests, 92/92 total green, clippy
-D warnings clean.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
brettchien added a commit that referenced this pull request Aug 23, 2026
…yment (slice 3b) (#99)

Second ProvisionDriver impl, behind kube-rs. Scope deliberately narrow:

- apply(): builds a k8s Deployment from an OABServiceManifest (image,
  command = `openab run -c <configFrom>` — same convention EcsDriver uses,
  env NAMESPACE/NAME/BOOTSTRAP_FROM, cpu/memory as resource requests+limits,
  service account / node selector / tolerations from KubernetesRuntime),
  server-side-applies it. Deployment name is `oab-{name}` — OAB's own
  `namespace` maps directly onto the k8s namespace (already an isolation
  boundary, the same job it does for ECS's flat `oab-{ns}-{name}` naming),
  so unlike ECS there's no need to fold namespace into the resource name.
- scale(): patches replicas, same 0/1-only constraint as EcsDriver.
- delete(): deletes the Deployment, idempotent on 404.
- Kubeconfig context resolution: `K8sDriver::from_context(Option<&str>)` —
  `None` uses the kubeconfig's current context, same "ambient default,
  explicit override" shape aws_config uses. This is also how orbstack's
  local cluster gets targeted later (3f) — just another context, no
  special-casing.

Explicitly NOT handled yet, and apply() bails loudly rather than silently
mis-deploying if a manifest needs them:
- spec.bundleFrom (persona/skills bundle) — needs a ConfigMap/volume carrier,
  sub-slice 3c.
- spec.secrets — ECS resolves into Secret.valueFrom ARNs; k8s needs a
  different output shape (a Secret key selector), sub-slice 3d.

Also out of scope for this PR: observing k8s state into the canonical
6-state (status.rs's ECS service_status/instance_status has no k8s
counterpart yet — a new agent-lifecycle RuntimeDriver impl per ADR-2's
6-state<->k8s mapping table). Substantial enough to land as its own
follow-up rather than growing this one further.

Adds kube 0.99 + k8s-openapi 0.24 (rustls-tls, no openssl).

Stacked on #98 (3a) — this branch is 3a's branch + the K8sDriver commit;
GitHub will show 3a+3b combined in the diff until #98 merges, at which point
the diff narrows to just this. 7 new unit tests, 92/92 total green, clippy
-D warnings clean.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
brettchien added a commit that referenced this pull request Aug 23, 2026
…r kubeconfig contexts (slice 3e)

observe_k8s_identity(context) is the k8s counterpart to the existing AWS
observe_identity(aws_config) — same RuntimeContext struct, per ADR-19's
AWS-driver/k8s-driver field mapping table:

  principal  <- SelfSubjectReview username (authentication.k8s.io/v1,
                stable since k8s 1.28 — the literal API `kubectl auth
                whoami` calls; a live server round-trip, not a value read
                out of the kubeconfig file, matching how observe_identity
                uses a live STS GetCallerIdentity rather than trusting the
                configured profile)
  principal_kind <- "service-account" / "user" / "unknown", mirrors
                     principal_kind's role/user/unknown split
  scope      <- "{cluster}/{namespace}" from the kubeconfig context entry
  location   <- left empty: k8s has no first-class region/zone concept
                the way AWS does, so there's nothing honest to fill in
                (same "empty if unset" contract location already has)
  source     <- "kubeconfig context: <name>"
  caller_id  <- SelfSubjectReview's UserInfo.uid

context = None uses the kubeconfig's current-context, mirroring
K8sDriver::from_context's "ambient default, explicit override" shape.

Adds kube 0.99 + k8s-openapi 0.24 to studio-cp (same versions oabctl
already pulls in slice 3b). 1 new test (k8s_principal_kind), 18/18 total
green in studio-cp. clippy clean for this code specifically — studio-cp/
studio-compose already carry a few pre-existing warnings elsewhere in the
crate, untouched by this change, not part of CI's gate for these crates.

Not stacked on #98/#99/#100 — this only touches studio-cp, independent of
oabctl's driver work, so it branches from main directly.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
brettchien added a commit that referenced this pull request Aug 23, 2026
…r kubeconfig contexts (slice 3e) (#101)

observe_k8s_identity(context) is the k8s counterpart to the existing AWS
observe_identity(aws_config) — same RuntimeContext struct, per ADR-19's
AWS-driver/k8s-driver field mapping table:

  principal  <- SelfSubjectReview username (authentication.k8s.io/v1,
                stable since k8s 1.28 — the literal API `kubectl auth
                whoami` calls; a live server round-trip, not a value read
                out of the kubeconfig file, matching how observe_identity
                uses a live STS GetCallerIdentity rather than trusting the
                configured profile)
  principal_kind <- "service-account" / "user" / "unknown", mirrors
                     principal_kind's role/user/unknown split
  scope      <- "{cluster}/{namespace}" from the kubeconfig context entry
  location   <- left empty: k8s has no first-class region/zone concept
                the way AWS does, so there's nothing honest to fill in
                (same "empty if unset" contract location already has)
  source     <- "kubeconfig context: <name>"
  caller_id  <- SelfSubjectReview's UserInfo.uid

context = None uses the kubeconfig's current-context, mirroring
K8sDriver::from_context's "ambient default, explicit override" shape.

Adds kube 0.99 + k8s-openapi 0.24 to studio-cp (same versions oabctl
already pulls in slice 3b). 1 new test (k8s_principal_kind), 18/18 total
green in studio-cp. clippy clean for this code specifically — studio-cp/
studio-compose already carry a few pre-existing warnings elsewhere in the
crate, untouched by this change, not part of CI's gate for these crates.

Not stacked on #98/#99/#100 — this only touches studio-cp, independent of
oabctl's driver work, so it branches from main directly.

Ref: studio#97 (K8s driver — ADR #63 slice 3, sub-slice tracking)
@brettchien
brettchien deleted the feat/k8s-driver-slice3a-trait-extraction branch August 23, 2026 17:31
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