Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .phase-b-complete
Empty file.
2 changes: 0 additions & 2 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ flowchart TD
| `step.dlq_replay` | Replays messages from the dead-letter queue | pipelinesteps |
| `step.retry_with_backoff` | Retries a sub-pipeline with exponential backoff | pipelinesteps |
| `step.resilient_circuit_breaker` | Wraps a sub-pipeline with a circuit breaker | pipelinesteps |
| `step.s3_upload` | Uploads a file or data to an S3-compatible bucket | pipelinesteps |
| `step.auth_validate` | Validates an authentication token and populates claims | pipelinesteps |
| `step.token_revoke` | Revokes an auth token | pipelinesteps |
| `step.field_reencrypt` | Re-encrypts a field with a new key | pipelinesteps |
Expand Down Expand Up @@ -537,7 +536,6 @@ See [v0.53.0 migration guide](docs/migrations/v0.53.0-aws-iac-removal.md).
### Storage
| Type | Description | Plugin |
|------|-------------|--------|
| `storage.s3` | Amazon S3 storage | storage |
| `storage.gcs` | Google Cloud Storage | storage |
| `storage.local` | Local filesystem storage | storage |
| `storage.sqlite` | SQLite storage | storage |
Expand Down
73 changes: 9 additions & 64 deletions cmd/wfctl/infra_state_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,16 @@ func resolveStateStore(cfgFile, envName string) (infraStateStore, error) {
}
return &fsWfctlStateStore{dir: dir}, nil

case "spaces":
return resolveSpacesStateStore(cfg)

case "postgres":
return resolvePostgresStateStore(cfg)

Comment on lines 82 to 87
case "spaces":
return nil, fmt.Errorf("iac.state backend %q is now plugin-served by workflow-plugin-digitalocean v1.1.0; "+
"install and load the plugin to use the Spaces backend (wfctl direct-path commands no longer support in-tree spaces)", backend)

case "s3":
return nil, fmt.Errorf("s3 state store backend not yet supported by wfctl direct-path commands; " +
"create the bucket manually and reference it in iac.state.bucket. " +
"Contribute a resolveS3StateStore helper to unblock this")
return nil, fmt.Errorf("iac.state backend %q is now plugin-served by workflow-plugin-aws v1.1.0; "+
"install and load the plugin to use the S3 backend (wfctl direct-path commands no longer support in-tree s3)", backend)

case "gcs":
return nil, fmt.Errorf("gcs state store backend not yet supported by wfctl direct-path commands; " +
Expand All @@ -117,10 +117,9 @@ type fsWfctlStateStore struct {
dir string
}

// iacStateRecord mirrors the JSON schema used by the filesystem and Spaces
// backends. The field names must stay stable to remain compatible with the
// existing loadFSState reader and the importFromTFState / importFromPulumi
// writers.
// iacStateRecord mirrors the JSON schema used by the filesystem backend. The
// field names must stay stable to remain compatible with the existing
// loadFSState reader and the importFromTFState / importFromPulumi writers.
type iacStateRecord struct {
ResourceID string `json:"resource_id"`
ResourceType string `json:"resource_type"`
Expand Down Expand Up @@ -226,60 +225,6 @@ func (s *fsWfctlStateStore) SaveMetadata(_ context.Context, meta interfaces.Gene
return nil
}

// ── Spaces backend ─────────────────────────────────────────────────────────────

// resolveSpacesStateStore builds a Spaces-backed state store from the expanded
// iac.state module config. Credentials fall back to DO_SPACES_ACCESS_KEY /
// DO_SPACES_SECRET_KEY environment variables via module.NewSpacesIaCStateStore.
func resolveSpacesStateStore(cfg map[string]any) (infraStateStore, error) {
bucket, _ := cfg["bucket"].(string)
region, _ := cfg["region"].(string)
prefix, _ := cfg["prefix"].(string)

accessKey, _ := cfg["accessKey"].(string)
if accessKey == "" {
accessKey, _ = cfg["access_key"].(string)
}
secretKey, _ := cfg["secretKey"].(string)
if secretKey == "" {
secretKey, _ = cfg["secret_key"].(string)
}
if bucket == "" {
return nil, fmt.Errorf("iac.state backend=spaces requires 'bucket' in config")
}
inner, err := module.NewSpacesIaCStateStore(region, bucket, prefix, accessKey, secretKey, "")
if err != nil {
return nil, fmt.Errorf("init spaces state store: %w", err)
}
return &spacesWfctlStateStore{inner: inner}, nil
}

// spacesWfctlStateStore wraps module.SpacesIaCStateStore to implement
// infraStateStore, bridging module.IaCState ↔ interfaces.ResourceState.
type spacesWfctlStateStore struct {
inner *module.SpacesIaCStateStore
}

func (s *spacesWfctlStateStore) ListResources(ctx context.Context) ([]interfaces.ResourceState, error) {
records, err := s.inner.ListStates(ctx, nil)
if err != nil {
return nil, fmt.Errorf("list spaces state: %w", err)
}
states := make([]interfaces.ResourceState, 0, len(records))
for _, r := range records {
states = append(states, iacStateToResourceState(r))
}
return states, nil
}

func (s *spacesWfctlStateStore) SaveResource(ctx context.Context, state interfaces.ResourceState) error {
return s.inner.SaveState(ctx, resourceStateToIaCState(state))
}

func (s *spacesWfctlStateStore) DeleteResource(ctx context.Context, name string) error {
return s.inner.DeleteState(ctx, name)
}

// ── Postgres backend ───────────────────────────────────────────────────────────

// resolvePostgresStateStore builds a Postgres-backed state store from the
Expand Down
6 changes: 3 additions & 3 deletions cmd/wfctl/state_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import (
// the real-world fidelity check.
//
// - The test reads the fixture via the v1.0.0 wfctl iacStateRecord
// decoder (the same path loadFSState / spacesWfctlStateStore.
// ListResources use), then converts it via iacRecordToResourceState
// and asserts every load-bearing field survived.
// decoder (the same path loadFSState / fsWfctlStateStore.ListResources
// use), then converts it via iacRecordToResourceState and asserts every
// load-bearing field survived.
//
// If this test FAILS in CI: PR 5 cascade-block surfaces. Plan response
// (per Task 31 §If FAIL):
Expand Down
135 changes: 135 additions & 0 deletions docs/migrations/2026-05-15-plugin-modules-on-iac.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# 2026-05-15 — Plugin-modules-on-IaC: Phase B clean break

This migration covers **Phase B** of the
[plugin-modules-on-IaC plan](../plans/2026-05-15-plugin-modules-on-iac.md):
workflow-core sheds the remaining in-core AWS/DO storage + state surfaces and
the SDK-bearing AWS credential resolvers. Each surface is now plugin-native.

The companion **Phase C** migration (GCP) follows in a separate PR; this doc is
amended in-place when that ships.

## Engine floor

Phase B requires **workflow `>= v0.53.0`** in any deployment that uses the
affected backends. The `>= v0.53.0` engine has the typed `IaCStateBackend`
gRPC contract (Phase A, decisions/0036), the `Configure` RPC that delivers the
`iac.state` module YAML to the plugin, and the plugin-backend registry that
`IaCModule.Init` consults in its `default:`-arm.

## What changed

| Surface | Was | Now |
|---|---|---|
| `iac.state` `backend: spaces` | in-core `module.SpacesIaCStateStore` | plugin-served by [`workflow-plugin-digitalocean`](https://github.com/GoCodeAlone/workflow-plugin-digitalocean) `>= v1.1.0` |
| `iac.state` `backend: s3` | (already moved in v0.53.0; no in-core impl since then) | plugin-served by [`workflow-plugin-aws`](https://github.com/GoCodeAlone/workflow-plugin-aws) `>= v1.1.0` |
| `storage.s3` module | in-core `module.S3Storage` (registered by `plugins/storage`) | plugin-native in `workflow-plugin-aws >= v1.1.0` |
| `step.s3_upload` pipeline step | in-core `module.S3UploadStep` (registered by `plugins/pipelinesteps`) | plugin-native in `workflow-plugin-aws >= v1.1.0` |
| `cloud.account` `provider: aws` + `credentials.type: profile` or `role_arn` | SDK-bearing resolver loaded the profile / called `sts:AssumeRole` in-core | core records a `credential_source` marker only; the aws plugin performs SDK resolution via `awscreds.BuildAWSConfig` (decisions/0036 + 0038) |

The YAML field names and `backend:` values are **unchanged**. The break is
strictly about *which binary* serves them.

## Why

Workflow core owns IaC orchestration interfaces, not provider SDKs. Provider
SDKs ride with the provider plugin, where Dependabot bumps and SDK CVE patches
belong. This continues the pattern set by the `godo` removal
([v0.52.0](v0.52.0-godo-removal.md)) and the AWS IaC core removal
([v0.53.0](v0.53.0-aws-iac-removal.md)).

## Breaking change — action required

### `iac.state backend: spaces`

Load `workflow-plugin-digitalocean >= v1.1.0`. The YAML `backend: spaces`
value is unchanged; all existing config keys (`region`, `bucket`, `prefix`,
`accessKey`, `secretKey`, `endpoint`) keep their semantics. The
`DO_SPACES_ACCESS_KEY` / `DO_SPACES_SECRET_KEY` environment fallbacks are
preserved by the plugin port.

Without the plugin, `IaCModule.Init` fails fast:

```
iac.state "<name>": backend "spaces" is not built into workflow core
(in-core backends: 'memory', 'filesystem', 'gcs', 'postgres').
If "spaces" is a plugin-provided backend (e.g. 'azure_blob' via
workflow-plugin-azure, 'spaces' via workflow-plugin-digitalocean,
's3' via workflow-plugin-aws), install and load that plugin
```

### `iac.state backend: s3`

Load `workflow-plugin-aws >= v1.1.0`. Same shape as the `spaces` migration —
YAML unchanged, error message above identifies the missing plugin.

### `storage.s3` module + `step.s3_upload` pipeline step

Both move into `workflow-plugin-aws >= v1.1.0`. Credentials can be inline in
the module/step config, or referenced via `credentials_ref:` pointing at an
`aws.credentials` module loaded by the plugin. With no plugin loaded the
module type / step type is unknown at engine boot — load the plugin in the
deployment's plugin manifest.

### `cloud.account provider: aws` with `credentials.type: profile` or `role_arn`

The credential config sits under the nested `credentials:` map on the
`cloud.account` module (the key is `credentials.type`, not a flat
`credentialType:`). The affected shape:

```yaml
modules:
- name: aws-account
type: cloud.account
config:
provider: aws
region: us-east-1
credentials:
type: profile # or role_arn
profile: team-prod # for type=profile
# roleArn / externalId / sessionName for type=role_arn
```

Core no longer resolves the profile or calls `sts:AssumeRole`. Instead the
resolver records `Extra["credential_source"] = "profile"` or `"role_arn"`
(plus `Extra["profile"]` / `m.creds.RoleARN` + `Extra["external_id"]`) and
logs a `workflow: aws credential_source=…` warning.

The aws plugin's `awscreds.BuildAWSConfig` consumes the marker at the point of
need and performs the SDK-bearing resolution in-plugin. This is a
**co-deploy** requirement: core `>= v0.53.0` AND `workflow-plugin-aws
>= v1.1.0` must be deployed together. Mixing an old plugin against new core
results in a `credential_source` marker the plugin can't interpret — the core
warning is what tells operators which side to upgrade.

`credentials.type: static` and `credentials.type: env` are unaffected — those
paths have always been SDK-free and resolve in-core.

## Rollback

Phase B's clean-breaks roll back only as a **matched pair** with the plugin
releases that serve them — reverting PR `feat/phase-b-core-deletion`
restores the in-core paths, but the plugin v1.1.0 tags are immutable. A
patch-level defect in either plugin port is resolved with a `v1.1.1`
release, not by re-introducing the in-core implementation.

The `cloud_account_aws.go` deletion (164 lines of dead code that #653 had
already orphaned) is not part of the matched-pair rollback — it had zero
non-test consumers.

## Verification

Once Phase B is merged:

- `go mod tidy` against the merged tree should make no net change to AWS SDK
service modules — `aws-sdk-go-v2` stays in `go.mod` because `provider/aws/`,
`plugin/rbac/aws.go`, `iam/aws.go`, and `artifact/s3.go` still import it.
- The `.phase-b-complete` marker arms
`scripts/audit-cloud-symbols.sh --check`'s zero-`aws-sdk-go-v2` invariant on
`module/cloud_account_aws_creds.go`. Running the audit script post-merge
must report `audit-cloud-symbols: OK`.

## Related design + plans

- Plan: [docs/plans/2026-05-15-plugin-modules-on-iac.md](../plans/2026-05-15-plugin-modules-on-iac.md)
- Decisions: 0034 (autonomous plugin releases), 0035 (assumed-seam grep), 0036 (Configure RPC), 0038 (credential_source marker)
- Predecessors: [v0.52.0 godo removal](v0.52.0-godo-removal.md), [v0.53.0 AWS IaC removal](v0.53.0-aws-iac-removal.md), [2026-05-14 azure plugin extraction](2026-05-14-cloud-sdk-extraction.md)
98 changes: 0 additions & 98 deletions module/cloud_account_aws.go

This file was deleted.

Loading
Loading