From bd119a6bb72d015500c18a8056ed551eb9eb7ab9 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 10 May 2026 11:45:43 -0400 Subject: [PATCH] =?UTF-8?q?feat(iac):=20capability=20extension=20=E2=80=94?= =?UTF-8?q?=20canonical=5Fkeys=20+=20compute=5Fplan=5Fversion=20on=20Capab?= =?UTF-8?q?ilitiesResponse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the SupportedCanonicalKeys + ComputePlanVersionDeclarer regressions left open by the strict-contracts force-cutover (ADRs 0024-0028) per the deferred option-d follow-up: Proto: adds 2 optional fields to CapabilitiesResponse — - canonical_keys (repeated string): provider-level override of interfaces.CanonicalKeys() default. DO plugin override path restored. - compute_plan_version (string): provider-level apply-time dispatch version. typedIaCAdapter now satisfies wfctlhelpers.ComputePlanVersionDeclarer so DispatchVersionFor reads the plugin's declaration instead of silently defaulting to v1. Adapter: caches CapabilitiesResponse on first access; subsequent SupportedCanonicalKeys / ComputePlanVersion / Capabilities calls reuse the cached value (capabilities are advertised once at plugin startup + don't change during a wfctl invocation; cache avoids RPC thrash on the apply-time dispatch hot path). Tests: 5 new cases on *typedIaCAdapter cover override path, default fallback (set-based comparison since CanonicalKeys() iteration order isn't guaranteed), apply-version declaration, empty-declaration default, and cache-reuse invariant. Full -short suite + -race PASS. ADR-0029 documents the decision, alternatives rejected (per-type canonical_keys, dedicated SupportedCanonicalKeys RPC, separate ComputePlanVersionDeclarer service), and plugin-author migration shape. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/wfctl/iac_typed_adapter.go | 79 +++++++++- cmd/wfctl/iac_typed_adapter_test.go | 145 ++++++++++++++++- ...canonical-keys-and-compute-plan-version.md | 147 ++++++++++++++++++ plugin/external/proto/iac.pb.go | 42 ++++- plugin/external/proto/iac.proto | 14 ++ 5 files changed, 411 insertions(+), 16 deletions(-) create mode 100644 decisions/0029-capability-extension-canonical-keys-and-compute-plan-version.md diff --git a/cmd/wfctl/iac_typed_adapter.go b/cmd/wfctl/iac_typed_adapter.go index 2cbbfd36..39cf882c 100644 --- a/cmd/wfctl/iac_typed_adapter.go +++ b/cmd/wfctl/iac_typed_adapter.go @@ -37,6 +37,7 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/GoCodeAlone/workflow/iac/wfctlhelpers" "github.com/GoCodeAlone/workflow/interfaces" pb "github.com/GoCodeAlone/workflow/plugin/external/proto" ) @@ -60,6 +61,13 @@ const ( // pb.IaC* gRPC clients. Optional clients are nil when the plugin did not // register the corresponding service — call paths gated on those clients // return interfaces.ErrProviderMethodUnimplemented. +// +// Capability cache (cachedCaps): the plugin's CapabilitiesResponse is +// fetched lazily on the first call to Capabilities() / SupportedCanonicalKeys() +// / ComputePlanVersion() and reused for the adapter's lifetime. Capabilities +// are advertised once at plugin startup and don't change during a wfctl +// invocation; caching lets per-call accessors (notably the apply-time +// dispatch decision) avoid an RPC round-trip per access. Per ADR-0029. type typedIaCAdapter struct { conn *grpc.ClientConn @@ -71,6 +79,12 @@ type typedIaCAdapter struct { validator pb.IaCProviderValidatorClient driftCfg pb.IaCProviderDriftConfigDetectorClient resourceDriv pb.ResourceDriverClient + + // cachedCaps memoizes the plugin's CapabilitiesResponse. Access via + // fetchCapabilities — never read this field directly. + cachedCaps *pb.CapabilitiesResponse + capsErr error + capsFetch bool // true once first fetch attempt completed (success OR error) } // newTypedIaCAdapter builds an adapter from a live gRPC connection plus a @@ -277,8 +291,27 @@ func (a *typedIaCAdapter) Initialize(ctx context.Context, config map[string]any) return err } -func (a *typedIaCAdapter) Capabilities() []interfaces.IaCCapabilityDeclaration { +// fetchCapabilities returns the plugin's CapabilitiesResponse, caching the +// first result for the adapter's lifetime. RPC errors are also cached so +// repeated accesses don't repeatedly fail against an unreachable plugin. +// Capabilities are advertised at plugin startup and don't change during +// a wfctl invocation; caching is correct + cheap. +func (a *typedIaCAdapter) fetchCapabilities() (*pb.CapabilitiesResponse, error) { + if a.capsFetch { + return a.cachedCaps, a.capsErr + } + a.capsFetch = true resp, err := a.required.Capabilities(context.Background(), &pb.CapabilitiesRequest{}) + if err != nil { + a.capsErr = err + return nil, err + } + a.cachedCaps = resp + return resp, nil +} + +func (a *typedIaCAdapter) Capabilities() []interfaces.IaCCapabilityDeclaration { + resp, err := a.fetchCapabilities() if err != nil { return nil } @@ -375,16 +408,42 @@ func (a *typedIaCAdapter) ResourceDriver(resourceType string) (interfaces.Resour return &typedResourceDriver{client: a.resourceDriv, resourceType: resourceType}, nil } +// SupportedCanonicalKeys returns the canonical IaC config keys this +// plugin supports. Reads from the cached CapabilitiesResponse: +// - non-empty CapabilitiesResponse.canonical_keys → use those (provider +// declared a strict subset, e.g. DO plugin removing loadbalancer/vpc/k8s) +// - empty list OR Capabilities RPC failure → fall back to +// interfaces.CanonicalKeys() wfctl-side default +// +// Per ADR-0029. Closes the regression where the typed cutover lost the +// per-provider override path that legacy remoteIaCProvider routed via +// InvokeService("SupportedCanonicalKeys", ...). func (a *typedIaCAdapter) SupportedCanonicalKeys() []string { - // SupportedCanonicalKeys is intentionally absent from the typed proto - // surface — providers declare their canonical-key support through the - // existing ContractRegistry capability flow (Task 5) rather than a - // dedicated RPC. Returning the canonical-keys default keeps engine - // consumers unchanged; provider-level overrides will land via the - // capability registry follow-up. + resp, err := a.fetchCapabilities() + if err == nil && resp != nil { + if keys := resp.GetCanonicalKeys(); len(keys) > 0 { + return append([]string(nil), keys...) + } + } return interfaces.CanonicalKeys() } +// ComputePlanVersion returns the apply-time dispatch version the plugin +// declared in CapabilitiesResponse. Empty string (or RPC failure) means +// "v1" by ComputePlanVersionDeclarer convention — DispatchVersionFor +// treats unknown values as v1, so unset cleanly degrades to legacy path. +// +// The presence of this method on *typedIaCAdapter means it satisfies +// wfctlhelpers.ComputePlanVersionDeclarer at compile time, restoring the +// type-assert dispatch parity with legacy remoteIaCProvider. Per ADR-0029. +func (a *typedIaCAdapter) ComputePlanVersion() string { + resp, err := a.fetchCapabilities() + if err != nil || resp == nil { + return "" + } + return resp.GetComputePlanVersion() +} + func (a *typedIaCAdapter) BootstrapStateBackend(ctx context.Context, cfg map[string]any) (*interfaces.BootstrapResult, error) { cfgJSON, err := marshalJSONMap(cfg) if err != nil { @@ -1277,4 +1336,10 @@ var ( _ interfaces.ProviderMigrationRepairer = (*typedIaCAdapter)(nil) _ interfaces.ResourceDriver = (*typedResourceDriver)(nil) _ interfaces.Troubleshooter = (*typedResourceDriver)(nil) + // ADR-0029 capability extension: typedIaCAdapter satisfies + // ComputePlanVersionDeclarer so wfctlhelpers.DispatchVersionFor's + // type-assert dispatch picks up the plugin's declared apply-version + // from the cached CapabilitiesResponse instead of silently falling + // back to "v1". + _ wfctlhelpers.ComputePlanVersionDeclarer = (*typedIaCAdapter)(nil) ) diff --git a/cmd/wfctl/iac_typed_adapter_test.go b/cmd/wfctl/iac_typed_adapter_test.go index 7d246bea..d41371b5 100644 --- a/cmd/wfctl/iac_typed_adapter_test.go +++ b/cmd/wfctl/iac_typed_adapter_test.go @@ -25,6 +25,8 @@ import ( "net" "testing" + "github.com/GoCodeAlone/workflow/iac/wfctlhelpers" + "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" @@ -228,6 +230,134 @@ func TestTypedAdapter_EndToEnd_NameVersionEnumerateAll(t *testing.T) { // ─── In-process gRPC test fixture ─────────────────────────────────────────── +// ─── ADR-0029 capability-extension tests ───────────────────────────────── + +// TestTypedAdapter_SupportedCanonicalKeys_PluginOverride exercises the +// regression closure: plugin declares a strict subset of canonical keys +// in CapabilitiesResponse, adapter returns those (not the wfctl-side +// default). +func TestTypedAdapter_SupportedCanonicalKeys_PluginOverride(t *testing.T) { + provider := &fullStubProvider{ + name: "do", + version: "v1.0.0", + canonicalKeys: []string{"infra.spaces", "infra.spaces_key", "infra.droplet"}, + } + srv, conn := startTestServer(t, provider, false) + t.Cleanup(srv.Stop) + t.Cleanup(func() { _ = conn.Close() }) + + adapter := newTypedIaCAdapter(conn, nil) + got := adapter.SupportedCanonicalKeys() + want := []string{"infra.spaces", "infra.spaces_key", "infra.droplet"} + if len(got) != len(want) { + t.Fatalf("SupportedCanonicalKeys returned %d keys; want %d (got=%v)", len(got), len(want), got) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("SupportedCanonicalKeys[%d] = %q; want %q", i, got[i], want[i]) + } + } +} + +// TestTypedAdapter_SupportedCanonicalKeys_FallbackToDefault exercises +// the empty-canonical-keys path: adapter falls back to +// interfaces.CanonicalKeys() so plugins without an override work as +// before. Comparison is set-based since the underlying default's +// iteration order isn't guaranteed. +func TestTypedAdapter_SupportedCanonicalKeys_FallbackToDefault(t *testing.T) { + provider := &fullStubProvider{name: "stub", version: "v0"} // no canonical_keys + srv, conn := startTestServer(t, provider, false) + t.Cleanup(srv.Stop) + t.Cleanup(func() { _ = conn.Close() }) + + adapter := newTypedIaCAdapter(conn, nil) + got := adapter.SupportedCanonicalKeys() + want := interfaces.CanonicalKeys() + if len(got) != len(want) { + t.Fatalf("SupportedCanonicalKeys returned %d keys; want %d (default fallback)", len(got), len(want)) + } + wantSet := make(map[string]bool, len(want)) + for _, k := range want { + wantSet[k] = true + } + for _, k := range got { + if !wantSet[k] { + t.Errorf("returned key %q not in interfaces.CanonicalKeys() default set", k) + } + } +} + +// TestTypedAdapter_ComputePlanVersion_PluginDeclares verifies +// CapabilitiesResponse.compute_plan_version surfaces through the adapter +// for ComputePlanVersionDeclarer dispatch. +func TestTypedAdapter_ComputePlanVersion_PluginDeclares(t *testing.T) { + provider := &fullStubProvider{name: "do", version: "v1.0.0", computePlanVersion: "v2"} + srv, conn := startTestServer(t, provider, false) + t.Cleanup(srv.Stop) + t.Cleanup(func() { _ = conn.Close() }) + + adapter := newTypedIaCAdapter(conn, nil) + if got := adapter.ComputePlanVersion(); got != "v2" { + t.Errorf("ComputePlanVersion = %q; want %q", got, "v2") + } + + // DispatchVersionFor honors the declaration. + if got := wfctlhelpers.DispatchVersionFor(adapter); got != "v2" { + t.Errorf("DispatchVersionFor = %q; want %q", got, "v2") + } +} + +// TestTypedAdapter_ComputePlanVersion_EmptyMeansV1 verifies plugins that +// don't declare compute_plan_version get the legacy "v1" dispatch path +// via DispatchVersionFor's default-on-empty rule. +func TestTypedAdapter_ComputePlanVersion_EmptyMeansV1(t *testing.T) { + provider := &fullStubProvider{name: "stub", version: "v0"} // no compute_plan_version + srv, conn := startTestServer(t, provider, false) + t.Cleanup(srv.Stop) + t.Cleanup(func() { _ = conn.Close() }) + + adapter := newTypedIaCAdapter(conn, nil) + if got := adapter.ComputePlanVersion(); got != "" { + t.Errorf("ComputePlanVersion = %q; want empty (no declaration)", got) + } + if got := wfctlhelpers.DispatchVersionFor(adapter); got != "v1" { + t.Errorf("DispatchVersionFor = %q; want %q (empty → v1)", got, "v1") + } +} + +// TestTypedAdapter_CapabilitiesCacheReusedAcrossCalls verifies the +// CapabilitiesResponse is fetched at most once across repeated accessor +// calls (avoids RPC thrash on the dispatch hot path). +func TestTypedAdapter_CapabilitiesCacheReusedAcrossCalls(t *testing.T) { + provider := &countingCapabilitiesProvider{computePlanVersion: "v2"} + srv, conn := startTestServer(t, provider, false) + t.Cleanup(srv.Stop) + t.Cleanup(func() { _ = conn.Close() }) + + adapter := newTypedIaCAdapter(conn, nil) + for i := 0; i < 5; i++ { + _ = adapter.ComputePlanVersion() + _ = adapter.SupportedCanonicalKeys() + _ = adapter.Capabilities() + } + if provider.calls != 1 { + t.Errorf("Capabilities RPC called %d times; want 1 (cache miss after first call)", provider.calls) + } +} + +// countingCapabilitiesProvider counts Capabilities() RPC invocations to +// verify caching behavior. +type countingCapabilitiesProvider struct { + pb.UnimplementedIaCProviderRequiredServer + computePlanVersion string + calls int +} + +func (p *countingCapabilitiesProvider) Capabilities(_ context.Context, _ *pb.CapabilitiesRequest) (*pb.CapabilitiesResponse, error) { + p.calls++ + return &pb.CapabilitiesResponse{ComputePlanVersion: p.computePlanVersion}, nil +} + // startTestServer spins up an in-process gRPC server registered with // the supplied IaCProviderRequiredServer (and optionally the matching // enumerator) on a localhost ephemeral port. Returns the server and a @@ -267,9 +397,18 @@ type fullStubProvider struct { pb.UnimplementedIaCProviderRequiredServer pb.UnimplementedIaCProviderEnumeratorServer - name string - version string - enumerated []*pb.ResourceOutput + name string + version string + enumerated []*pb.ResourceOutput + canonicalKeys []string // ADR-0029: empty = adapter falls back to interfaces.CanonicalKeys() + computePlanVersion string // ADR-0029: empty = adapter returns "" (DispatchVersionFor → "v1") +} + +func (s *fullStubProvider) Capabilities(_ context.Context, _ *pb.CapabilitiesRequest) (*pb.CapabilitiesResponse, error) { + return &pb.CapabilitiesResponse{ + CanonicalKeys: s.canonicalKeys, + ComputePlanVersion: s.computePlanVersion, + }, nil } func (s *fullStubProvider) Name(_ context.Context, _ *pb.NameRequest) (*pb.NameResponse, error) { diff --git a/decisions/0029-capability-extension-canonical-keys-and-compute-plan-version.md b/decisions/0029-capability-extension-canonical-keys-and-compute-plan-version.md new file mode 100644 index 00000000..82d1dfb3 --- /dev/null +++ b/decisions/0029-capability-extension-canonical-keys-and-compute-plan-version.md @@ -0,0 +1,147 @@ +# 0029 — Capability extension: canonical_keys + compute_plan_version on `CapabilitiesResponse` + +**Status:** Accepted +**Date:** 2026-05-10 +**Plan:** docs/plans/2026-05-10-strict-contracts-force-cutover.md (rev5, scope-locked at e82b7e0c) +**Supersedes nothing.** Closes regressions surfaced during ADR-0024/0025/0026 implementation. + +## Context + +The strict-contracts force-cutover (ADRs 0024-0028) replaced legacy +`InvokeService(method, args)` IaC dispatch with typed `pb.IaCProvider*` +gRPC services. Two engine-side IaCProvider methods had no typed-RPC +counterpart in the rev5 proto: + +1. **`SupportedCanonicalKeys() []string`** — wfctl-side canonical-key set, + per ADR-0026 documented as "wfctl-side resolved" via the static + `interfaces.CanonicalKeys()` default. The legacy `remoteIaCProvider` + actually routed this through `InvokeService("SupportedCanonicalKeys", …)`, + letting plugins **override** with a strict subset (DO plugin removes + `loadbalancer`, `vpc`, `kubernetes_cluster` via its + `doUnsupportedCanonicalKeys` filter). The typed cutover preserved the + wfctl-side default but lost the override path — confirmed regression + on DO plugin per Task 30 implementer survey. + +2. **`ComputePlanVersion() string`** (optional via + `wfctlhelpers.ComputePlanVersionDeclarer`) — apply-time dispatch + version selector. Legacy `remoteIaCProvider` exposed this from the + plugin manifest (`iacProvider.computePlanVersion` field). DO plugin + declares `"v2"`, routing apply through `wfctlhelpers.ApplyPlan` instead + of legacy `provider.Apply`. The typed `*typedIaCAdapter` did not + implement `ComputePlanVersionDeclarer`, so `DispatchVersionFor` fell + silently back to `"v1"` — silent dispatch downgrade. + +Both regressions are bounded (only DO plugin overrides today; AWS/GCP/ +Azure surveys came back empty) but real. Spec-reviewer + team-lead +ruled the fix as a follow-up additive PR (option-d capability extension) +sequenced between Task 17 and Task 20. + +## Decision + +Extend the existing `CapabilitiesResponse` proto message with two +optional plugin-level fields: + +```protobuf +message CapabilitiesResponse { + repeated IaCCapabilityDeclaration capabilities = 1; + // Provider-level override of interfaces.CanonicalKeys() default. + // Empty = use wfctl-side default; non-empty = filter to these keys. + repeated string canonical_keys = 2; + // Provider-level apply-time dispatch version. "" or unrecognized = "v1"; + // "v2" routes through wfctlhelpers.ApplyPlan. + string compute_plan_version = 3; +} +``` + +Update `*typedIaCAdapter`: + +- **Cache `CapabilitiesResponse`** at first access via + `fetchCapabilities()`. Capabilities are advertised once at plugin + startup and don't change during a wfctl invocation; caching avoids + RPC thrash on the apply-time dispatch hot path. +- **`SupportedCanonicalKeys()`** reads `resp.GetCanonicalKeys()`; falls + back to `interfaces.CanonicalKeys()` when empty or RPC fails. +- **`ComputePlanVersion()`** reads `resp.GetComputePlanVersion()`; + returns `""` on miss (empty string defaults to `"v1"` via + `DispatchVersionFor`'s existing default-on-empty rule). +- Compile-time guard + `var _ wfctlhelpers.ComputePlanVersionDeclarer = (*typedIaCAdapter)(nil)` + so the type-assert dispatch resolves to the typed adapter without + callers needing concrete-type knowledge. + +## Consequences + +- **DO plugin regression closed** once DO plugin v1.0.0 (Phase 2) populates + these fields in its `Capabilities` RPC response. Until then, DO plugin + on rev5+ workflow yields the wfctl-side default canonical keys + v1 + dispatch — matches the typed-cutover transient behavior (acceptable). +- **AWS/GCP/Azure plugins**: when they cut over to typed gRPC services, + they SHOULD populate `canonical_keys` if they want to filter the + default set; otherwise they get the default automatically. Backward- + compatible. +- **Wire-level**: additive proto field numbers (2, 3); old plugin + binaries return empty values; new wfctl interprets empty as + "use defaults". No flag-day required. +- **Engine consumers** (`module/infra_module.go`, `iac/wfctlhelpers/apply.go`) + unchanged — they call `provider.SupportedCanonicalKeys()` and + `wfctlhelpers.DispatchVersionFor(provider)` exactly as before; the + adapter satisfies both contracts now. +- **Tests**: 5 new test cases on `*typedIaCAdapter` cover the override + path, the fallback path, the apply-version declaration, the empty- + declaration default, and the capability-cache reuse invariant. + +## Alternatives Rejected + +- **Per-`IaCCapabilityDeclaration` `canonical_keys` (per resource type)**: + rejected. DO's `doUnsupportedCanonicalKeys` is a top-level filter, not + per-resource-type. Per-type would add structural complexity for a + flexibility no current plugin exercises. YAGNI. + +- **Dedicated `SupportedCanonicalKeys` RPC**: rejected per spec-reviewer + during Task 30 review. Capability discovery is the natural home; an + extra RPC would mean two round-trips at plugin load time, extra + schema drift surface, and another optional-service registration to + manage. The Capabilities RPC is already required and lightweight. + +- **`ComputePlanVersionDeclarer` as a separate optional gRPC service**: + rejected. The version is a single string declaration, not a method + surface. Extending `CapabilitiesResponse` is a 1-field change; adding + a service is a registration + stub + auto-detect cycle. Keep the + registration-based capability advertisement (ADR-0025) for actual + *behavior* surfaces (Enumerator, DriftDetector, etc.), not for + static configuration values. + +- **Embed in `IaCCapabilityDeclaration` operations field**: rejected. + `operations` is the per-resource-type op set (create/read/update/…). + Plugin-level filters and apply-version don't belong there. + +## Migration + +For plugin authors: + +```go +// Capabilities RPC returns: +return &pb.CapabilitiesResponse{ + Capabilities: []*pb.IaCCapabilityDeclaration{ /* per-type decls */ }, + CanonicalKeys: []string{ + "infra.spaces", "infra.spaces_key", "infra.droplet", + // (omit infra.loadbalancer, infra.vpc, infra.kubernetes_cluster + // if your provider doesn't support them) + }, + ComputePlanVersion: "v2", // or "" for legacy v1 path +}, nil +``` + +For engine consumers: no change. `provider.SupportedCanonicalKeys()` +and `wfctlhelpers.DispatchVersionFor(provider)` work as before. + +## Related + +- ADR 0024 (IaC typed force-cutover) +- ADR 0026 (direct gRPC client, no wrapper) — established + SupportedCanonicalKeys as wfctl-side; this ADR adds the + plugin-override path +- Task 30 implementer survey (DO plugin override evidence) +- `feedback_force_strict_contracts_no_compat`: this PR is fix-forward + to a typed gRPC field, NOT a compat shim. Empty fields default to + legacy/wfctl-side behavior. diff --git a/plugin/external/proto/iac.pb.go b/plugin/external/proto/iac.pb.go index cb581c71..50b8f4d8 100644 --- a/plugin/external/proto/iac.pb.go +++ b/plugin/external/proto/iac.pb.go @@ -2193,10 +2193,24 @@ func (*CapabilitiesRequest) Descriptor() ([]byte, []int) { } type CapabilitiesResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Capabilities []*IaCCapabilityDeclaration `protobuf:"bytes,1,rep,name=capabilities,proto3" json:"capabilities,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Capabilities []*IaCCapabilityDeclaration `protobuf:"bytes,1,rep,name=capabilities,proto3" json:"capabilities,omitempty"` + // canonical_keys: provider-level override of interfaces.CanonicalKeys() + // wfctl-side default. Empty list = use default; non-empty = filter to + // exactly these keys (e.g. DO plugin removes loadbalancer/vpc/k8s). + // Closes the SupportedCanonicalKeys regression introduced by the typed + // cutover (typedIaCAdapter previously couldn't ask the plugin for its + // canonical-keys override). See ADR-0029. + CanonicalKeys []string `protobuf:"bytes,2,rep,name=canonical_keys,json=canonicalKeys,proto3" json:"canonical_keys,omitempty"` + // compute_plan_version: provider-level apply-time dispatch version. + // "" or unrecognized = "v1" (legacy provider.Apply path); "v2" routes + // through wfctlhelpers.ApplyPlan. Mirrors the + // ComputePlanVersionDeclarer optional Go interface so plugin authors + // can declare apply-version via gRPC instead of a separate type-assert + // probe. See ADR-0029. + ComputePlanVersion string `protobuf:"bytes,3,opt,name=compute_plan_version,json=computePlanVersion,proto3" json:"compute_plan_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CapabilitiesResponse) Reset() { @@ -2236,6 +2250,20 @@ func (x *CapabilitiesResponse) GetCapabilities() []*IaCCapabilityDeclaration { return nil } +func (x *CapabilitiesResponse) GetCanonicalKeys() []string { + if x != nil { + return x.CanonicalKeys + } + return nil +} + +func (x *CapabilitiesResponse) GetComputePlanVersion() string { + if x != nil { + return x.ComputePlanVersion + } + return "" +} + type PlanRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Desired []*ResourceSpec `protobuf:"bytes,1,rep,name=desired,proto3" json:"desired,omitempty"` @@ -4705,9 +4733,11 @@ const file_iac_proto_rawDesc = "" + "\x0eVersionRequest\"+\n" + "\x0fVersionResponse\x12\x18\n" + "\aversion\x18\x01 \x01(\tR\aversion\"\x15\n" + - "\x13CapabilitiesRequest\"r\n" + + "\x13CapabilitiesRequest\"\xcb\x01\n" + "\x14CapabilitiesResponse\x12Z\n" + - "\fcapabilities\x18\x01 \x03(\v26.workflow.plugin.external.iac.IaCCapabilityDeclarationR\fcapabilities\"\x9a\x01\n" + + "\fcapabilities\x18\x01 \x03(\v26.workflow.plugin.external.iac.IaCCapabilityDeclarationR\fcapabilities\x12%\n" + + "\x0ecanonical_keys\x18\x02 \x03(\tR\rcanonicalKeys\x120\n" + + "\x14compute_plan_version\x18\x03 \x01(\tR\x12computePlanVersion\"\x9a\x01\n" + "\vPlanRequest\x12D\n" + "\adesired\x18\x01 \x03(\v2*.workflow.plugin.external.iac.ResourceSpecR\adesired\x12E\n" + "\acurrent\x18\x02 \x03(\v2+.workflow.plugin.external.iac.ResourceStateR\acurrent\"I\n" + diff --git a/plugin/external/proto/iac.proto b/plugin/external/proto/iac.proto index b6d6a110..a68c1f13 100644 --- a/plugin/external/proto/iac.proto +++ b/plugin/external/proto/iac.proto @@ -366,6 +366,20 @@ message VersionResponse { message CapabilitiesRequest {} message CapabilitiesResponse { repeated IaCCapabilityDeclaration capabilities = 1; + // canonical_keys: provider-level override of interfaces.CanonicalKeys() + // wfctl-side default. Empty list = use default; non-empty = filter to + // exactly these keys (e.g. DO plugin removes loadbalancer/vpc/k8s). + // Closes the SupportedCanonicalKeys regression introduced by the typed + // cutover (typedIaCAdapter previously couldn't ask the plugin for its + // canonical-keys override). See ADR-0029. + repeated string canonical_keys = 2; + // compute_plan_version: provider-level apply-time dispatch version. + // "" or unrecognized = "v1" (legacy provider.Apply path); "v2" routes + // through wfctlhelpers.ApplyPlan. Mirrors the + // ComputePlanVersionDeclarer optional Go interface so plugin authors + // can declare apply-version via gRPC instead of a separate type-assert + // probe. See ADR-0029. + string compute_plan_version = 3; } message PlanRequest {