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
98 changes: 98 additions & 0 deletions cmd/wfctl/iac_typed_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,104 @@ func newTypedIaCAdapter(conn *grpc.ClientConn, registered map[string]bool) *type
return a
}

// ─── Typed-client accessors (Task 17 capability discovery) ──────────────────
//
// Each accessor returns the underlying typed pb client for the named
// optional service, or nil if the plugin's ContractRegistry didn't
// advertise it. wfctl dispatch sites that previously did
// `if x, ok := provider.(interfaces.X); ok { x.Method(...) }` now
// type-assert to *typedIaCAdapter and use these accessors. The
// non-typed branch is per-site UX (ADR-0028 §Per-site dispatch UX) —
// hard-error at single-shot sites, soft-skip at iteration sites, e.g.:
//
// // Hard-error (single-shot — cleanup, apply-refresh):
// adapter, ok := provider.(*typedIaCAdapter)
// if !ok {
// return fmt.Errorf("provider %T is not a typed IaC adapter", provider)
// }
// if cli := adapter.Enumerator(); cli != nil {
// resp, err := cli.EnumerateByTag(ctx, &pb.EnumerateByTagRequest{Tag: t})
// // ...
// }
//
// // Soft-skip (iteration — status-drift, R-A10, bootstrap revoker):
// adapter, ok := provider.(*typedIaCAdapter)
// if !ok {
// fmt.Printf("WARNING: provider %q is not a typed adapter\n", name)
// continue // or return false / nil-skip per site
// }
//
// Either way the legacy interfaces.X fallback is gone. The interfaces.X
// definitions remain in `interfaces/` for engine-side / module-factory
// consumers — wfctl call sites are pure typed-pb (no string dispatch,
// no Go-interface indirection at the wfctl boundary).

// RequiredClient returns the typed pb.IaCProviderRequiredClient. Always
// non-nil (the loader rejects plugins that don't register the required
// service via the AssertIaCPluginAdvertisesRequiredService gate in
// PR #610). Exposed for symmetry with the optional accessors and for
// dispatch sites that want to call required RPCs directly without going
// through the interfaces.IaCProvider Go-interface methods.
func (a *typedIaCAdapter) RequiredClient() pb.IaCProviderRequiredClient {
return a.required
}

// Enumerator returns the typed pb.IaCProviderEnumeratorClient or nil
// when the plugin did not register IaCProviderEnumerator. Used by
// `wfctl infra cleanup --tag` (EnumerateByTag) and
// `wfctl infra audit-keys` / `wfctl infra prune` (EnumerateAll).
func (a *typedIaCAdapter) Enumerator() pb.IaCProviderEnumeratorClient {
return a.enumerator
}

// DriftDetector returns the typed pb.IaCProviderDriftDetectorClient or
// nil when the plugin did not register IaCProviderDriftDetector.
func (a *typedIaCAdapter) DriftDetector() pb.IaCProviderDriftDetectorClient {
return a.drift
}

// DriftConfigDetector returns the typed
// pb.IaCProviderDriftConfigDetectorClient or nil when the plugin did
// not register IaCProviderDriftConfigDetector. Used by
// `wfctl infra status drift` and `wfctl infra apply --refresh`
// to short-circuit between DetectDriftWithSpecs (config-aware) and
// the required IaCProvider.DetectDrift (existence-only) per ADR 0016.
func (a *typedIaCAdapter) DriftConfigDetector() pb.IaCProviderDriftConfigDetectorClient {
return a.driftCfg
}

// CredentialRevoker returns the typed
// pb.IaCProviderCredentialRevokerClient or nil when the plugin did not
// register IaCProviderCredentialRevoker. Used by
// `wfctl infra bootstrap --force-rotate` to invalidate the OLD
// provider credential after the new one is minted (ADR 0012).
func (a *typedIaCAdapter) CredentialRevoker() pb.IaCProviderCredentialRevokerClient {
return a.revoker
}

// MigrationRepairer returns the typed
// pb.IaCProviderMigrationRepairerClient or nil when the plugin did not
// register IaCProviderMigrationRepairer.
func (a *typedIaCAdapter) MigrationRepairer() pb.IaCProviderMigrationRepairerClient {
return a.repairer
}

// Validator returns the typed pb.IaCProviderValidatorClient or nil
// when the plugin did not register IaCProviderValidator. Used by R-A10
// (`wfctl infra align --strict`) to surface provider-side cross-
// resource constraint diagnostics at plan time.
func (a *typedIaCAdapter) Validator() pb.IaCProviderValidatorClient {
return a.validator
}

// ResourceDriverClient returns the typed pb.ResourceDriverClient or
// nil when the plugin did not register ResourceDriver. Each per-type
// dispatch carries the resource_type on every RPC, matching the DO
// plugin's 14-driver type-routing pattern in Task 11.
func (a *typedIaCAdapter) ResourceDriverClient() pb.ResourceDriverClient {
return a.resourceDriv
}

// translateRPCErr converts a gRPC Unimplemented status (the wire signal a
// plugin emits when an optional method is not supported) into the stable
// interfaces.ErrProviderMethodUnimplemented sentinel callers iterate on
Expand Down
99 changes: 99 additions & 0 deletions cmd/wfctl/iac_typed_dispatch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package main

// iac_typed_dispatch.go — typed-RPC dispatch helpers for the wfctl
// call sites converted in Task 17 of the strict-contracts force-cutover
// (docs/plans/2026-05-10-strict-contracts-force-cutover.md, rev5).
//
// Each helper wraps a single typed pb.IaC* client method behind a
// signature that matches the Go-interface contract the call site used
// to dispatch through. This keeps the conversion mechanical at the
// call site (`if cli := adapter.X(); cli != nil { typedRPC(...) }`)
// without leaking pb-message construction across infra_*.go boundaries.
//
// Why a separate file: the typed adapter (iac_typed_adapter.go from
// PR #605) defines the marshalling helpers (refsToPB, specToPB,
// driftsFromPB, etc.) at file-scope; reusing them here keeps a single
// source of truth for the proto/Go shape conversions while letting
// each call site stay focused on its dispatch logic.

import (
"context"
"fmt"

"github.com/GoCodeAlone/workflow/interfaces"
pb "github.com/GoCodeAlone/workflow/plugin/external/proto"
)

// detectDriftConfigTyped invokes IaCProviderDriftConfigDetector.DetectDriftConfig
// via the supplied typed client and converts the response into the
// engine-side []interfaces.DriftResult shape callers consume.
//
// Used by `wfctl infra status drift` and `wfctl infra apply --refresh`
// as the typed replacement for the legacy
// `provider.(interfaces.DriftConfigDetector).DetectDriftWithSpecs(...)`
// dispatch.
func detectDriftConfigTyped(ctx context.Context, cli pb.IaCProviderDriftConfigDetectorClient, refs []interfaces.ResourceRef, specs map[string]interfaces.ResourceSpec) ([]interfaces.DriftResult, error) {
pbSpecs := make(map[string]*pb.ResourceSpec, len(specs))
for k, s := range specs {
ps, err := specToPB(s)
if err != nil {
// Per code-review MINOR-2 (PR 618 round 4): name the offending
// spec key so post-mortem debugging doesn't require crashing
// through the marshalling helpers to find which entry in the
// per-resource map blew up.
return nil, fmt.Errorf("specToPB %q: %w", k, err)
}
pbSpecs[k] = ps
Comment on lines +36 to +46
}
resp, err := cli.DetectDriftConfig(ctx, &pb.DetectDriftConfigRequest{
Refs: refsToPB(refs),
Specs: pbSpecs,
})
if err != nil {
// Per code-review IMPORTANT-1 (PR 618 round 4): translate
// codes.Unimplemented at the wire boundary to
// interfaces.ErrProviderMethodUnimplemented so callers using
// errors.Is to detect "optional capability absent at runtime"
// keep the signal. Without this, a plugin that registered the
// IaCProviderDriftConfigDetector service but returns Unimplemented
// at the RPC level (e.g., a provider whose DriftConfigDetector
// is wired but the underlying driver doesn't support the
// resource type) would surface as a generic gRPC error rather
// than the iterate-and-skip sentinel. ADR-0028 §Migration's
// "Strict-mode invariant translation" depends on this.
return nil, translateRPCErr(err)
}
return driftsFromPB(resp.GetDrifts())
}

// validatePlanTyped invokes IaCProviderValidator.ValidatePlan via the
// supplied typed client. Replaces the legacy
// `provider.(interfaces.ProviderValidator).ValidatePlan(plan)` dispatch
// in infra_align_rules.go (R-A10 cross-resource constraint validation).
//
// The Go interfaces.ProviderValidator.ValidatePlan signature returns
// only []PlanDiagnostic (no error); errors are swallowed and surfaced
// as nil-diagnostics so callers that type-asserted-then-iterated
// continue to behave identically to "provider does not implement
// validation". This helper preserves that contract to keep R-A10
// behavior stable across the cutover.
func validatePlanTyped(ctx context.Context, cli pb.IaCProviderValidatorClient, plan *interfaces.IaCPlan) []interfaces.PlanDiagnostic {
pbPlan, err := planToPB(plan)
if err != nil {
return nil
}
resp, err := cli.ValidatePlan(ctx, &pb.ValidatePlanRequest{Plan: pbPlan})
if err != nil {
return nil
}
out := make([]interfaces.PlanDiagnostic, 0, len(resp.GetDiagnostics()))
for _, d := range resp.GetDiagnostics() {
out = append(out, interfaces.PlanDiagnostic{
Severity: planDiagnosticSeverityFromPB(d.GetSeverity()),
Resource: d.GetResource(),
Field: d.GetField(),
Message: d.GetMessage(),
})
}
return out
}
Loading
Loading