Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d2cd4c5
docs(plans): cloud-SDK extraction design — workflow core → strict-con…
intel352 May 14, 2026
98448d5
docs(plans): cloud-SDK extraction design — adversarial review cycle 1…
intel352 May 14, 2026
18534e8
docs(plans): cloud-SDK extraction design — adversarial review cycle 2…
intel352 May 14, 2026
5fb2000
docs(plans): fix stale Phase A/B refs + Status line post-cycle-2
intel352 May 14, 2026
39a3b43
docs(plans): cloud-SDK extraction design — adversarial review cycle 3…
intel352 May 14, 2026
a6d6396
docs(plans): cloud-SDK extraction design — adversarial review cycle 4…
intel352 May 14, 2026
e71fa10
docs(plans): cloud-SDK extraction design — adversarial review cycle 5…
intel352 May 14, 2026
ac10b67
docs(plans): cloud-SDK extraction design — adversarial review cycle 6…
intel352 May 14, 2026
808ec51
docs(plans): cloud-SDK extraction design — adversarial review cycle 7…
intel352 May 14, 2026
337d0e0
docs(plans): cloud-SDK extraction design — cycle-8 re-baseline agains…
intel352 May 14, 2026
b9facf5
docs(plans): cloud-SDK extraction design — cycle-9 re-baseline + audi…
intel352 May 14, 2026
264791b
docs(plans): cloud-SDK extraction design — cycle-10 re-baseline, AWS …
intel352 May 14, 2026
1b8fb38
docs(plans): cloud-SDK extraction design — cycle-11 PASS, minor cleanups
intel352 May 14, 2026
a81f0b6
fix(scripts): audit-cloud-symbols single-line-import grep poisoned th…
intel352 May 14, 2026
02fe621
docs(plans): cloud-SDK extraction implementation plan (Phase 0 + Phas…
intel352 May 14, 2026
3499c8b
docs(plans): cloud-SDK extraction plan — address plan-phase adversari…
intel352 May 14, 2026
e34be7c
docs(plans): cloud-SDK extraction plan — plan-review cycle 2 fixes
intel352 May 14, 2026
bdf0de8
docs(plans): cloud-SDK extraction plan — plan-review cycle 3 PASS + m…
intel352 May 14, 2026
e67757c
chore: lock scope for cloud-sdk-extraction (alignment passed)
intel352 May 14, 2026
cdc2694
refactor(module): split platform_kubernetes_kind.go into _core + _gke
intel352 May 14, 2026
c220beb
docs(module): add file-purpose headers to platform_kubernetes _core/_gke
intel352 May 14, 2026
443f495
docs(module): fix stale 'Requires the Azure SDK' comment on aksBackend
intel352 May 14, 2026
bf203f4
ci(audit): enforce k8s-backend init() partition + run audit on every PR
intel352 May 14, 2026
67c8b0a
docs(plans): IaCStateBackend transport benchmark result — decision pe…
intel352 May 14, 2026
20d7fbb
docs(plans): Task 6 resolved — unary IaCStateBackend LOCKED (operator…
intel352 May 14, 2026
6062520
Revert "chore: lock scope for cloud-sdk-extraction (alignment passed)"
intel352 May 14, 2026
2bbe60f
docs(plans): amend cloud-sdk-extraction plan — PR 6 (ctx) + de-gate PR 4
intel352 May 14, 2026
bfa06e6
chore: re-lock scope for cloud-sdk-extraction (amended — alignment re…
intel352 May 14, 2026
a3759d2
feat(proto): add IaCStateBackend service to iac.proto
intel352 May 14, 2026
fc333a0
test(module): add IaCStateBackend gRPC-vs-in-process benchmark harness
intel352 May 14, 2026
7da06cf
test(wftest): add IaCStateBackend to iacServiceChecks coverage table
intel352 May 14, 2026
4aab57b
feat(module): IaCState proto converters + grpcIaCStateStore client ad…
intel352 May 14, 2026
abd526f
docs(module): note context.Background() follow-up on grpcIaCStateStore
intel352 May 14, 2026
774a3bd
feat(module): engine-side iac.state plugin-backend registry + dispatch
intel352 May 14, 2026
3589a99
feat(module): exempt *_ref keys from redaction; lock in credentials: …
intel352 May 14, 2026
f613432
refactor(module): name the _ref redaction-exemption suffix as a const
intel352 May 14, 2026
35642c0
test(plugin/external): guard against gRPC body-logging interceptors
intel352 May 14, 2026
cfa6b0b
test(plugin/external): broaden interceptor guard to Stream interceptors
intel352 May 14, 2026
22fb371
Merge remote-tracking branch 'origin/main' into feat/cloud-sdk-extrac…
intel352 May 14, 2026
003dd58
fix(iac-host): narrow _ref redaction exemption, validate registry inp…
intel352 May 14, 2026
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
74 changes: 6 additions & 68 deletions module/benchmark_iac_state_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@ package module

import (
"context"
"encoding/json"
"net"
"strconv"
"strings"
"testing"

pb "github.com/GoCodeAlone/workflow/plugin/external/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"google.golang.org/grpc/test/bufconn"
)

Expand All @@ -30,68 +27,6 @@ func oneMBState() *IaCState {
}
}

// benchStateToProto — local, self-contained IaCState -> pb.IaCState converter.
// Task 7 replaces this with the production iacStateToProto.
func benchStateToProto(s *IaCState) *pb.IaCState {
outJSON, _ := json.Marshal(s.Outputs)
cfgJSON, _ := json.Marshal(s.Config)
return &pb.IaCState{
ResourceId: s.ResourceID, ResourceType: s.ResourceType, Provider: s.Provider,
Status: s.Status, OutputsJson: outJSON, ConfigJson: cfgJSON,
CreatedAt: s.CreatedAt, UpdatedAt: s.UpdatedAt,
}
}

// benchStateBackendServer wraps an IaCStateStore behind pb.IaCStateBackendServer.
// Task 7 promotes this to the production iacStateBackendServer.
type benchStateBackendServer struct {
pb.UnimplementedIaCStateBackendServer
store IaCStateStore
}

func (s *benchStateBackendServer) GetState(_ context.Context, r *pb.GetStateRequest) (*pb.GetStateResponse, error) {
st, err := s.store.GetState(r.ResourceId)
if err != nil {
return nil, err
}
if st == nil {
return &pb.GetStateResponse{Exists: false}, nil
}
return &pb.GetStateResponse{Exists: true, State: benchStateToProto(st)}, nil
}
func (s *benchStateBackendServer) SaveState(_ context.Context, r *pb.SaveStateRequest) (*pb.SaveStateResponse, error) {
if r.State == nil {
return nil, status.Error(codes.InvalidArgument, "SaveState: request State is nil")
}
var outputs, config map[string]any
if len(r.State.OutputsJson) > 0 {
if err := json.Unmarshal(r.State.OutputsJson, &outputs); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "SaveState: invalid OutputsJson: %v", err)
}
}
if len(r.State.ConfigJson) > 0 {
if err := json.Unmarshal(r.State.ConfigJson, &config); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "SaveState: invalid ConfigJson: %v", err)
}
}
return &pb.SaveStateResponse{}, s.store.SaveState(&IaCState{
ResourceID: r.State.ResourceId, ResourceType: r.State.ResourceType,
Provider: r.State.Provider, Status: r.State.Status, Outputs: outputs, Config: config,
})
}
func (s *benchStateBackendServer) Lock(_ context.Context, r *pb.LockRequest) (*pb.LockResponse, error) {
return &pb.LockResponse{}, s.store.Lock(r.ResourceId)
}
func (s *benchStateBackendServer) Unlock(_ context.Context, r *pb.UnlockRequest) (*pb.UnlockResponse, error) {
return &pb.UnlockResponse{}, s.store.Unlock(r.ResourceId)
}
func (s *benchStateBackendServer) ListStates(_ context.Context, _ *pb.ListStatesRequest) (*pb.ListStatesResponse, error) {
return &pb.ListStatesResponse{}, nil
}
func (s *benchStateBackendServer) DeleteState(_ context.Context, r *pb.DeleteStateRequest) (*pb.DeleteStateResponse, error) {
return &pb.DeleteStateResponse{}, s.store.DeleteState(r.ResourceId)
}

// BenchmarkIaCStateBackend_InProcess is the baseline: direct IaCStateStore calls.
func BenchmarkIaCStateBackend_InProcess(b *testing.B) {
store := NewMemoryIaCStateStore()
Expand Down Expand Up @@ -119,9 +54,9 @@ func BenchmarkIaCStateBackend_GRPC(b *testing.B) {
// 4 MiB in-memory listener buffer. Note: this sizes the bufconn pipe only;
// gRPC's own max message size is configured separately via dial/server options.
lis := bufconn.Listen(4 << 20)
defer lis.Close()
b.Cleanup(func() { _ = lis.Close() })
srv := grpc.NewServer()
pb.RegisterIaCStateBackendServer(srv, &benchStateBackendServer{store: NewMemoryIaCStateStore()})
pb.RegisterIaCStateBackendServer(srv, &iacStateBackendServer{store: NewMemoryIaCStateStore()})
go func() { _ = srv.Serve(lis) }()
defer srv.Stop()

Expand All @@ -134,7 +69,10 @@ func BenchmarkIaCStateBackend_GRPC(b *testing.B) {
defer conn.Close()
client := pb.NewIaCStateBackendClient(conn)
st := oneMBState()
pbState := benchStateToProto(st)
pbState, err := iacStateToProto(st)
if err != nil {
b.Fatal(err)
}
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Expand Down
9 changes: 8 additions & 1 deletion module/iac_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,14 @@ func (m *IaCModule) Init(app modular.Application) error {
}
m.store = store
default:
return fmt.Errorf("iac.state %q: unsupported backend %q (use 'memory', 'filesystem', 'spaces', 'gcs', 'azure_blob', or 'postgres')", m.name, m.backend)
// Not a core in-process backend — consult the plugin-backend registry.
// The engine populates iacStateBackendRegistryInstance at plugin-load
// time; a resolved backend is served over gRPC via grpcIaCStateStore.
if client, ok := iacStateBackendRegistryInstance.resolve(m.backend); ok {
m.store = newGRPCIaCStateStore(client)
break
}
return fmt.Errorf("iac.state %q: unsupported backend %q (use 'memory', 'filesystem', 'spaces', 'gcs', 'azure_blob', or 'postgres', or load the plugin that provides it)", m.name, m.backend)
}

return app.RegisterService(m.name, m.store)
Expand Down
253 changes: 253 additions & 0 deletions module/iac_state_grpc_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
package module

import (
"context"
"encoding/json"

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

// ─────────────────────────────────────────────────────────────────────────────
// IaCState ⇄ pb.IaCState converters.
//
// The free-form Outputs / Config map[string]any fields cross the wire as JSON
// bytes — the iac.proto hard invariant (iac.proto:6-10) forbids
// google.protobuf.Struct. The plugin/host owns json.Marshal/Unmarshal directly.
// ─────────────────────────────────────────────────────────────────────────────

// iacStateToProto converts a module IaCState into its proto wire form.
func iacStateToProto(s *IaCState) (*pb.IaCState, error) {
if s == nil {
return nil, nil
}
outputsJSON, err := json.Marshal(s.Outputs)
if err != nil {
return nil, err
}
configJSON, err := json.Marshal(s.Config)
if err != nil {
return nil, err
}
return &pb.IaCState{
ResourceId: s.ResourceID,
ResourceType: s.ResourceType,
Provider: s.Provider,
ProviderRef: s.ProviderRef,
ProviderId: s.ProviderID,
ConfigHash: s.ConfigHash,
Status: s.Status,
OutputsJson: outputsJSON,
ConfigJson: configJSON,
Dependencies: s.Dependencies,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
Error: s.Error,
}, nil
}

// iacStateFromProto converts a proto IaCState back into a module IaCState.
//
// Empty / "null" / "{}" JSON byte payloads decode to a nil map (not an empty
// non-nil map) so round-trips through a nil Outputs/Config stay clean.
func iacStateFromProto(p *pb.IaCState) (*IaCState, error) {
if p == nil {
return nil, nil
}
outputs, err := jsonBytesToMap(p.OutputsJson)
if err != nil {
return nil, err
}
config, err := jsonBytesToMap(p.ConfigJson)
if err != nil {
return nil, err
}
return &IaCState{
ResourceID: p.ResourceId,
ResourceType: p.ResourceType,
Provider: p.Provider,
ProviderRef: p.ProviderRef,
ProviderID: p.ProviderId,
ConfigHash: p.ConfigHash,
Status: p.Status,
Outputs: outputs,
Config: config,
Dependencies: p.Dependencies,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
Error: p.Error,
}, nil
}

// jsonBytesToMap decodes JSON bytes into a map[string]any. Empty, "null" and
// "{}" inputs yield a nil map.
func jsonBytesToMap(b []byte) (map[string]any, error) {
s := string(b)
if len(b) == 0 || s == "null" || s == "{}" {
return nil, nil
}
var m map[string]any
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
return m, nil
}

// ─────────────────────────────────────────────────────────────────────────────
// grpcIaCStateStore — host-side IaCStateStore implemented over an
// IaCStateBackendClient. The host half of the strict IaCStateBackend contract.
// ─────────────────────────────────────────────────────────────────────────────

// grpcIaCStateStore adapts a pb.IaCStateBackendClient to module.IaCStateStore.
//
// All six methods call the backend with context.Background(): the
// module.IaCStateStore interface has no ctx parameter, so there is no caller
// context to plumb today. Threading a real context through IaCStateStore is a
// known follow-up, out of scope for this extraction.
type grpcIaCStateStore struct {
client pb.IaCStateBackendClient
}

// newGRPCIaCStateStore wraps an IaCStateBackendClient as an IaCStateStore.
func newGRPCIaCStateStore(c pb.IaCStateBackendClient) *grpcIaCStateStore {
return &grpcIaCStateStore{client: c}
}

// GetState retrieves a state record by resource ID. Returns nil, nil when the
// backend reports the record does not exist.
func (s *grpcIaCStateStore) GetState(resourceID string) (*IaCState, error) {
resp, err := s.client.GetState(context.Background(), &pb.GetStateRequest{ResourceId: resourceID})
if err != nil {
return nil, err
}
if !resp.Exists {
return nil, nil
}
return iacStateFromProto(resp.State)
}

// SaveState inserts or replaces a state record.
func (s *grpcIaCStateStore) SaveState(state *IaCState) error {
pbState, err := iacStateToProto(state)
if err != nil {
return err
}
_, err = s.client.SaveState(context.Background(), &pb.SaveStateRequest{State: pbState})
return err
}

// ListStates returns all state records matching the provided key=value filter.
func (s *grpcIaCStateStore) ListStates(filter map[string]string) ([]*IaCState, error) {
resp, err := s.client.ListStates(context.Background(), &pb.ListStatesRequest{Filter: filter})
if err != nil {
return nil, err
}
states := make([]*IaCState, 0, len(resp.States))
for _, p := range resp.States {
st, convErr := iacStateFromProto(p)
if convErr != nil {
return nil, convErr
}
states = append(states, st)
}
return states, nil
}

// DeleteState removes a state record by resource ID.
func (s *grpcIaCStateStore) DeleteState(resourceID string) error {
_, err := s.client.DeleteState(context.Background(), &pb.DeleteStateRequest{ResourceId: resourceID})
return err
}

// Lock acquires an exclusive lock for the given resource ID.
func (s *grpcIaCStateStore) Lock(resourceID string) error {
_, err := s.client.Lock(context.Background(), &pb.LockRequest{ResourceId: resourceID})
return err
}

// Unlock releases the lock for the given resource ID.
func (s *grpcIaCStateStore) Unlock(resourceID string) error {
_, err := s.client.Unlock(context.Background(), &pb.UnlockRequest{ResourceId: resourceID})
return err
}

// ─────────────────────────────────────────────────────────────────────────────
// iacStateBackendServer — production pb.IaCStateBackendServer that delegates to
// any module.IaCStateStore. The plugin-side half of the contract.
// ─────────────────────────────────────────────────────────────────────────────

// iacStateBackendServer serves an IaCStateStore over the IaCStateBackend gRPC
// contract.
type iacStateBackendServer struct {
pb.UnimplementedIaCStateBackendServer
store IaCStateStore
}

// GetState delegates to the backing store, mapping a not-found (nil) result to
// GetStateResponse{Exists: false}.
func (s *iacStateBackendServer) GetState(_ context.Context, r *pb.GetStateRequest) (*pb.GetStateResponse, error) {
st, err := s.store.GetState(r.ResourceId)
if err != nil {
return nil, err
}
if st == nil {
return &pb.GetStateResponse{Exists: false}, nil
}
pbState, err := iacStateToProto(st)
if err != nil {
return nil, err
}
return &pb.GetStateResponse{Exists: true, State: pbState}, nil
}

// SaveState delegates a full-state replace to the backing store.
func (s *iacStateBackendServer) SaveState(_ context.Context, r *pb.SaveStateRequest) (*pb.SaveStateResponse, error) {
st, err := iacStateFromProto(r.State)
if err != nil {
return nil, err
}
if err := s.store.SaveState(st); err != nil {
return nil, err
}
return &pb.SaveStateResponse{}, nil
}

// ListStates delegates a filtered listing to the backing store.
func (s *iacStateBackendServer) ListStates(_ context.Context, r *pb.ListStatesRequest) (*pb.ListStatesResponse, error) {
states, err := s.store.ListStates(r.Filter)
if err != nil {
return nil, err
}
pbStates := make([]*pb.IaCState, 0, len(states))
for _, st := range states {
pbState, convErr := iacStateToProto(st)
if convErr != nil {
return nil, convErr
}
pbStates = append(pbStates, pbState)
}
return &pb.ListStatesResponse{States: pbStates}, nil
}

// DeleteState delegates a delete-by-ID to the backing store.
func (s *iacStateBackendServer) DeleteState(_ context.Context, r *pb.DeleteStateRequest) (*pb.DeleteStateResponse, error) {
if err := s.store.DeleteState(r.ResourceId); err != nil {
return nil, err
}
return &pb.DeleteStateResponse{}, nil
}

// Lock delegates lock acquisition to the backing store.
func (s *iacStateBackendServer) Lock(_ context.Context, r *pb.LockRequest) (*pb.LockResponse, error) {
if err := s.store.Lock(r.ResourceId); err != nil {
return nil, err
}
return &pb.LockResponse{}, nil
}

// Unlock delegates lock release to the backing store.
func (s *iacStateBackendServer) Unlock(_ context.Context, r *pb.UnlockRequest) (*pb.UnlockResponse, error) {
if err := s.store.Unlock(r.ResourceId); err != nil {
return nil, err
}
return &pb.UnlockResponse{}, nil
}
Loading
Loading