Skip to content

docs: Cloud-SDK Extraction Phase B/C/D plan + ADR 0036 (Configure RPC)#677

Merged
intel352 merged 4 commits into
mainfrom
feat/cloud-sdk-extraction-bcd
May 14, 2026
Merged

docs: Cloud-SDK Extraction Phase B/C/D plan + ADR 0036 (Configure RPC)#677
intel352 merged 4 commits into
mainfrom
feat/cloud-sdk-extraction-bcd

Conversation

@intel352
Copy link
Copy Markdown
Contributor

Summary

  • Locked implementation plan for Phase B/C/D of the cloud-SDK extraction (docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md) — 10 PRs / 29 tasks across workflow + workflow-plugin-{azure,aws,gcp,digitalocean}.
  • decisions/0036 — adds the IaCStateBackend.Configure RPC: Phase A's contract shipped with no config-passing RPC, leaving plugin-served IaC state backends non-functional end-to-end. PRs 1–2 of the B/C/D plan close that gap.
  • Scope-lock file (alignment-check + adversarial-design-review both passed; cycle-2 PASS).

Docs/decisions only — no code. This lands the plan + ADR on main so the 10 execution PRs (each off main) can reference decisions/0036.

Test plan

  • plan-scope-check.sh PASS (manifest well-formed, 10 PRs / 29 tasks)
  • adversarial-design-review --phase=plan: PASS (cycle 2)
  • alignment-check: PASS (zero drift)
  • docs-only — no build/test impact

🤖 Generated with Claude Code

intel352 and others added 4 commits May 14, 2026 17:23
8 PRs / 26 tasks across 4 repos (workflow + workflow-plugin-{aws,gcp,digitalocean}).
Builds on Phase A's merged IaCStateBackend contract + ListBackendNames RPC +
SDK serve hook + engine host-wiring. Phase B (AWS) + Phase D (DigitalOcean) share
the S3-compatible store; Phase C (GCP) gke contract gated on an interface-audit
spike (ADR 0036, Task 16).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ adversarial fixes

Cycle-1 adversarial review FAIL + a grep-verified design gap (ADR 0035 lesson):
the IaCStateBackend contract has no config-passing RPC, so Phase A's plugin-served
backends are non-functional end-to-end. Add ADR 0036 (Configure RPC) and PRs 1-2
(host wiring + azure retrofit) as the prerequisite. Fold in cycle-1 fixes: aws
S3 store drops DO env fallbacks; no in-core s3 case to delete; credential_source
gap-window warning + coordinated-upgrade migration note; credentials_ref mechanism
specified; minEngineVersion comparison verified before set; Option-3 PR9/PR8
serial note; parity-audit tasks folded. 8 PRs/24 tasks -> 10 PRs/29 tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Important: PR1->PR2 azure startup-gap-window co-deploy note in Task 4 CHANGELOG +
Task 18 migration doc. Minor: ConfigureRequest/Response compile-guard line in
Task 1; intentional-stdlib-log note in Task 15; explicit v1.0.0->v1.1.0 in Task 10;
soft-fail rejected-alternative recorded in ADR 0036.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 14, 2026 22:05
@codecov
Copy link
Copy Markdown

codecov Bot commented May 14, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR documents the next cloud-SDK extraction phases and records ADR 0036 for adding IaCStateBackend.Configure so plugin-served state backends can receive configuration.

Changes:

  • Adds a locked Phase B/C/D implementation plan spanning workflow core and cloud provider plugins.
  • Adds ADR 0036 for the IaCStateBackend.Configure RPC and its operational tradeoffs.
  • Adds a scope-lock hash for the new plan.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md New multi-phase implementation plan for extracting AWS/GCP/DO SDK-backed surfaces into plugins.
docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md.scope-lock Scope-lock hash for the new plan.
decisions/0036-iac-state-backend-configure-rpc.md ADR accepting the new Configure RPC for plugin-served IaC state backend config plumbing.
Comments suppressed due to low confidence (18)

docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:958

  • This refers to platform.kubernetes selecting gke via provider, but the module actually reads the backend from config key type (see module/platform_kubernetes.go and the schema's type field). If the migration guide follows this text, users will be told to set the wrong YAML key and gke configs may continue to default to kind.
**Step 1: Write the Phase C migration section** — `iac.state backend: gcs` → load `workflow-plugin-gcp`; `platform.kubernetes provider: gke` → load `workflow-plugin-gcp` (`provider: kind|k3s|eks|aks` unchanged, still core); `storage.gcs` → load `workflow-plugin-gcp`, `credentials:` inline (or `credentials_ref:` a `gcp.credentials` module). yaml `backend:`/`provider:`/module-type names unchanged.

docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:874

  • This implementation instruction uses provider for the Kubernetes backend selector, but PlatformKubernetes.Init reads m.config["type"] and the schema exposes type as the cluster selector. The task should describe dispatching on type/cluster type; otherwise the registry wiring may be implemented or tested against a nonexistent config key.
**Context:** Structurally-identical to Phase A's `iac.state` plugin-backend wiring, for **kubernetes backends**: a `kubernetesBackendClientRegistry` (`gke` → contract client), an exported `RegisterKubernetesBackendClient`, a `plugin.KubernetesBackendProvider` optional interface, an `ExternalPluginAdapter` accessor, the `engine.go` `loadPluginInternal` seam. `module/platform_kubernetes.go` resolution: `provider: kind|k3s|eks|aks` use the in-core `kubernetesBackendRegistry` factory map unchanged; any other provider (`gke`) consult the new client registry and wrap the client in Task 25's `grpcKubernetesBackend`.

docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:567

  • The rewrite drops data that the current role_arn resolver consumes from the same credentials block: inline base accessKey/secretKey/sessionToken and sessionName. After core only emits RoleARN/external_id/credential_source, the plugin's BuildAWSConfig cannot recreate the current AssumeRole behavior for configs that supply base static credentials or a custom session name, so this task should preserve those fields (for example in CloudCredentials/Extra) and test them.
`awsRoleARNResolver.Resolve` — keep the `credsMap` nil-check, the `roleARN`/`externalID` extraction, the `RoleARN` + `Extra["external_id"]` records, the `roleARN == ""` required-check; then:
```go
	m.creds.Extra["credential_source"] = "role_arn"
	logCredentialSourceMarker("aws", "role_arn")
	return nil
}

(delete the sessionName extraction and the entire SDK block.) Add a small logCredentialSourceMarker(provider, source string) helper. The resolver call-site is a pure resolver with no module-scoped logger, so a stdlib log.Printf is the intentional pragmatic choice here — add a // TODO: plumb a structured logger when the resolver gains module context comment so it isn't mistaken for an oversight. Update the import block to "fmt" + "log" + "os" (drop context, aws, config, credentials, sts).

**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:323**
* `CredInput` omits `SessionName`, even though the existing in-core `awsRoleARNResolver` reads `credentials.sessionName` and passes it to STS AssumeRole. Without carrying that field into the plugin credential API, the extraction silently changes AssumeRole behavior for users with a custom session name.

Step 3: Implementinternal/awscreds/awscreds.go: func BuildAWSConfig(ctx context.Context, creds CredInput) (aws.Config, error) where CredInput carries AccessKey/SecretKey/SessionToken/Region/RoleARN/ExternalID/Profile/Source. Logic: Source == "profile"config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile(profile)); Source == "role_arn" (or RoleARN != "") → port the deleted-from-core awsRoleARNResolver SDK block (base config + sts.NewFromConfig + AssumeRole), return a config carrying the assumed creds; static keys → config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(...)); else → config.LoadDefaultConfig(ctx). Wire the aws plugin's existing IaC-provider credential path to call BuildAWSConfig so a host-supplied CloudCredentials with a marker resolves in-plugin.

**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:352**
* Resolving `credentials_ref` from a process-local registry during the consumer factory call makes module order load-bearing: if a `storage.s3`/`step.s3_upload` module appears before the referenced `aws.credentials` module (or lacks an explicit `dependsOn` that causes topological reordering), the lookup fails even though the config is otherwise valid. The plan should either require/validate `dependsOn`, add a transform that injects it from `credentials_ref`, or defer resolution until module Init after all factories have run.

Context: storage.s3 becomes a plugin-native module via the existing ModuleFactories SDK path (no new contract). Credentials move inline per design §3 Option-1: a credentials: block resolved via awscreds.BuildAWSConfig, OR credentials_ref: an in-plugin aws.credentials module. credentials_ref: resolution mechanism (explicit per adversarial review): the aws plugin maintains a process-local credref registry (a map[string]CredInput guarded by a mutex, package internal/modules/credref); each aws.credentials module registers its resolved CredInput under its module name at factory-construction time; storage.s3/step.s3_upload factories look up credentials_ref: in that registry. credentials_ref: names must be unique within a config — duplicate registration is a factory error, not a silent clobber.

Step 1: Write the failing testsstorage_s3_test.go: factory builds the module from a config with an inline credentials: block AND from one with credentials_ref:. aws_credentials_test.go: the module registers its CredInput in the credref registry under its name; a second module with the same name → error.

Step 2: Verify they failcd /Users/jon/workspace/workflow-plugin-aws && go test ./internal/modules/... -v → FAIL undefined.

Step 3: Implementinternal/modules/credref/registry.go (the mutex-guarded map[string]CredInput + Register(name, CredInput) error rejecting duplicates + Resolve(name) (CredInput, bool)). Port module/s3_storage.gointernal/modules/storage_s3.go (package modules, resolve creds via awscreds.BuildAWSConfig from the inline block or the credref registry). internal/modules/aws_credentials.go — the aws.credentials module: parses a credentials: block into a CredInput, registers it in credref. Register "storage.s3" + "aws.credentials" in ModuleFactories; update plugin.json.

**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:793**
* This mirrors the AWS `credentials_ref` registry pattern, so it inherits the same ordering problem: resolving the ref in the `storage.gcs` factory only works if the `gcp.credentials` factory has already registered the name. Without an explicit dependency/transform or deferring lookup to Init, valid configs can fail solely because the credential module appears later in the YAML.

Context: storage.gcs becomes plugin-native, mirroring Task 8. The gcp credential resolvers (module/cloud_account_gcp.go) are already SDK-free, so gcpcreds.BuildGCPOptions builds []option.ClientOption from an inline credentials: block (ServiceAccountJSONoption.WithCredentialsJSON) with an ADC fallback. gcp.credentials + the credref registry mirror Task 8 exactly. This task also cuts the gcp plugin release (PR 10 is blocked on it).

Step 1: Write the failing testsgcpcreds_test.go: BuildGCPOptions with inline service-account JSON; with empty input (ADC fallback). storage_gcs_test.go: factory from a config with credentials:/credentials_ref:. gcp_credentials_test.go: the module registers in credref by name; duplicate → error.

Step 2: Verify they failcd /Users/jon/workspace/workflow-plugin-gcp && go test ./internal/gcpcreds/ ./internal/modules/... -v → FAIL.

Step 3: Implementinternal/gcpcreds/gcpcreds.go (BuildGCPOptions); internal/modules/credref/registry.go (mirror Task 8); port module/storage_gcs.gointernal/modules/storage_gcs.go; internal/modules/gcp_credentials.go (the gcp.credentials DRY module). Register "storage.gcs" + "gcp.credentials" in ModuleFactories; extend host_conformance_test.go (capability parity for everything PR 8 added); update plugin.json (moduleTypes + a minor version bump); CHANGELOG entry naming gcs + the gke contract + storage.gcs/gcp.credentials.

**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:611**
* This deletion plan misses the hardcoded type/schema registries that still advertise these as core types. `storage.s3` is in `schema/schema.go`/`schema/module_schema.go`, `cmd/wfctl/type_registry.go`, `module/api_workflow_ui.go`, and manifest analyzers; `step.s3_upload` is also in `schema/step_schema_builtins.go`/`schema/module_schema.go`/`schema/schema.go`. If only the plugin factory maps and DOCUMENTATION are changed, validation/UI can still treat removed built-ins as available while the engine has no factory unless the AWS plugin is loaded.
  • Modify: plugins/storage/plugin.go (drop the "storage.s3" factory :89, the capability entry :37, the schema :326)
  • Modify: plugins/pipelinesteps/plugin.go (drop the "step.s3_upload" factory :183, the capability entry :93)
  • Modify: DOCUMENTATION.md (remove storage.s3 / step.s3_upload from the module/step tables)
**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:900**
* The GCS deletion has the same stale-registry gap: `storage.gcs` is still listed in `schema/schema.go`, `schema/module_schema.go`, `cmd/wfctl/type_registry.go`, `module/api_workflow_ui.go`, manifest analyzers, and multiple docs. Removing only the storage plugin entry and DOCUMENTATION leaves core validators/UI advertising a built-in module type whose factory was deleted unless the GCP plugin is loaded.
  • Delete: module/iac_state_gcs.go, module/storage_gcs.go, module/platform_kubernetes_gke.go (+ their _test.go if present)
  • Modify: module/iac_module.go (remove case "gcs":)
  • Modify: plugins/storage/plugin.go (drop the "storage.gcs" factory :109, capability :39, schema :352)
  • Modify: DOCUMENTATION.md (remove storage.gcs)
**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:932**
* The planned `go list -deps` gate is broader than the files being removed and is likely unsatisfiable as written. Core still imports `golang.org/x/oauth2/google` in `provider/gcp/plugin.go` and `module/auth_oauth2.go`; that package brings Google metadata/cloud packages into the dependency graph in current `go.mod` (`cloud.google.com/go/compute/metadata` is already present). Deleting `storage.gcs`/`gke` may remove direct `cloud.google.com/go/storage` and `google.golang.org/api` importers, but it does not guarantee zero `cloud.google.com/go` packages in `go list -deps ./...`.

Context: After Task 27, cloud.google.com/go/storage + google.golang.org/api/* have zero importers in core's build graph — go mod tidy drops them entirely. The permanent CI gate is asymmetric (design Goals): (a) go list -deps ./... asserts zero Azure/azure-sdk-for-go AND zero cloud.google.com/go / google.golang.org/api packages in core's build graph; (b) audit-cloud-symbols.sh --check asserts zero aws-sdk-go-v2 imports under module/ — AWS gone from module/, but aws-sdk-go-v2 remains a go.mod entry for the out-of-scope provider/aws/ etc. surface. godo remains — not asserted.

Change class: Build pipeline + go.mod dependency change → runtime-launch-validation required. Rollback: revert PR 10; deleted files recoverable from git, the in-core gcs/storage.gcs/gke paths restore, go.mod re-adds the GCP SDKs on go mod tidy. Note: a running deployment that already cut over to plugin-served gcs must, on rollback, either also roll back the gcp plugin to a pre-gcs version OR keep the gcs-serving plugin installed (the reverted engine routes backend: gcs to the in-core case, so the in-core path must be the one in use) — coordinate engine + plugin versions.

Step 1: Tidy + markerGOWORK=off go mod tidy && touch .phase-c-complete. Confirm go.mod no longer lists cloud.google.com/go/storage or google.golang.org/api.

Step 2: Add the permanent invariants — in scripts/audit-cloud-symbols.sh, add a --check block: GOWORK=off go list -deps ./... 2>/dev/null | grep -E 'Azure/azure-sdk-for-go|cloud\.google\.com/go|google\.golang\.org/api' must be empty (FAIL if any line). Add a module/-scoped aws-sdk-go-v2 zero-import assertion (the existing whole-repo map already separates module/ from elsewhere — assert the module/ count is 0). In .github/workflows/ci.yml cloud-sdk-audit job, confirm audit-cloud-symbols.sh --check runs (wired in Phase 0) and the new graph check executes there.

**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:260**
* Porting `iac_state_spaces.go` for the AWS `s3` backend needs to remove more than the `DO_SPACES_*` credential fallbacks. The current constructor also synthesizes a DigitalOcean endpoint (`https://<region>.digitaloceanspaces.com`) whenever `endpoint` is empty and forces `BaseEndpoint`/path-style; if that logic is copied into `NewS3IaCStateStore`, a normal AWS S3 backend with only `region`/`bucket` will target DigitalOcean instead of AWS. The plan should explicitly require AWS behavior: no synthesized DO endpoint, no custom `BaseEndpoint` unless configured, and path-style only when using a custom S3-compatible endpoint.

Step 3: Port the store — copy module/iac_state_spaces.gointernal/statebackend/s3.go. Edits:

  • package modulepackage statebackend.
  • Rename SpacesIaCStateStoreS3IaCStateStore, NewSpacesIaCStateStoreNewS3IaCStateStore, NewSpacesIaCStateStoreWithClientNewS3IaCStateStoreWithClient, SpacesS3ClientS3Client.
  • Strip the DO_SPACES_ACCESS_KEY / DO_SPACES_SECRET_KEY env-var fallbacks from the constructor — they are DigitalOcean-specific and would silently authenticate an aws s3 backend against DO credentials in a mixed deployment. Replace with the AWS-conventional behavior: if accessKey/secretKey are empty, do not inject static creds — let aws-sdk-go-v2's default credential chain (env AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, instance role, etc.) apply via config.LoadDefaultConfig. (The DO plugin's copy in Task 11 keeps the DO_SPACES_* fallbacks — that is correct there.)
  • Define a local IaCState struct + IaCStateStore interface in this package. The struct fields must match the proto IaCState message (iac.proto:636) exactly — the proto is the canonical wire shape; if the proto and core's Go struct ever diverge, the proto wins. The 6 method signatures are the ctx-ful module.IaCStateStore shape. The plugin does NOT import workflow/module.
**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:204**
* The test/input shape here uses `accountURL`, but the existing `azure_blob` YAML keys are snake_case (`account_url`, `account_name`, `account_key`, `container`, `prefix`) as documented in the Phase A migration guide and tests. If the retrofit decodes camelCase keys, existing `azure_blob` configs will still fail after `Configure`; the plan should require compatibility with the current snake_case keys and test those exact names.

Step 1: Write the failing testinternal/statebackend_server_test.go: call azureIaCServer.Configure(ctx, &pb.ConfigureRequest{BackendName: "azure_blob", ConfigJson: <json of {accountURL,container,...}>}); assert resolveStore() subsequently returns a non-nil store (not FailedPrecondition).

Step 2: Verify it failscd /Users/jon/workspace/workflow-plugin-azure && go test ./internal/ -run Configure -v → FAIL (Configure is the Unimplemented default → returns Unimplemented status).

Step 3: ImplementazureIaCServer.Configure: json.Unmarshal(req.ConfigJson, &cfg), validate req.BackendName == "azure_blob", construct statebackend.NewAzureBlobIaCStateStore(...) from the decoded config fields (account URL / container / credential — the same fields the deleted in-core iac_state_azure.go switch case read), call s.stateBackend.setStateStore(store), return &pb.ConfigureResponse{}. Pin workflow to PR 1's merge commit.

**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:652**
* This uses `credentialType`, but `cloud.account` reads the credential kind from `credentials.type` (defaulting to `static`) rather than a top-level `credentialType` key. The migration guide should name the actual YAML shape (`provider: aws` with `credentials.type: profile|role_arn`) so users can identify the affected configs.
  • provider: aws with credentialType: profile or role_arn — credential resolution is now performed in-plugin. Core and workflow-plugin-aws must be upgraded together: a new core against a pre-extraction aws plugin will emit a credential_source marker the old plugin ignores, producing empty credentials (core logs a warning). State this prominently.
**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:260**
* This should also require the local `IaCState` JSON tags to match the existing core `module.IaCState` at-rest format (`resource_id`, `resource_type`, etc.). The plan explicitly keeps the at-rest JSON format out of scope, so defining the struct only from the proto shape (or omitting the core JSON tags) would make the plugin unable to read state objects previously written by the in-core S3-compatible store.
  • Define a local IaCState struct + IaCStateStore interface in this package. The struct fields must match the proto IaCState message (iac.proto:636) exactly — the proto is the canonical wire shape; if the proto and core's Go struct ever diverge, the proto wins. The 6 method signatures are the ctx-ful module.IaCStateStore shape. The plugin does NOT import workflow/module.
**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:432**
* Because the in-core `spaces` backend has existing JSON state objects, the copied plugin struct must preserve the core `module.IaCState` JSON tags in addition to matching the proto fields. Otherwise the DO plugin can compile and pass wire-shape tests while failing to read state written before the extraction, which violates the plan's out-of-scope at-rest-format change.

Step 3: Implement — copy module/iac_state_spaces.gointernal/statebackend/spaces.go (package statebackend; keep the Spaces* names and the DO_SPACES_* env fallbacks; define a local IaCState struct matching the proto IaCState message + the IaCStateStore interface; do NOT import workflow/module). go mod tidy.

**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:705**
* The GCS port also needs an explicit at-rest compatibility requirement: the local `IaCState` struct should preserve the current `module.IaCState` JSON tags, not just match the proto fields. Existing GCS state files are JSON written with the core tags, and the plan says the at-rest format change is out of scope.

Step 3: Implement — copy module/iac_state_gcs.gointernal/statebackend/gcs.go (package statebackend, local IaCState + IaCStateStore, keep the GCSObjectClient indirection + gcsRealClient, do NOT import workflow/module). go mod tidy.

**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:991**
* This note says the AWS and DO copies differ only in env-var fallbacks, but the AWS `s3` copy also must not inherit the DO-specific endpoint synthesis/path-style behavior from `SpacesIaCStateStore`. Keeping the note as-is encourages future changes to preserve an AWS bug (routing `s3` to `*.digitaloceanspaces.com`) or to reintroduce it when syncing the two copies.
  • S3IaCStateStore (aws) and SpacesIaCStateStore (DO) are deliberately diverging copies of the same upstream module/iac_state_spaces.go — they differ only in env-var fallback behavior (aws strips the DO_SPACES_* fallbacks; DO keeps them). Any future fix to the shared S3-locking protocol must be applied to both copies. The design's "Alternative 3 — shared s3compat module" is the recommended eventual cleanup; out of scope here.
**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:847**
* The core adapter instructions do not specify how the plugin receives the `cloud.account` credentials that `gkeBackend.containerService` currently reads from `k.provider.GetCredentials()`. Task 22 assumes credentials arrive as serialized `CloudCredentials`, but the Task 25 ResourceDriver/adapter mapping only mentions plan/result/state JSON conversion. Without explicitly fetching `k.provider.GetCredentials(ctx)` and embedding the credentials/project/region in the ResourceDriver request config, the plugin-served GKE path cannot authenticate.

Context: The host-side adapter that lets platform.kubernetes's in-core kubernetesBackend interface dispatch the gke provider to a plugin gRPC client. Shape per ADR 0037:

  • Option 1 (ResourceDriver fold): grpcKubernetesBackend implements kubernetesBackend, delegating planDiff, applyCreate/Update, statusRead, destroyDelete on a pb.ResourceDriverClient. JSON-bytes converters (PlatformPlan/PlatformResult/KubernetesClusterState ↔ the ResourceDriver messages), mirroring iac_state_grpc_client.go. No proto change.
  • Option 2: the RemoteModule adapter for a plugin-native kubernetesBackend.
  • Option 3: add the minimal PlatformBackend service to iac.proto (regenerate — additive, preserves the no-structpb invariant) + the grpcKubernetesBackend adapter over it. If Option 3, this task's proto regen must merge before PR 8's Task 22.

Step 1: Read ADR 0037. Pin the contract.

Step 2: Write the failing testplatform_kubernetes_grpc_test.go: a fake client of the chosen contract; assert grpcKubernetesBackend.{plan,apply,status,destroy} round-trip (incl. KubernetesClusterState surviving the JSON-bytes round-trip).

**docs/plans/2026-05-14-cloud-sdk-extraction-bcd.md:675**
* The interface-audit spike needs to check more than lifecycle/status shape: GKE `status` and `destroy` currently need project/location and GCP credentials from `PlatformKubernetes`/`cloud.account`, but `ResourceReadRequest` and `ResourceDeleteRequest` carry only `ResourceRef` (no `config_json` or credentials). If ADR 0037 chooses the ResourceDriver fold without solving that, the plugin will not have enough data to implement `status`/`destroy` unless it relies on some separate provider initialization path.

Step 1: Audit the in-core interface — read module/platform_kubernetes.go, module/platform_kubernetes_gke.go (the gkeBackend 4 methods + containerService), module/platform_provider.go (PlatformPlan/PlatformResult), module/platform_kubernetes.go:11 (KubernetesClusterState), and plugin/external/proto/iac.proto (ResourceDriver + its messages). Map each kubernetesBackend method onto a ResourceDriver RPC; note any shape mismatch (status returns the rich typed KubernetesClusterState — does ResourceReadResponse.outputs_json carry it cleanly? — and confirm gke has no continuous-reconciliation behavior: the 4 methods are one-shot lifecycle).

</details>

**Step 5: Commit**

```bash
git add plugin/external/proto/ module/iac_state_grpc_client.go module/iac_state_grpc_client_test.go
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.

Valid systemic nit — acknowledged. The plan is scope-locked (the git add lines sit inside task descriptions, but the lock hook blocks all plan edits and this isn't a scope change, so the heavyweight unlock path isn't warranted). Mitigated instead at the execution layer: every implementer has been briefed that the git add lines are illustrative — always git status and stage every created/modified file (test fakes for regenerated interfaces, generated .pb.go, go.sum). Empirically confirmed on Task 1, which staged all 7 files (proto + regen + client + 2 fakes + test + compile-guard) correctly.

@github-actions
Copy link
Copy Markdown

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:276: parsing iteration count: invalid syntax
baseline-bench.txt:336820: parsing iteration count: invalid syntax
baseline-bench.txt:684464: parsing iteration count: invalid syntax
baseline-bench.txt:978417: parsing iteration count: invalid syntax
baseline-bench.txt:1301086: parsing iteration count: invalid syntax
baseline-bench.txt:1601591: parsing iteration count: invalid syntax
benchmark-results.txt:276: parsing iteration count: invalid syntax
benchmark-results.txt:334525: parsing iteration count: invalid syntax
benchmark-results.txt:657170: parsing iteration count: invalid syntax
benchmark-results.txt:1206270: parsing iteration count: invalid syntax
benchmark-results.txt:1538470: parsing iteration count: invalid syntax
benchmark-results.txt:2024054: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 7763 64-Core Processor                
                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │       sec/op       │    sec/op     vs base               │
InterpreterCreation-4              3.289m ± 205%   7.400m ± 57%        ~ (p=0.132 n=6)
ComponentLoad-4                    3.503m ±   9%   3.787m ±  0%        ~ (p=0.065 n=6)
ComponentExecute-4                 1.905µ ±   0%   1.989µ ±  0%   +4.41% (p=0.002 n=6)
PoolContention/workers-1-4         1.060µ ±   1%   1.116µ ±  1%   +5.28% (p=0.002 n=6)
PoolContention/workers-2-4         1.064µ ±   2%   1.125µ ±  2%   +5.74% (p=0.002 n=6)
PoolContention/workers-4-4         1.068µ ±   0%   1.115µ ±  1%   +4.35% (p=0.002 n=6)
PoolContention/workers-8-4         1.070µ ±   1%   1.141µ ±  3%   +6.68% (p=0.002 n=6)
PoolContention/workers-16-4        1.070µ ±   0%   1.115µ ±  5%   +4.16% (p=0.002 n=6)
ComponentLifecycle-4               3.517m ±   0%   3.900m ±  2%  +10.89% (p=0.002 n=6)
SourceValidation-4                 2.262µ ±   1%   2.381µ ±  1%   +5.26% (p=0.002 n=6)
RegistryConcurrent-4               759.6n ±   5%   888.1n ±  2%  +16.92% (p=0.002 n=6)
LoaderLoadFromString-4             3.531m ±   1%   3.777m ±  2%   +6.98% (p=0.002 n=6)
geomean                            17.15µ          19.54µ        +13.94%

                            │ baseline-bench.txt │        benchmark-results.txt         │
                            │        B/op        │     B/op      vs base                │
InterpreterCreation-4               2.027Mi ± 0%   2.027Mi ± 0%       ~ (p=0.818 n=6)
ComponentLoad-4                     2.180Mi ± 0%   2.180Mi ± 0%       ~ (p=0.909 n=6)
ComponentExecute-4                  1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4          1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4         1.203Ki ± 0%   1.203Ki ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                2.183Mi ± 0%   2.183Mi ± 0%       ~ (p=0.288 n=6)
SourceValidation-4                  1.984Ki ± 0%   1.984Ki ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                1.133Ki ± 0%   1.133Ki ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4              2.182Mi ± 0%   2.182Mi ± 0%       ~ (p=0.974 n=6)
geomean                             15.25Ki        15.25Ki       +0.00%
¹ all samples are equal

                            │ baseline-bench.txt │        benchmark-results.txt        │
                            │     allocs/op      │  allocs/op   vs base                │
InterpreterCreation-4                15.68k ± 0%   15.68k ± 0%       ~ (p=1.000 n=6)
ComponentLoad-4                      18.02k ± 0%   18.02k ± 0%       ~ (p=1.000 n=6)
ComponentExecute-4                    25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-1-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-2-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-4-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-8-4            25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
PoolContention/workers-16-4           25.00 ± 0%    25.00 ± 0%       ~ (p=1.000 n=6) ¹
ComponentLifecycle-4                 18.07k ± 0%   18.07k ± 0%       ~ (p=1.000 n=6) ¹
SourceValidation-4                    32.00 ± 0%    32.00 ± 0%       ~ (p=1.000 n=6) ¹
RegistryConcurrent-4                  2.000 ± 0%    2.000 ± 0%       ~ (p=1.000 n=6) ¹
LoaderLoadFromString-4               18.06k ± 0%   18.06k ± 0%       ~ (p=1.000 n=6) ¹
geomean                               183.3         183.3       +0.00%
¹ all samples are equal

pkg: github.com/GoCodeAlone/workflow/middleware
                                  │ baseline-bench.txt │       benchmark-results.txt       │
                                  │       sec/op       │   sec/op     vs base              │
CircuitBreakerDetection-4                  288.7n ± 9%   284.9n ± 4%       ~ (p=0.589 n=6)
CircuitBreakerExecution_Success-4          21.50n ± 1%   21.56n ± 1%  +0.30% (p=0.037 n=6)
CircuitBreakerExecution_Failure-4          66.20n ± 0%   66.36n ± 0%       ~ (p=0.058 n=6)
geomean                                    74.34n        74.15n       -0.25%

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │        B/op        │    B/op     vs base                │
CircuitBreakerDetection-4                 144.0 ± 0%     144.0 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │       benchmark-results.txt        │
                                  │     allocs/op      │ allocs/op   vs base                │
CircuitBreakerDetection-4                 1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Success-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
CircuitBreakerExecution_Failure-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                              ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
                                 │ baseline-bench.txt │        benchmark-results.txt        │
                                 │       sec/op       │    sec/op     vs base               │
IaCStateBackend_InProcess-4              295.6n ±  2%   303.6n ±  4%   +2.72% (p=0.013 n=6)
IaCStateBackend_GRPC-4                  10.577m ±  7%   9.561m ± 19%        ~ (p=0.310 n=6)
JQTransform_Simple-4                     645.8n ± 32%   688.0n ± 40%        ~ (p=0.240 n=6)
JQTransform_ObjectConstruction-4         1.415µ ±  1%   1.596µ ±  1%  +12.83% (p=0.002 n=6)
JQTransform_ArraySelect-4                3.306µ ±  1%   3.732µ ±  1%  +12.89% (p=0.002 n=6)
JQTransform_Complex-4                    37.77µ ±  2%   40.45µ ±  0%   +7.11% (p=0.002 n=6)
JQTransform_Throughput-4                 1.758µ ±  1%   1.911µ ±  3%   +8.67% (p=0.002 n=6)
SSEPublishDelivery-4                     67.97n ±  1%   67.36n ±  1%        ~ (p=0.093 n=6)
geomean                                  3.790µ         3.972µ         +4.78%

                                 │ baseline-bench.txt │         benchmark-results.txt         │
                                 │        B/op        │     B/op       vs base                │
IaCStateBackend_InProcess-4              416.0 ± 0%       416.0 ±  0%       ~ (p=1.000 n=6) ¹
IaCStateBackend_GRPC-4                 6.099Mi ± 5%     5.848Mi ± 11%       ~ (p=0.310 n=6)
JQTransform_Simple-4                   1.273Ki ± 0%     1.273Ki ±  0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4       1.773Ki ± 0%     1.773Ki ±  0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4              2.625Ki ± 0%     2.625Ki ±  0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                  16.22Ki ± 0%     16.22Ki ±  0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4               1.984Ki ± 0%     1.984Ki ±  0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%       0.000 ±  0%       ~ (p=1.000 n=6) ¹
geomean                                             ²                  -0.52%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │        benchmark-results.txt        │
                                 │     allocs/op      │  allocs/op   vs base                │
IaCStateBackend_InProcess-4              2.000 ± 0%      2.000 ± 0%       ~ (p=1.000 n=6) ¹
IaCStateBackend_GRPC-4                  6.834k ± 0%     6.843k ± 0%       ~ (p=0.102 n=6)
JQTransform_Simple-4                     10.00 ± 0%      10.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ObjectConstruction-4         15.00 ± 0%      15.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_ArraySelect-4                30.00 ± 0%      30.00 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Complex-4                    324.0 ± 0%      324.0 ± 0%       ~ (p=1.000 n=6) ¹
JQTransform_Throughput-4                 17.00 ± 0%      17.00 ± 0%       ~ (p=1.000 n=6) ¹
SSEPublishDelivery-4                     0.000 ± 0%      0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                             ²                +0.02%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │       sec/op       │    sec/op     vs base              │
SchemaValidation_Simple-4                   1.112µ ±  4%   1.093µ ±  4%       ~ (p=0.065 n=6)
SchemaValidation_AllFields-4                1.673µ ± 19%   1.671µ ± 21%       ~ (p=0.788 n=6)
SchemaValidation_FormatValidation-4         1.588µ ±  2%   1.591µ ±  1%       ~ (p=1.000 n=6)
SchemaValidation_ManySchemas-4              1.816µ ±  1%   1.830µ ±  3%       ~ (p=0.225 n=6)
geomean                                     1.522µ         1.518µ        -0.25%

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │        B/op        │    B/op     vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │       benchmark-results.txt        │
                                    │     allocs/op      │ allocs/op   vs base                │
SchemaValidation_Simple-4                   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_AllFields-4                0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_FormatValidation-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
SchemaValidation_ManySchemas-4              0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                ²               +0.00%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │       sec/op       │    sec/op     vs base               │
EventStoreAppend_InMemory-4                1.279µ ± 23%   1.171µ ± 18%        ~ (p=0.394 n=6)
EventStoreAppend_SQLite-4                  1.213m ±  4%   1.207m ± 10%        ~ (p=0.699 n=6)
GetTimeline_InMemory/events-10-4           13.47µ ±  2%   13.73µ ±  3%        ~ (p=0.223 n=6)
GetTimeline_InMemory/events-50-4           74.29µ ±  2%   76.20µ ±  6%   +2.58% (p=0.026 n=6)
GetTimeline_InMemory/events-100-4          120.9µ ± 28%   152.8µ ±  4%  +26.46% (p=0.041 n=6)
GetTimeline_InMemory/events-500-4          617.8µ ±  1%   781.6µ ±  2%  +26.51% (p=0.002 n=6)
GetTimeline_InMemory/events-1000-4         1.268m ±  1%   1.348m ± 19%   +6.30% (p=0.015 n=6)
GetTimeline_SQLite/events-10-4             104.0µ ±  1%   106.2µ ±  0%   +2.14% (p=0.002 n=6)
GetTimeline_SQLite/events-50-4             241.5µ ±  1%   246.3µ ±  2%   +2.00% (p=0.004 n=6)
GetTimeline_SQLite/events-100-4            409.6µ ±  0%   416.5µ ±  1%   +1.70% (p=0.002 n=6)
GetTimeline_SQLite/events-500-4            1.744m ±  2%   1.780m ±  1%   +2.04% (p=0.002 n=6)
GetTimeline_SQLite/events-1000-4           3.416m ±  1%   3.455m ±  1%        ~ (p=0.065 n=6)
geomean                                    213.6µ         224.0µ         +4.87%

                                   │ baseline-bench.txt │        benchmark-results.txt         │
                                   │        B/op        │     B/op      vs base                │
EventStoreAppend_InMemory-4                  819.0 ± 9%     799.0 ± 9%       ~ (p=0.699 n=6)
EventStoreAppend_SQLite-4                  1.984Ki ± 2%   1.982Ki ± 2%       ~ (p=0.721 n=6)
GetTimeline_InMemory/events-10-4           7.953Ki ± 0%   7.953Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4           46.62Ki ± 0%   46.62Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4          94.48Ki ± 0%   94.48Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4          472.8Ki ± 0%   472.8Ki ± 0%       ~ (p=1.000 n=6)
GetTimeline_InMemory/events-1000-4         944.3Ki ± 0%   944.3Ki ± 0%       ~ (p=1.000 n=6)
GetTimeline_SQLite/events-10-4             16.74Ki ± 0%   16.74Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4             87.14Ki ± 0%   87.14Ki ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4            175.4Ki ± 0%   175.4Ki ± 0%       ~ (p=1.000 n=6)
GetTimeline_SQLite/events-500-4            846.1Ki ± 0%   846.1Ki ± 0%  +0.00% (p=0.032 n=6)
GetTimeline_SQLite/events-1000-4           1.639Mi ± 0%   1.639Mi ± 0%       ~ (p=1.000 n=6)
geomean                                    67.55Ki        67.40Ki       -0.22%
¹ all samples are equal

                                   │ baseline-bench.txt │        benchmark-results.txt        │
                                   │     allocs/op      │  allocs/op   vs base                │
EventStoreAppend_InMemory-4                  7.000 ± 0%    7.000 ± 0%       ~ (p=1.000 n=6) ¹
EventStoreAppend_SQLite-4                    53.00 ± 0%    53.00 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-10-4             125.0 ± 0%    125.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-50-4             653.0 ± 0%    653.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-100-4           1.306k ± 0%   1.306k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-500-4           6.514k ± 0%   6.514k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_InMemory/events-1000-4          13.02k ± 0%   13.02k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-10-4               382.0 ± 0%    382.0 ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-50-4              1.852k ± 0%   1.852k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-100-4             3.681k ± 0%   3.681k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-500-4             18.54k ± 0%   18.54k ± 0%       ~ (p=1.000 n=6) ¹
GetTimeline_SQLite/events-1000-4            37.29k ± 0%   37.29k ± 0%       ~ (p=1.000 n=6) ¹
geomean                                     1.162k        1.162k       +0.00%
¹ all samples are equal

Benchmarks run with go test -bench=. -benchmem -count=6.
Regressions ≥ 20% are flagged. Results compared via benchstat.

@intel352 intel352 merged commit 976eb0e into main May 14, 2026
26 checks passed
@intel352 intel352 deleted the feat/cloud-sdk-extraction-bcd branch May 14, 2026 22:19
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