From a40dd6ebdb32d4541a5a12d6a2d1912e21cbe4c2 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 17 May 2026 01:51:59 -0400 Subject: [PATCH 01/11] feat(proto): add IaCProviderFinalizer optional service + FinalizeApply RPC (Phase 2.5) Adds optional IaCProviderFinalizer service with FinalizeApply RPC plus FinalizeApplyRequest{plan_id} and FinalizeApplyResponse{repeated ActionError errors} messages. Plugins opt in by registering the service; absence = no finalize hook (ADR 0024). Errors preserve per-driver attribution shape used by the DigitalOcean v1 wrapper (ADR 0040). Regenerated iac.pb.go + iac_grpc.pb.go via buf generate; bundled with proto edit per Phase 2 cycle-1 I-1 precedent (split would create broken intermediate commit). Phase 2.5 of workflow#640 / workflow#695. Co-Authored-By: Claude Opus 4.7 --- plugin/external/proto/iac.pb.go | 709 ++++++++++++++++----------- plugin/external/proto/iac.proto | 33 ++ plugin/external/proto/iac_grpc.pb.go | 118 +++++ 3 files changed, 562 insertions(+), 298 deletions(-) diff --git a/plugin/external/proto/iac.pb.go b/plugin/external/proto/iac.pb.go index 70c7242f..7bf2a30a 100644 --- a/plugin/external/proto/iac.pb.go +++ b/plugin/external/proto/iac.pb.go @@ -3502,6 +3502,108 @@ func (*RevokeProviderCredentialResponse) Descriptor() ([]byte, []int) { return file_iac_proto_rawDescGZIP(), []int{55} } +// ───────────────────────────────────────────────────────────────────────────── +// IaCProviderFinalizer messages. +// ───────────────────────────────────────────────────────────────────────────── +type FinalizeApplyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // plan_id is the IaCPlan.ID being finalized (for plugin-side logging / + // correlation; not load-bearing for dispatch). + PlanId string `protobuf:"bytes,1,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinalizeApplyRequest) Reset() { + *x = FinalizeApplyRequest{} + mi := &file_iac_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinalizeApplyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinalizeApplyRequest) ProtoMessage() {} + +func (x *FinalizeApplyRequest) ProtoReflect() protoreflect.Message { + mi := &file_iac_proto_msgTypes[56] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinalizeApplyRequest.ProtoReflect.Descriptor instead. +func (*FinalizeApplyRequest) Descriptor() ([]byte, []int) { + return file_iac_proto_rawDescGZIP(), []int{56} +} + +func (x *FinalizeApplyRequest) GetPlanId() string { + if x != nil { + return x.PlanId + } + return "" +} + +type FinalizeApplyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // errors is the per-driver finalize-side error array. Each entry + // preserves the v1 wrapper's per-driver attribution shape + // (workflow-plugin-digitalocean/internal/provider.go:301-306 uses + // ActionError{Resource: resourceType, Action: "deferred_update", Error: ...}). + // Empty array = success. Non-empty = each error is surfaced to wfctl's + // result.Errors as-is (preserving per-driver attribution), AND the + // outer finalize call returns a wrapped error to the caller. + // + // Tag 2 reserved for Phase 2.3 compensation evidence. + Errors []*ActionError `protobuf:"bytes,1,rep,name=errors,proto3" json:"errors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FinalizeApplyResponse) Reset() { + *x = FinalizeApplyResponse{} + mi := &file_iac_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FinalizeApplyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FinalizeApplyResponse) ProtoMessage() {} + +func (x *FinalizeApplyResponse) ProtoReflect() protoreflect.Message { + mi := &file_iac_proto_msgTypes[57] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FinalizeApplyResponse.ProtoReflect.Descriptor instead. +func (*FinalizeApplyResponse) Descriptor() ([]byte, []int) { + return file_iac_proto_rawDescGZIP(), []int{57} +} + +func (x *FinalizeApplyResponse) GetErrors() []*ActionError { + if x != nil { + return x.Errors + } + return nil +} + // ───────────────────────────────────────────────────────────────────────────── // IaCProviderMigrationRepairer messages. // ───────────────────────────────────────────────────────────────────────────── @@ -3514,7 +3616,7 @@ type RepairDirtyMigrationRequest struct { func (x *RepairDirtyMigrationRequest) Reset() { *x = RepairDirtyMigrationRequest{} - mi := &file_iac_proto_msgTypes[56] + mi := &file_iac_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3526,7 +3628,7 @@ func (x *RepairDirtyMigrationRequest) String() string { func (*RepairDirtyMigrationRequest) ProtoMessage() {} func (x *RepairDirtyMigrationRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[56] + mi := &file_iac_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3539,7 +3641,7 @@ func (x *RepairDirtyMigrationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RepairDirtyMigrationRequest.ProtoReflect.Descriptor instead. func (*RepairDirtyMigrationRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{56} + return file_iac_proto_rawDescGZIP(), []int{58} } func (x *RepairDirtyMigrationRequest) GetRequest() *MigrationRepairRequest { @@ -3558,7 +3660,7 @@ type RepairDirtyMigrationResponse struct { func (x *RepairDirtyMigrationResponse) Reset() { *x = RepairDirtyMigrationResponse{} - mi := &file_iac_proto_msgTypes[57] + mi := &file_iac_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3570,7 +3672,7 @@ func (x *RepairDirtyMigrationResponse) String() string { func (*RepairDirtyMigrationResponse) ProtoMessage() {} func (x *RepairDirtyMigrationResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[57] + mi := &file_iac_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3583,7 +3685,7 @@ func (x *RepairDirtyMigrationResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RepairDirtyMigrationResponse.ProtoReflect.Descriptor instead. func (*RepairDirtyMigrationResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{57} + return file_iac_proto_rawDescGZIP(), []int{59} } func (x *RepairDirtyMigrationResponse) GetResult() *MigrationRepairResult { @@ -3605,7 +3707,7 @@ type ValidatePlanRequest struct { func (x *ValidatePlanRequest) Reset() { *x = ValidatePlanRequest{} - mi := &file_iac_proto_msgTypes[58] + mi := &file_iac_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3617,7 +3719,7 @@ func (x *ValidatePlanRequest) String() string { func (*ValidatePlanRequest) ProtoMessage() {} func (x *ValidatePlanRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[58] + mi := &file_iac_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3630,7 +3732,7 @@ func (x *ValidatePlanRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidatePlanRequest.ProtoReflect.Descriptor instead. func (*ValidatePlanRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{58} + return file_iac_proto_rawDescGZIP(), []int{60} } func (x *ValidatePlanRequest) GetPlan() *IaCPlan { @@ -3649,7 +3751,7 @@ type ValidatePlanResponse struct { func (x *ValidatePlanResponse) Reset() { *x = ValidatePlanResponse{} - mi := &file_iac_proto_msgTypes[59] + mi := &file_iac_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3661,7 +3763,7 @@ func (x *ValidatePlanResponse) String() string { func (*ValidatePlanResponse) ProtoMessage() {} func (x *ValidatePlanResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[59] + mi := &file_iac_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3674,7 +3776,7 @@ func (x *ValidatePlanResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidatePlanResponse.ProtoReflect.Descriptor instead. func (*ValidatePlanResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{59} + return file_iac_proto_rawDescGZIP(), []int{61} } func (x *ValidatePlanResponse) GetDiagnostics() []*PlanDiagnostic { @@ -3700,7 +3802,7 @@ type DetectDriftConfigRequest struct { func (x *DetectDriftConfigRequest) Reset() { *x = DetectDriftConfigRequest{} - mi := &file_iac_proto_msgTypes[60] + mi := &file_iac_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3712,7 +3814,7 @@ func (x *DetectDriftConfigRequest) String() string { func (*DetectDriftConfigRequest) ProtoMessage() {} func (x *DetectDriftConfigRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[60] + mi := &file_iac_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3725,7 +3827,7 @@ func (x *DetectDriftConfigRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DetectDriftConfigRequest.ProtoReflect.Descriptor instead. func (*DetectDriftConfigRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{60} + return file_iac_proto_rawDescGZIP(), []int{62} } func (x *DetectDriftConfigRequest) GetRefs() []*ResourceRef { @@ -3751,7 +3853,7 @@ type DetectDriftConfigResponse struct { func (x *DetectDriftConfigResponse) Reset() { *x = DetectDriftConfigResponse{} - mi := &file_iac_proto_msgTypes[61] + mi := &file_iac_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3763,7 +3865,7 @@ func (x *DetectDriftConfigResponse) String() string { func (*DetectDriftConfigResponse) ProtoMessage() {} func (x *DetectDriftConfigResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[61] + mi := &file_iac_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3776,7 +3878,7 @@ func (x *DetectDriftConfigResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DetectDriftConfigResponse.ProtoReflect.Descriptor instead. func (*DetectDriftConfigResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{61} + return file_iac_proto_rawDescGZIP(), []int{63} } func (x *DetectDriftConfigResponse) GetDrifts() []*DriftResult { @@ -3802,7 +3904,7 @@ type ResourceCreateRequest struct { func (x *ResourceCreateRequest) Reset() { *x = ResourceCreateRequest{} - mi := &file_iac_proto_msgTypes[62] + mi := &file_iac_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3814,7 +3916,7 @@ func (x *ResourceCreateRequest) String() string { func (*ResourceCreateRequest) ProtoMessage() {} func (x *ResourceCreateRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[62] + mi := &file_iac_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3827,7 +3929,7 @@ func (x *ResourceCreateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceCreateRequest.ProtoReflect.Descriptor instead. func (*ResourceCreateRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{62} + return file_iac_proto_rawDescGZIP(), []int{64} } func (x *ResourceCreateRequest) GetResourceType() string { @@ -3853,7 +3955,7 @@ type ResourceCreateResponse struct { func (x *ResourceCreateResponse) Reset() { *x = ResourceCreateResponse{} - mi := &file_iac_proto_msgTypes[63] + mi := &file_iac_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3865,7 +3967,7 @@ func (x *ResourceCreateResponse) String() string { func (*ResourceCreateResponse) ProtoMessage() {} func (x *ResourceCreateResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[63] + mi := &file_iac_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3878,7 +3980,7 @@ func (x *ResourceCreateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceCreateResponse.ProtoReflect.Descriptor instead. func (*ResourceCreateResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{63} + return file_iac_proto_rawDescGZIP(), []int{65} } func (x *ResourceCreateResponse) GetOutput() *ResourceOutput { @@ -3898,7 +4000,7 @@ type ResourceReadRequest struct { func (x *ResourceReadRequest) Reset() { *x = ResourceReadRequest{} - mi := &file_iac_proto_msgTypes[64] + mi := &file_iac_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3910,7 +4012,7 @@ func (x *ResourceReadRequest) String() string { func (*ResourceReadRequest) ProtoMessage() {} func (x *ResourceReadRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[64] + mi := &file_iac_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3923,7 +4025,7 @@ func (x *ResourceReadRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceReadRequest.ProtoReflect.Descriptor instead. func (*ResourceReadRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{64} + return file_iac_proto_rawDescGZIP(), []int{66} } func (x *ResourceReadRequest) GetResourceType() string { @@ -3949,7 +4051,7 @@ type ResourceReadResponse struct { func (x *ResourceReadResponse) Reset() { *x = ResourceReadResponse{} - mi := &file_iac_proto_msgTypes[65] + mi := &file_iac_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3961,7 +4063,7 @@ func (x *ResourceReadResponse) String() string { func (*ResourceReadResponse) ProtoMessage() {} func (x *ResourceReadResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[65] + mi := &file_iac_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3974,7 +4076,7 @@ func (x *ResourceReadResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceReadResponse.ProtoReflect.Descriptor instead. func (*ResourceReadResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{65} + return file_iac_proto_rawDescGZIP(), []int{67} } func (x *ResourceReadResponse) GetOutput() *ResourceOutput { @@ -3995,7 +4097,7 @@ type ResourceUpdateRequest struct { func (x *ResourceUpdateRequest) Reset() { *x = ResourceUpdateRequest{} - mi := &file_iac_proto_msgTypes[66] + mi := &file_iac_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4007,7 +4109,7 @@ func (x *ResourceUpdateRequest) String() string { func (*ResourceUpdateRequest) ProtoMessage() {} func (x *ResourceUpdateRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[66] + mi := &file_iac_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4020,7 +4122,7 @@ func (x *ResourceUpdateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceUpdateRequest.ProtoReflect.Descriptor instead. func (*ResourceUpdateRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{66} + return file_iac_proto_rawDescGZIP(), []int{68} } func (x *ResourceUpdateRequest) GetResourceType() string { @@ -4053,7 +4155,7 @@ type ResourceUpdateResponse struct { func (x *ResourceUpdateResponse) Reset() { *x = ResourceUpdateResponse{} - mi := &file_iac_proto_msgTypes[67] + mi := &file_iac_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4065,7 +4167,7 @@ func (x *ResourceUpdateResponse) String() string { func (*ResourceUpdateResponse) ProtoMessage() {} func (x *ResourceUpdateResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[67] + mi := &file_iac_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4078,7 +4180,7 @@ func (x *ResourceUpdateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceUpdateResponse.ProtoReflect.Descriptor instead. func (*ResourceUpdateResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{67} + return file_iac_proto_rawDescGZIP(), []int{69} } func (x *ResourceUpdateResponse) GetOutput() *ResourceOutput { @@ -4098,7 +4200,7 @@ type ResourceDeleteRequest struct { func (x *ResourceDeleteRequest) Reset() { *x = ResourceDeleteRequest{} - mi := &file_iac_proto_msgTypes[68] + mi := &file_iac_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4110,7 +4212,7 @@ func (x *ResourceDeleteRequest) String() string { func (*ResourceDeleteRequest) ProtoMessage() {} func (x *ResourceDeleteRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[68] + mi := &file_iac_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4123,7 +4225,7 @@ func (x *ResourceDeleteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceDeleteRequest.ProtoReflect.Descriptor instead. func (*ResourceDeleteRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{68} + return file_iac_proto_rawDescGZIP(), []int{70} } func (x *ResourceDeleteRequest) GetResourceType() string { @@ -4148,7 +4250,7 @@ type ResourceDeleteResponse struct { func (x *ResourceDeleteResponse) Reset() { *x = ResourceDeleteResponse{} - mi := &file_iac_proto_msgTypes[69] + mi := &file_iac_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4160,7 +4262,7 @@ func (x *ResourceDeleteResponse) String() string { func (*ResourceDeleteResponse) ProtoMessage() {} func (x *ResourceDeleteResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[69] + mi := &file_iac_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4173,7 +4275,7 @@ func (x *ResourceDeleteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceDeleteResponse.ProtoReflect.Descriptor instead. func (*ResourceDeleteResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{69} + return file_iac_proto_rawDescGZIP(), []int{71} } type ResourceDiffRequest struct { @@ -4187,7 +4289,7 @@ type ResourceDiffRequest struct { func (x *ResourceDiffRequest) Reset() { *x = ResourceDiffRequest{} - mi := &file_iac_proto_msgTypes[70] + mi := &file_iac_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4199,7 +4301,7 @@ func (x *ResourceDiffRequest) String() string { func (*ResourceDiffRequest) ProtoMessage() {} func (x *ResourceDiffRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[70] + mi := &file_iac_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4212,7 +4314,7 @@ func (x *ResourceDiffRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceDiffRequest.ProtoReflect.Descriptor instead. func (*ResourceDiffRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{70} + return file_iac_proto_rawDescGZIP(), []int{72} } func (x *ResourceDiffRequest) GetResourceType() string { @@ -4245,7 +4347,7 @@ type ResourceDiffResponse struct { func (x *ResourceDiffResponse) Reset() { *x = ResourceDiffResponse{} - mi := &file_iac_proto_msgTypes[71] + mi := &file_iac_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4257,7 +4359,7 @@ func (x *ResourceDiffResponse) String() string { func (*ResourceDiffResponse) ProtoMessage() {} func (x *ResourceDiffResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[71] + mi := &file_iac_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4270,7 +4372,7 @@ func (x *ResourceDiffResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceDiffResponse.ProtoReflect.Descriptor instead. func (*ResourceDiffResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{71} + return file_iac_proto_rawDescGZIP(), []int{73} } func (x *ResourceDiffResponse) GetResult() *DiffResult { @@ -4291,7 +4393,7 @@ type ResourceScaleRequest struct { func (x *ResourceScaleRequest) Reset() { *x = ResourceScaleRequest{} - mi := &file_iac_proto_msgTypes[72] + mi := &file_iac_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4303,7 +4405,7 @@ func (x *ResourceScaleRequest) String() string { func (*ResourceScaleRequest) ProtoMessage() {} func (x *ResourceScaleRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[72] + mi := &file_iac_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4316,7 +4418,7 @@ func (x *ResourceScaleRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceScaleRequest.ProtoReflect.Descriptor instead. func (*ResourceScaleRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{72} + return file_iac_proto_rawDescGZIP(), []int{74} } func (x *ResourceScaleRequest) GetResourceType() string { @@ -4349,7 +4451,7 @@ type ResourceScaleResponse struct { func (x *ResourceScaleResponse) Reset() { *x = ResourceScaleResponse{} - mi := &file_iac_proto_msgTypes[73] + mi := &file_iac_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4361,7 +4463,7 @@ func (x *ResourceScaleResponse) String() string { func (*ResourceScaleResponse) ProtoMessage() {} func (x *ResourceScaleResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[73] + mi := &file_iac_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4374,7 +4476,7 @@ func (x *ResourceScaleResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceScaleResponse.ProtoReflect.Descriptor instead. func (*ResourceScaleResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{73} + return file_iac_proto_rawDescGZIP(), []int{75} } func (x *ResourceScaleResponse) GetOutput() *ResourceOutput { @@ -4394,7 +4496,7 @@ type ResourceHealthCheckRequest struct { func (x *ResourceHealthCheckRequest) Reset() { *x = ResourceHealthCheckRequest{} - mi := &file_iac_proto_msgTypes[74] + mi := &file_iac_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4406,7 +4508,7 @@ func (x *ResourceHealthCheckRequest) String() string { func (*ResourceHealthCheckRequest) ProtoMessage() {} func (x *ResourceHealthCheckRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[74] + mi := &file_iac_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4419,7 +4521,7 @@ func (x *ResourceHealthCheckRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceHealthCheckRequest.ProtoReflect.Descriptor instead. func (*ResourceHealthCheckRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{74} + return file_iac_proto_rawDescGZIP(), []int{76} } func (x *ResourceHealthCheckRequest) GetResourceType() string { @@ -4445,7 +4547,7 @@ type ResourceHealthCheckResponse struct { func (x *ResourceHealthCheckResponse) Reset() { *x = ResourceHealthCheckResponse{} - mi := &file_iac_proto_msgTypes[75] + mi := &file_iac_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4457,7 +4559,7 @@ func (x *ResourceHealthCheckResponse) String() string { func (*ResourceHealthCheckResponse) ProtoMessage() {} func (x *ResourceHealthCheckResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[75] + mi := &file_iac_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4470,7 +4572,7 @@ func (x *ResourceHealthCheckResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceHealthCheckResponse.ProtoReflect.Descriptor instead. func (*ResourceHealthCheckResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{75} + return file_iac_proto_rawDescGZIP(), []int{77} } func (x *ResourceHealthCheckResponse) GetResult() *HealthResult { @@ -4489,7 +4591,7 @@ type SensitiveKeysRequest struct { func (x *SensitiveKeysRequest) Reset() { *x = SensitiveKeysRequest{} - mi := &file_iac_proto_msgTypes[76] + mi := &file_iac_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4501,7 +4603,7 @@ func (x *SensitiveKeysRequest) String() string { func (*SensitiveKeysRequest) ProtoMessage() {} func (x *SensitiveKeysRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[76] + mi := &file_iac_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4514,7 +4616,7 @@ func (x *SensitiveKeysRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SensitiveKeysRequest.ProtoReflect.Descriptor instead. func (*SensitiveKeysRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{76} + return file_iac_proto_rawDescGZIP(), []int{78} } func (x *SensitiveKeysRequest) GetResourceType() string { @@ -4533,7 +4635,7 @@ type SensitiveKeysResponse struct { func (x *SensitiveKeysResponse) Reset() { *x = SensitiveKeysResponse{} - mi := &file_iac_proto_msgTypes[77] + mi := &file_iac_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4545,7 +4647,7 @@ func (x *SensitiveKeysResponse) String() string { func (*SensitiveKeysResponse) ProtoMessage() {} func (x *SensitiveKeysResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[77] + mi := &file_iac_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4558,7 +4660,7 @@ func (x *SensitiveKeysResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SensitiveKeysResponse.ProtoReflect.Descriptor instead. func (*SensitiveKeysResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{77} + return file_iac_proto_rawDescGZIP(), []int{79} } func (x *SensitiveKeysResponse) GetKeys() []string { @@ -4579,7 +4681,7 @@ type TroubleshootRequest struct { func (x *TroubleshootRequest) Reset() { *x = TroubleshootRequest{} - mi := &file_iac_proto_msgTypes[78] + mi := &file_iac_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4591,7 +4693,7 @@ func (x *TroubleshootRequest) String() string { func (*TroubleshootRequest) ProtoMessage() {} func (x *TroubleshootRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[78] + mi := &file_iac_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4604,7 +4706,7 @@ func (x *TroubleshootRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TroubleshootRequest.ProtoReflect.Descriptor instead. func (*TroubleshootRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{78} + return file_iac_proto_rawDescGZIP(), []int{80} } func (x *TroubleshootRequest) GetResourceType() string { @@ -4637,7 +4739,7 @@ type TroubleshootResponse struct { func (x *TroubleshootResponse) Reset() { *x = TroubleshootResponse{} - mi := &file_iac_proto_msgTypes[79] + mi := &file_iac_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4649,7 +4751,7 @@ func (x *TroubleshootResponse) String() string { func (*TroubleshootResponse) ProtoMessage() {} func (x *TroubleshootResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[79] + mi := &file_iac_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4662,7 +4764,7 @@ func (x *TroubleshootResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TroubleshootResponse.ProtoReflect.Descriptor instead. func (*TroubleshootResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{79} + return file_iac_proto_rawDescGZIP(), []int{81} } func (x *TroubleshootResponse) GetDiagnostics() []*Diagnostic { @@ -4696,7 +4798,7 @@ type IaCState struct { func (x *IaCState) Reset() { *x = IaCState{} - mi := &file_iac_proto_msgTypes[80] + mi := &file_iac_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4708,7 +4810,7 @@ func (x *IaCState) String() string { func (*IaCState) ProtoMessage() {} func (x *IaCState) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[80] + mi := &file_iac_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4721,7 +4823,7 @@ func (x *IaCState) ProtoReflect() protoreflect.Message { // Deprecated: Use IaCState.ProtoReflect.Descriptor instead. func (*IaCState) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{80} + return file_iac_proto_rawDescGZIP(), []int{82} } func (x *IaCState) GetResourceId() string { @@ -4829,7 +4931,7 @@ type ConfigureRequest struct { func (x *ConfigureRequest) Reset() { *x = ConfigureRequest{} - mi := &file_iac_proto_msgTypes[81] + mi := &file_iac_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4841,7 +4943,7 @@ func (x *ConfigureRequest) String() string { func (*ConfigureRequest) ProtoMessage() {} func (x *ConfigureRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[81] + mi := &file_iac_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4854,7 +4956,7 @@ func (x *ConfigureRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureRequest.ProtoReflect.Descriptor instead. func (*ConfigureRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{81} + return file_iac_proto_rawDescGZIP(), []int{83} } func (x *ConfigureRequest) GetBackendName() string { @@ -4879,7 +4981,7 @@ type ConfigureResponse struct { func (x *ConfigureResponse) Reset() { *x = ConfigureResponse{} - mi := &file_iac_proto_msgTypes[82] + mi := &file_iac_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4891,7 +4993,7 @@ func (x *ConfigureResponse) String() string { func (*ConfigureResponse) ProtoMessage() {} func (x *ConfigureResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[82] + mi := &file_iac_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4904,7 +5006,7 @@ func (x *ConfigureResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureResponse.ProtoReflect.Descriptor instead. func (*ConfigureResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{82} + return file_iac_proto_rawDescGZIP(), []int{84} } type GetStateRequest struct { @@ -4916,7 +5018,7 @@ type GetStateRequest struct { func (x *GetStateRequest) Reset() { *x = GetStateRequest{} - mi := &file_iac_proto_msgTypes[83] + mi := &file_iac_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4928,7 +5030,7 @@ func (x *GetStateRequest) String() string { func (*GetStateRequest) ProtoMessage() {} func (x *GetStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[83] + mi := &file_iac_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4941,7 +5043,7 @@ func (x *GetStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetStateRequest.ProtoReflect.Descriptor instead. func (*GetStateRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{83} + return file_iac_proto_rawDescGZIP(), []int{85} } func (x *GetStateRequest) GetResourceId() string { @@ -4961,7 +5063,7 @@ type GetStateResponse struct { func (x *GetStateResponse) Reset() { *x = GetStateResponse{} - mi := &file_iac_proto_msgTypes[84] + mi := &file_iac_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4973,7 +5075,7 @@ func (x *GetStateResponse) String() string { func (*GetStateResponse) ProtoMessage() {} func (x *GetStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[84] + mi := &file_iac_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4986,7 +5088,7 @@ func (x *GetStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetStateResponse.ProtoReflect.Descriptor instead. func (*GetStateResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{84} + return file_iac_proto_rawDescGZIP(), []int{86} } func (x *GetStateResponse) GetState() *IaCState { @@ -5012,7 +5114,7 @@ type SaveStateRequest struct { func (x *SaveStateRequest) Reset() { *x = SaveStateRequest{} - mi := &file_iac_proto_msgTypes[85] + mi := &file_iac_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5024,7 +5126,7 @@ func (x *SaveStateRequest) String() string { func (*SaveStateRequest) ProtoMessage() {} func (x *SaveStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[85] + mi := &file_iac_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5037,7 +5139,7 @@ func (x *SaveStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SaveStateRequest.ProtoReflect.Descriptor instead. func (*SaveStateRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{85} + return file_iac_proto_rawDescGZIP(), []int{87} } func (x *SaveStateRequest) GetState() *IaCState { @@ -5055,7 +5157,7 @@ type SaveStateResponse struct { func (x *SaveStateResponse) Reset() { *x = SaveStateResponse{} - mi := &file_iac_proto_msgTypes[86] + mi := &file_iac_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5067,7 +5169,7 @@ func (x *SaveStateResponse) String() string { func (*SaveStateResponse) ProtoMessage() {} func (x *SaveStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[86] + mi := &file_iac_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5080,7 +5182,7 @@ func (x *SaveStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SaveStateResponse.ProtoReflect.Descriptor instead. func (*SaveStateResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{86} + return file_iac_proto_rawDescGZIP(), []int{88} } type ListStatesRequest struct { @@ -5092,7 +5194,7 @@ type ListStatesRequest struct { func (x *ListStatesRequest) Reset() { *x = ListStatesRequest{} - mi := &file_iac_proto_msgTypes[87] + mi := &file_iac_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5104,7 +5206,7 @@ func (x *ListStatesRequest) String() string { func (*ListStatesRequest) ProtoMessage() {} func (x *ListStatesRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[87] + mi := &file_iac_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5117,7 +5219,7 @@ func (x *ListStatesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStatesRequest.ProtoReflect.Descriptor instead. func (*ListStatesRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{87} + return file_iac_proto_rawDescGZIP(), []int{89} } func (x *ListStatesRequest) GetFilter() map[string]string { @@ -5136,7 +5238,7 @@ type ListStatesResponse struct { func (x *ListStatesResponse) Reset() { *x = ListStatesResponse{} - mi := &file_iac_proto_msgTypes[88] + mi := &file_iac_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5148,7 +5250,7 @@ func (x *ListStatesResponse) String() string { func (*ListStatesResponse) ProtoMessage() {} func (x *ListStatesResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[88] + mi := &file_iac_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5161,7 +5263,7 @@ func (x *ListStatesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStatesResponse.ProtoReflect.Descriptor instead. func (*ListStatesResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{88} + return file_iac_proto_rawDescGZIP(), []int{90} } func (x *ListStatesResponse) GetStates() []*IaCState { @@ -5180,7 +5282,7 @@ type DeleteStateRequest struct { func (x *DeleteStateRequest) Reset() { *x = DeleteStateRequest{} - mi := &file_iac_proto_msgTypes[89] + mi := &file_iac_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5192,7 +5294,7 @@ func (x *DeleteStateRequest) String() string { func (*DeleteStateRequest) ProtoMessage() {} func (x *DeleteStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[89] + mi := &file_iac_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5205,7 +5307,7 @@ func (x *DeleteStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteStateRequest.ProtoReflect.Descriptor instead. func (*DeleteStateRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{89} + return file_iac_proto_rawDescGZIP(), []int{91} } func (x *DeleteStateRequest) GetResourceId() string { @@ -5223,7 +5325,7 @@ type DeleteStateResponse struct { func (x *DeleteStateResponse) Reset() { *x = DeleteStateResponse{} - mi := &file_iac_proto_msgTypes[90] + mi := &file_iac_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5235,7 +5337,7 @@ func (x *DeleteStateResponse) String() string { func (*DeleteStateResponse) ProtoMessage() {} func (x *DeleteStateResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[90] + mi := &file_iac_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5248,7 +5350,7 @@ func (x *DeleteStateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteStateResponse.ProtoReflect.Descriptor instead. func (*DeleteStateResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{90} + return file_iac_proto_rawDescGZIP(), []int{92} } type LockRequest struct { @@ -5260,7 +5362,7 @@ type LockRequest struct { func (x *LockRequest) Reset() { *x = LockRequest{} - mi := &file_iac_proto_msgTypes[91] + mi := &file_iac_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5272,7 +5374,7 @@ func (x *LockRequest) String() string { func (*LockRequest) ProtoMessage() {} func (x *LockRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[91] + mi := &file_iac_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5285,7 +5387,7 @@ func (x *LockRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LockRequest.ProtoReflect.Descriptor instead. func (*LockRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{91} + return file_iac_proto_rawDescGZIP(), []int{93} } func (x *LockRequest) GetResourceId() string { @@ -5303,7 +5405,7 @@ type LockResponse struct { func (x *LockResponse) Reset() { *x = LockResponse{} - mi := &file_iac_proto_msgTypes[92] + mi := &file_iac_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5315,7 +5417,7 @@ func (x *LockResponse) String() string { func (*LockResponse) ProtoMessage() {} func (x *LockResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[92] + mi := &file_iac_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5328,7 +5430,7 @@ func (x *LockResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LockResponse.ProtoReflect.Descriptor instead. func (*LockResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{92} + return file_iac_proto_rawDescGZIP(), []int{94} } type UnlockRequest struct { @@ -5340,7 +5442,7 @@ type UnlockRequest struct { func (x *UnlockRequest) Reset() { *x = UnlockRequest{} - mi := &file_iac_proto_msgTypes[93] + mi := &file_iac_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5352,7 +5454,7 @@ func (x *UnlockRequest) String() string { func (*UnlockRequest) ProtoMessage() {} func (x *UnlockRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[93] + mi := &file_iac_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5365,7 +5467,7 @@ func (x *UnlockRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UnlockRequest.ProtoReflect.Descriptor instead. func (*UnlockRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{93} + return file_iac_proto_rawDescGZIP(), []int{95} } func (x *UnlockRequest) GetResourceId() string { @@ -5383,7 +5485,7 @@ type UnlockResponse struct { func (x *UnlockResponse) Reset() { *x = UnlockResponse{} - mi := &file_iac_proto_msgTypes[94] + mi := &file_iac_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5395,7 +5497,7 @@ func (x *UnlockResponse) String() string { func (*UnlockResponse) ProtoMessage() {} func (x *UnlockResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[94] + mi := &file_iac_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5408,7 +5510,7 @@ func (x *UnlockResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UnlockResponse.ProtoReflect.Descriptor instead. func (*UnlockResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{94} + return file_iac_proto_rawDescGZIP(), []int{96} } // ListBackendNames lets the engine ask a loaded plugin which iac.state backend @@ -5422,7 +5524,7 @@ type ListBackendNamesRequest struct { func (x *ListBackendNamesRequest) Reset() { *x = ListBackendNamesRequest{} - mi := &file_iac_proto_msgTypes[95] + mi := &file_iac_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5434,7 +5536,7 @@ func (x *ListBackendNamesRequest) String() string { func (*ListBackendNamesRequest) ProtoMessage() {} func (x *ListBackendNamesRequest) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[95] + mi := &file_iac_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5447,7 +5549,7 @@ func (x *ListBackendNamesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListBackendNamesRequest.ProtoReflect.Descriptor instead. func (*ListBackendNamesRequest) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{95} + return file_iac_proto_rawDescGZIP(), []int{97} } type ListBackendNamesResponse struct { @@ -5459,7 +5561,7 @@ type ListBackendNamesResponse struct { func (x *ListBackendNamesResponse) Reset() { *x = ListBackendNamesResponse{} - mi := &file_iac_proto_msgTypes[96] + mi := &file_iac_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5471,7 +5573,7 @@ func (x *ListBackendNamesResponse) String() string { func (*ListBackendNamesResponse) ProtoMessage() {} func (x *ListBackendNamesResponse) ProtoReflect() protoreflect.Message { - mi := &file_iac_proto_msgTypes[96] + mi := &file_iac_proto_msgTypes[98] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5484,7 +5586,7 @@ func (x *ListBackendNamesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListBackendNamesResponse.ProtoReflect.Descriptor instead. func (*ListBackendNamesResponse) Descriptor() ([]byte, []int) { - return file_iac_proto_rawDescGZIP(), []int{96} + return file_iac_proto_rawDescGZIP(), []int{98} } func (x *ListBackendNamesResponse) GetBackendNames() []string { @@ -5751,7 +5853,11 @@ const file_iac_proto_rawDesc = "" + "\x1fRevokeProviderCredentialRequest\x12\x16\n" + "\x06source\x18\x01 \x01(\tR\x06source\x12#\n" + "\rcredential_id\x18\x02 \x01(\tR\fcredentialId\"\"\n" + - " RevokeProviderCredentialResponse\"m\n" + + " RevokeProviderCredentialResponse\"/\n" + + "\x14FinalizeApplyRequest\x12\x17\n" + + "\aplan_id\x18\x01 \x01(\tR\x06planId\"Z\n" + + "\x15FinalizeApplyResponse\x12A\n" + + "\x06errors\x18\x01 \x03(\v2).workflow.plugin.external.iac.ActionErrorR\x06errors\"m\n" + "\x1bRepairDirtyMigrationRequest\x12N\n" + "\arequest\x18\x01 \x01(\v24.workflow.plugin.external.iac.MigrationRepairRequestR\arequest\"k\n" + "\x1cRepairDirtyMigrationResponse\x12K\n" + @@ -5909,7 +6015,9 @@ const file_iac_proto_rawDesc = "" + "\vDetectDrift\x120.workflow.plugin.external.iac.DetectDriftRequest\x1a1.workflow.plugin.external.iac.DetectDriftResponse\x12\x8d\x01\n" + "\x14DetectDriftWithSpecs\x129.workflow.plugin.external.iac.DetectDriftWithSpecsRequest\x1a:.workflow.plugin.external.iac.DetectDriftWithSpecsResponse2\xba\x01\n" + "\x1cIaCProviderCredentialRevoker\x12\x99\x01\n" + - "\x18RevokeProviderCredential\x12=.workflow.plugin.external.iac.RevokeProviderCredentialRequest\x1a>.workflow.plugin.external.iac.RevokeProviderCredentialResponse2\xae\x01\n" + + "\x18RevokeProviderCredential\x12=.workflow.plugin.external.iac.RevokeProviderCredentialRequest\x1a>.workflow.plugin.external.iac.RevokeProviderCredentialResponse2\x90\x01\n" + + "\x14IaCProviderFinalizer\x12x\n" + + "\rFinalizeApply\x122.workflow.plugin.external.iac.FinalizeApplyRequest\x1a3.workflow.plugin.external.iac.FinalizeApplyResponse2\xae\x01\n" + "\x1cIaCProviderMigrationRepairer\x12\x8d\x01\n" + "\x14RepairDirtyMigration\x129.workflow.plugin.external.iac.RepairDirtyMigrationRequest\x1a:.workflow.plugin.external.iac.RepairDirtyMigrationResponse2\x8d\x01\n" + "\x14IaCProviderValidator\x12u\n" + @@ -5950,7 +6058,7 @@ func file_iac_proto_rawDescGZIP() []byte { } var file_iac_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_iac_proto_msgTypes = make([]protoimpl.MessageInfo, 106) +var file_iac_proto_msgTypes = make([]protoimpl.MessageInfo, 108) var file_iac_proto_goTypes = []any{ (DriftClass)(0), // 0: workflow.plugin.external.iac.DriftClass (PlanDiagnosticSeverity)(0), // 1: workflow.plugin.external.iac.PlanDiagnosticSeverity @@ -6011,84 +6119,86 @@ var file_iac_proto_goTypes = []any{ (*DetectDriftWithSpecsResponse)(nil), // 56: workflow.plugin.external.iac.DetectDriftWithSpecsResponse (*RevokeProviderCredentialRequest)(nil), // 57: workflow.plugin.external.iac.RevokeProviderCredentialRequest (*RevokeProviderCredentialResponse)(nil), // 58: workflow.plugin.external.iac.RevokeProviderCredentialResponse - (*RepairDirtyMigrationRequest)(nil), // 59: workflow.plugin.external.iac.RepairDirtyMigrationRequest - (*RepairDirtyMigrationResponse)(nil), // 60: workflow.plugin.external.iac.RepairDirtyMigrationResponse - (*ValidatePlanRequest)(nil), // 61: workflow.plugin.external.iac.ValidatePlanRequest - (*ValidatePlanResponse)(nil), // 62: workflow.plugin.external.iac.ValidatePlanResponse - (*DetectDriftConfigRequest)(nil), // 63: workflow.plugin.external.iac.DetectDriftConfigRequest - (*DetectDriftConfigResponse)(nil), // 64: workflow.plugin.external.iac.DetectDriftConfigResponse - (*ResourceCreateRequest)(nil), // 65: workflow.plugin.external.iac.ResourceCreateRequest - (*ResourceCreateResponse)(nil), // 66: workflow.plugin.external.iac.ResourceCreateResponse - (*ResourceReadRequest)(nil), // 67: workflow.plugin.external.iac.ResourceReadRequest - (*ResourceReadResponse)(nil), // 68: workflow.plugin.external.iac.ResourceReadResponse - (*ResourceUpdateRequest)(nil), // 69: workflow.plugin.external.iac.ResourceUpdateRequest - (*ResourceUpdateResponse)(nil), // 70: workflow.plugin.external.iac.ResourceUpdateResponse - (*ResourceDeleteRequest)(nil), // 71: workflow.plugin.external.iac.ResourceDeleteRequest - (*ResourceDeleteResponse)(nil), // 72: workflow.plugin.external.iac.ResourceDeleteResponse - (*ResourceDiffRequest)(nil), // 73: workflow.plugin.external.iac.ResourceDiffRequest - (*ResourceDiffResponse)(nil), // 74: workflow.plugin.external.iac.ResourceDiffResponse - (*ResourceScaleRequest)(nil), // 75: workflow.plugin.external.iac.ResourceScaleRequest - (*ResourceScaleResponse)(nil), // 76: workflow.plugin.external.iac.ResourceScaleResponse - (*ResourceHealthCheckRequest)(nil), // 77: workflow.plugin.external.iac.ResourceHealthCheckRequest - (*ResourceHealthCheckResponse)(nil), // 78: workflow.plugin.external.iac.ResourceHealthCheckResponse - (*SensitiveKeysRequest)(nil), // 79: workflow.plugin.external.iac.SensitiveKeysRequest - (*SensitiveKeysResponse)(nil), // 80: workflow.plugin.external.iac.SensitiveKeysResponse - (*TroubleshootRequest)(nil), // 81: workflow.plugin.external.iac.TroubleshootRequest - (*TroubleshootResponse)(nil), // 82: workflow.plugin.external.iac.TroubleshootResponse - (*IaCState)(nil), // 83: workflow.plugin.external.iac.IaCState - (*ConfigureRequest)(nil), // 84: workflow.plugin.external.iac.ConfigureRequest - (*ConfigureResponse)(nil), // 85: workflow.plugin.external.iac.ConfigureResponse - (*GetStateRequest)(nil), // 86: workflow.plugin.external.iac.GetStateRequest - (*GetStateResponse)(nil), // 87: workflow.plugin.external.iac.GetStateResponse - (*SaveStateRequest)(nil), // 88: workflow.plugin.external.iac.SaveStateRequest - (*SaveStateResponse)(nil), // 89: workflow.plugin.external.iac.SaveStateResponse - (*ListStatesRequest)(nil), // 90: workflow.plugin.external.iac.ListStatesRequest - (*ListStatesResponse)(nil), // 91: workflow.plugin.external.iac.ListStatesResponse - (*DeleteStateRequest)(nil), // 92: workflow.plugin.external.iac.DeleteStateRequest - (*DeleteStateResponse)(nil), // 93: workflow.plugin.external.iac.DeleteStateResponse - (*LockRequest)(nil), // 94: workflow.plugin.external.iac.LockRequest - (*LockResponse)(nil), // 95: workflow.plugin.external.iac.LockResponse - (*UnlockRequest)(nil), // 96: workflow.plugin.external.iac.UnlockRequest - (*UnlockResponse)(nil), // 97: workflow.plugin.external.iac.UnlockResponse - (*ListBackendNamesRequest)(nil), // 98: workflow.plugin.external.iac.ListBackendNamesRequest - (*ListBackendNamesResponse)(nil), // 99: workflow.plugin.external.iac.ListBackendNamesResponse - nil, // 100: workflow.plugin.external.iac.ResourceOutput.SensitiveEntry - nil, // 101: workflow.plugin.external.iac.IaCPlan.InputSnapshotEntry - nil, // 102: workflow.plugin.external.iac.ApplyResult.InitialInputSnapshotEntry - nil, // 103: workflow.plugin.external.iac.ApplyResult.ReplaceIdMapEntry - nil, // 104: workflow.plugin.external.iac.BootstrapResult.EnvVarsEntry - nil, // 105: workflow.plugin.external.iac.MigrationRepairRequest.EnvEntry - nil, // 106: workflow.plugin.external.iac.DetectDriftWithSpecsRequest.SpecsEntry - nil, // 107: workflow.plugin.external.iac.DetectDriftConfigRequest.SpecsEntry - nil, // 108: workflow.plugin.external.iac.ListStatesRequest.FilterEntry - (*timestamppb.Timestamp)(nil), // 109: google.protobuf.Timestamp + (*FinalizeApplyRequest)(nil), // 59: workflow.plugin.external.iac.FinalizeApplyRequest + (*FinalizeApplyResponse)(nil), // 60: workflow.plugin.external.iac.FinalizeApplyResponse + (*RepairDirtyMigrationRequest)(nil), // 61: workflow.plugin.external.iac.RepairDirtyMigrationRequest + (*RepairDirtyMigrationResponse)(nil), // 62: workflow.plugin.external.iac.RepairDirtyMigrationResponse + (*ValidatePlanRequest)(nil), // 63: workflow.plugin.external.iac.ValidatePlanRequest + (*ValidatePlanResponse)(nil), // 64: workflow.plugin.external.iac.ValidatePlanResponse + (*DetectDriftConfigRequest)(nil), // 65: workflow.plugin.external.iac.DetectDriftConfigRequest + (*DetectDriftConfigResponse)(nil), // 66: workflow.plugin.external.iac.DetectDriftConfigResponse + (*ResourceCreateRequest)(nil), // 67: workflow.plugin.external.iac.ResourceCreateRequest + (*ResourceCreateResponse)(nil), // 68: workflow.plugin.external.iac.ResourceCreateResponse + (*ResourceReadRequest)(nil), // 69: workflow.plugin.external.iac.ResourceReadRequest + (*ResourceReadResponse)(nil), // 70: workflow.plugin.external.iac.ResourceReadResponse + (*ResourceUpdateRequest)(nil), // 71: workflow.plugin.external.iac.ResourceUpdateRequest + (*ResourceUpdateResponse)(nil), // 72: workflow.plugin.external.iac.ResourceUpdateResponse + (*ResourceDeleteRequest)(nil), // 73: workflow.plugin.external.iac.ResourceDeleteRequest + (*ResourceDeleteResponse)(nil), // 74: workflow.plugin.external.iac.ResourceDeleteResponse + (*ResourceDiffRequest)(nil), // 75: workflow.plugin.external.iac.ResourceDiffRequest + (*ResourceDiffResponse)(nil), // 76: workflow.plugin.external.iac.ResourceDiffResponse + (*ResourceScaleRequest)(nil), // 77: workflow.plugin.external.iac.ResourceScaleRequest + (*ResourceScaleResponse)(nil), // 78: workflow.plugin.external.iac.ResourceScaleResponse + (*ResourceHealthCheckRequest)(nil), // 79: workflow.plugin.external.iac.ResourceHealthCheckRequest + (*ResourceHealthCheckResponse)(nil), // 80: workflow.plugin.external.iac.ResourceHealthCheckResponse + (*SensitiveKeysRequest)(nil), // 81: workflow.plugin.external.iac.SensitiveKeysRequest + (*SensitiveKeysResponse)(nil), // 82: workflow.plugin.external.iac.SensitiveKeysResponse + (*TroubleshootRequest)(nil), // 83: workflow.plugin.external.iac.TroubleshootRequest + (*TroubleshootResponse)(nil), // 84: workflow.plugin.external.iac.TroubleshootResponse + (*IaCState)(nil), // 85: workflow.plugin.external.iac.IaCState + (*ConfigureRequest)(nil), // 86: workflow.plugin.external.iac.ConfigureRequest + (*ConfigureResponse)(nil), // 87: workflow.plugin.external.iac.ConfigureResponse + (*GetStateRequest)(nil), // 88: workflow.plugin.external.iac.GetStateRequest + (*GetStateResponse)(nil), // 89: workflow.plugin.external.iac.GetStateResponse + (*SaveStateRequest)(nil), // 90: workflow.plugin.external.iac.SaveStateRequest + (*SaveStateResponse)(nil), // 91: workflow.plugin.external.iac.SaveStateResponse + (*ListStatesRequest)(nil), // 92: workflow.plugin.external.iac.ListStatesRequest + (*ListStatesResponse)(nil), // 93: workflow.plugin.external.iac.ListStatesResponse + (*DeleteStateRequest)(nil), // 94: workflow.plugin.external.iac.DeleteStateRequest + (*DeleteStateResponse)(nil), // 95: workflow.plugin.external.iac.DeleteStateResponse + (*LockRequest)(nil), // 96: workflow.plugin.external.iac.LockRequest + (*LockResponse)(nil), // 97: workflow.plugin.external.iac.LockResponse + (*UnlockRequest)(nil), // 98: workflow.plugin.external.iac.UnlockRequest + (*UnlockResponse)(nil), // 99: workflow.plugin.external.iac.UnlockResponse + (*ListBackendNamesRequest)(nil), // 100: workflow.plugin.external.iac.ListBackendNamesRequest + (*ListBackendNamesResponse)(nil), // 101: workflow.plugin.external.iac.ListBackendNamesResponse + nil, // 102: workflow.plugin.external.iac.ResourceOutput.SensitiveEntry + nil, // 103: workflow.plugin.external.iac.IaCPlan.InputSnapshotEntry + nil, // 104: workflow.plugin.external.iac.ApplyResult.InitialInputSnapshotEntry + nil, // 105: workflow.plugin.external.iac.ApplyResult.ReplaceIdMapEntry + nil, // 106: workflow.plugin.external.iac.BootstrapResult.EnvVarsEntry + nil, // 107: workflow.plugin.external.iac.MigrationRepairRequest.EnvEntry + nil, // 108: workflow.plugin.external.iac.DetectDriftWithSpecsRequest.SpecsEntry + nil, // 109: workflow.plugin.external.iac.DetectDriftConfigRequest.SpecsEntry + nil, // 110: workflow.plugin.external.iac.ListStatesRequest.FilterEntry + (*timestamppb.Timestamp)(nil), // 111: google.protobuf.Timestamp } var file_iac_proto_depIdxs = []int32{ 5, // 0: workflow.plugin.external.iac.ResourceSpec.hints:type_name -> workflow.plugin.external.iac.ResourceHints - 109, // 1: workflow.plugin.external.iac.ResourceState.created_at:type_name -> google.protobuf.Timestamp - 109, // 2: workflow.plugin.external.iac.ResourceState.updated_at:type_name -> google.protobuf.Timestamp - 109, // 3: workflow.plugin.external.iac.ResourceState.last_drift_check:type_name -> google.protobuf.Timestamp - 100, // 4: workflow.plugin.external.iac.ResourceOutput.sensitive:type_name -> workflow.plugin.external.iac.ResourceOutput.SensitiveEntry + 111, // 1: workflow.plugin.external.iac.ResourceState.created_at:type_name -> google.protobuf.Timestamp + 111, // 2: workflow.plugin.external.iac.ResourceState.updated_at:type_name -> google.protobuf.Timestamp + 111, // 3: workflow.plugin.external.iac.ResourceState.last_drift_check:type_name -> google.protobuf.Timestamp + 102, // 4: workflow.plugin.external.iac.ResourceOutput.sensitive:type_name -> workflow.plugin.external.iac.ResourceOutput.SensitiveEntry 11, // 5: workflow.plugin.external.iac.DiffResult.changes:type_name -> workflow.plugin.external.iac.FieldChange 0, // 6: workflow.plugin.external.iac.DriftResult.class:type_name -> workflow.plugin.external.iac.DriftClass - 109, // 7: workflow.plugin.external.iac.Diagnostic.at:type_name -> google.protobuf.Timestamp + 111, // 7: workflow.plugin.external.iac.Diagnostic.at:type_name -> google.protobuf.Timestamp 1, // 8: workflow.plugin.external.iac.PlanDiagnostic.severity:type_name -> workflow.plugin.external.iac.PlanDiagnosticSeverity 3, // 9: workflow.plugin.external.iac.PlanAction.resource:type_name -> workflow.plugin.external.iac.ResourceSpec 8, // 10: workflow.plugin.external.iac.PlanAction.current:type_name -> workflow.plugin.external.iac.ResourceState 11, // 11: workflow.plugin.external.iac.PlanAction.changes:type_name -> workflow.plugin.external.iac.FieldChange 18, // 12: workflow.plugin.external.iac.IaCPlan.actions:type_name -> workflow.plugin.external.iac.PlanAction - 109, // 13: workflow.plugin.external.iac.IaCPlan.created_at:type_name -> google.protobuf.Timestamp - 101, // 14: workflow.plugin.external.iac.IaCPlan.input_snapshot:type_name -> workflow.plugin.external.iac.IaCPlan.InputSnapshotEntry + 111, // 13: workflow.plugin.external.iac.IaCPlan.created_at:type_name -> google.protobuf.Timestamp + 103, // 14: workflow.plugin.external.iac.IaCPlan.input_snapshot:type_name -> workflow.plugin.external.iac.IaCPlan.InputSnapshotEntry 2, // 15: workflow.plugin.external.iac.ActionResult.status:type_name -> workflow.plugin.external.iac.ActionStatus 9, // 16: workflow.plugin.external.iac.ApplyResult.resources:type_name -> workflow.plugin.external.iac.ResourceOutput 20, // 17: workflow.plugin.external.iac.ApplyResult.errors:type_name -> workflow.plugin.external.iac.ActionError - 102, // 18: workflow.plugin.external.iac.ApplyResult.initial_input_snapshot:type_name -> workflow.plugin.external.iac.ApplyResult.InitialInputSnapshotEntry + 104, // 18: workflow.plugin.external.iac.ApplyResult.initial_input_snapshot:type_name -> workflow.plugin.external.iac.ApplyResult.InitialInputSnapshotEntry 14, // 19: workflow.plugin.external.iac.ApplyResult.input_drift_report:type_name -> workflow.plugin.external.iac.DriftEntry - 103, // 20: workflow.plugin.external.iac.ApplyResult.replace_id_map:type_name -> workflow.plugin.external.iac.ApplyResult.ReplaceIdMapEntry + 105, // 20: workflow.plugin.external.iac.ApplyResult.replace_id_map:type_name -> workflow.plugin.external.iac.ApplyResult.ReplaceIdMapEntry 21, // 21: workflow.plugin.external.iac.ApplyResult.actions:type_name -> workflow.plugin.external.iac.ActionResult 20, // 22: workflow.plugin.external.iac.DestroyResult.errors:type_name -> workflow.plugin.external.iac.ActionError - 104, // 23: workflow.plugin.external.iac.BootstrapResult.env_vars:type_name -> workflow.plugin.external.iac.BootstrapResult.EnvVarsEntry - 105, // 24: workflow.plugin.external.iac.MigrationRepairRequest.env:type_name -> workflow.plugin.external.iac.MigrationRepairRequest.EnvEntry + 106, // 23: workflow.plugin.external.iac.BootstrapResult.env_vars:type_name -> workflow.plugin.external.iac.BootstrapResult.EnvVarsEntry + 107, // 24: workflow.plugin.external.iac.MigrationRepairRequest.env:type_name -> workflow.plugin.external.iac.MigrationRepairRequest.EnvEntry 16, // 25: workflow.plugin.external.iac.MigrationRepairResult.diagnostics:type_name -> workflow.plugin.external.iac.Diagnostic 7, // 26: workflow.plugin.external.iac.CapabilitiesResponse.capabilities:type_name -> workflow.plugin.external.iac.IaCCapabilityDeclaration 3, // 27: workflow.plugin.external.iac.PlanRequest.desired:type_name -> workflow.plugin.external.iac.ResourceSpec @@ -6109,115 +6219,118 @@ var file_iac_proto_depIdxs = []int32{ 4, // 42: workflow.plugin.external.iac.DetectDriftRequest.refs:type_name -> workflow.plugin.external.iac.ResourceRef 13, // 43: workflow.plugin.external.iac.DetectDriftResponse.drifts:type_name -> workflow.plugin.external.iac.DriftResult 4, // 44: workflow.plugin.external.iac.DetectDriftWithSpecsRequest.refs:type_name -> workflow.plugin.external.iac.ResourceRef - 106, // 45: workflow.plugin.external.iac.DetectDriftWithSpecsRequest.specs:type_name -> workflow.plugin.external.iac.DetectDriftWithSpecsRequest.SpecsEntry + 108, // 45: workflow.plugin.external.iac.DetectDriftWithSpecsRequest.specs:type_name -> workflow.plugin.external.iac.DetectDriftWithSpecsRequest.SpecsEntry 13, // 46: workflow.plugin.external.iac.DetectDriftWithSpecsResponse.drifts:type_name -> workflow.plugin.external.iac.DriftResult - 25, // 47: workflow.plugin.external.iac.RepairDirtyMigrationRequest.request:type_name -> workflow.plugin.external.iac.MigrationRepairRequest - 26, // 48: workflow.plugin.external.iac.RepairDirtyMigrationResponse.result:type_name -> workflow.plugin.external.iac.MigrationRepairResult - 19, // 49: workflow.plugin.external.iac.ValidatePlanRequest.plan:type_name -> workflow.plugin.external.iac.IaCPlan - 17, // 50: workflow.plugin.external.iac.ValidatePlanResponse.diagnostics:type_name -> workflow.plugin.external.iac.PlanDiagnostic - 4, // 51: workflow.plugin.external.iac.DetectDriftConfigRequest.refs:type_name -> workflow.plugin.external.iac.ResourceRef - 107, // 52: workflow.plugin.external.iac.DetectDriftConfigRequest.specs:type_name -> workflow.plugin.external.iac.DetectDriftConfigRequest.SpecsEntry - 13, // 53: workflow.plugin.external.iac.DetectDriftConfigResponse.drifts:type_name -> workflow.plugin.external.iac.DriftResult - 3, // 54: workflow.plugin.external.iac.ResourceCreateRequest.spec:type_name -> workflow.plugin.external.iac.ResourceSpec - 9, // 55: workflow.plugin.external.iac.ResourceCreateResponse.output:type_name -> workflow.plugin.external.iac.ResourceOutput - 4, // 56: workflow.plugin.external.iac.ResourceReadRequest.ref:type_name -> workflow.plugin.external.iac.ResourceRef - 9, // 57: workflow.plugin.external.iac.ResourceReadResponse.output:type_name -> workflow.plugin.external.iac.ResourceOutput - 4, // 58: workflow.plugin.external.iac.ResourceUpdateRequest.ref:type_name -> workflow.plugin.external.iac.ResourceRef - 3, // 59: workflow.plugin.external.iac.ResourceUpdateRequest.spec:type_name -> workflow.plugin.external.iac.ResourceSpec - 9, // 60: workflow.plugin.external.iac.ResourceUpdateResponse.output:type_name -> workflow.plugin.external.iac.ResourceOutput - 4, // 61: workflow.plugin.external.iac.ResourceDeleteRequest.ref:type_name -> workflow.plugin.external.iac.ResourceRef - 3, // 62: workflow.plugin.external.iac.ResourceDiffRequest.desired:type_name -> workflow.plugin.external.iac.ResourceSpec - 9, // 63: workflow.plugin.external.iac.ResourceDiffRequest.current:type_name -> workflow.plugin.external.iac.ResourceOutput - 12, // 64: workflow.plugin.external.iac.ResourceDiffResponse.result:type_name -> workflow.plugin.external.iac.DiffResult - 4, // 65: workflow.plugin.external.iac.ResourceScaleRequest.ref:type_name -> workflow.plugin.external.iac.ResourceRef - 9, // 66: workflow.plugin.external.iac.ResourceScaleResponse.output:type_name -> workflow.plugin.external.iac.ResourceOutput - 4, // 67: workflow.plugin.external.iac.ResourceHealthCheckRequest.ref:type_name -> workflow.plugin.external.iac.ResourceRef - 15, // 68: workflow.plugin.external.iac.ResourceHealthCheckResponse.result:type_name -> workflow.plugin.external.iac.HealthResult - 4, // 69: workflow.plugin.external.iac.TroubleshootRequest.ref:type_name -> workflow.plugin.external.iac.ResourceRef - 16, // 70: workflow.plugin.external.iac.TroubleshootResponse.diagnostics:type_name -> workflow.plugin.external.iac.Diagnostic - 83, // 71: workflow.plugin.external.iac.GetStateResponse.state:type_name -> workflow.plugin.external.iac.IaCState - 83, // 72: workflow.plugin.external.iac.SaveStateRequest.state:type_name -> workflow.plugin.external.iac.IaCState - 108, // 73: workflow.plugin.external.iac.ListStatesRequest.filter:type_name -> workflow.plugin.external.iac.ListStatesRequest.FilterEntry - 83, // 74: workflow.plugin.external.iac.ListStatesResponse.states:type_name -> workflow.plugin.external.iac.IaCState - 3, // 75: workflow.plugin.external.iac.DetectDriftWithSpecsRequest.SpecsEntry.value:type_name -> workflow.plugin.external.iac.ResourceSpec - 3, // 76: workflow.plugin.external.iac.DetectDriftConfigRequest.SpecsEntry.value:type_name -> workflow.plugin.external.iac.ResourceSpec - 27, // 77: workflow.plugin.external.iac.IaCProviderRequired.Initialize:input_type -> workflow.plugin.external.iac.InitializeRequest - 29, // 78: workflow.plugin.external.iac.IaCProviderRequired.Name:input_type -> workflow.plugin.external.iac.NameRequest - 31, // 79: workflow.plugin.external.iac.IaCProviderRequired.Version:input_type -> workflow.plugin.external.iac.VersionRequest - 33, // 80: workflow.plugin.external.iac.IaCProviderRequired.Capabilities:input_type -> workflow.plugin.external.iac.CapabilitiesRequest - 35, // 81: workflow.plugin.external.iac.IaCProviderRequired.Plan:input_type -> workflow.plugin.external.iac.PlanRequest - 37, // 82: workflow.plugin.external.iac.IaCProviderRequired.Apply:input_type -> workflow.plugin.external.iac.ApplyRequest - 39, // 83: workflow.plugin.external.iac.IaCProviderRequired.Destroy:input_type -> workflow.plugin.external.iac.DestroyRequest - 41, // 84: workflow.plugin.external.iac.IaCProviderRequired.Status:input_type -> workflow.plugin.external.iac.StatusRequest - 43, // 85: workflow.plugin.external.iac.IaCProviderRequired.Import:input_type -> workflow.plugin.external.iac.ImportRequest - 45, // 86: workflow.plugin.external.iac.IaCProviderRequired.ResolveSizing:input_type -> workflow.plugin.external.iac.ResolveSizingRequest - 47, // 87: workflow.plugin.external.iac.IaCProviderRequired.BootstrapStateBackend:input_type -> workflow.plugin.external.iac.BootstrapStateBackendRequest - 49, // 88: workflow.plugin.external.iac.IaCProviderEnumerator.EnumerateAll:input_type -> workflow.plugin.external.iac.EnumerateAllRequest - 51, // 89: workflow.plugin.external.iac.IaCProviderEnumerator.EnumerateByTag:input_type -> workflow.plugin.external.iac.EnumerateByTagRequest - 53, // 90: workflow.plugin.external.iac.IaCProviderDriftDetector.DetectDrift:input_type -> workflow.plugin.external.iac.DetectDriftRequest - 55, // 91: workflow.plugin.external.iac.IaCProviderDriftDetector.DetectDriftWithSpecs:input_type -> workflow.plugin.external.iac.DetectDriftWithSpecsRequest - 57, // 92: workflow.plugin.external.iac.IaCProviderCredentialRevoker.RevokeProviderCredential:input_type -> workflow.plugin.external.iac.RevokeProviderCredentialRequest - 59, // 93: workflow.plugin.external.iac.IaCProviderMigrationRepairer.RepairDirtyMigration:input_type -> workflow.plugin.external.iac.RepairDirtyMigrationRequest - 61, // 94: workflow.plugin.external.iac.IaCProviderValidator.ValidatePlan:input_type -> workflow.plugin.external.iac.ValidatePlanRequest - 63, // 95: workflow.plugin.external.iac.IaCProviderDriftConfigDetector.DetectDriftConfig:input_type -> workflow.plugin.external.iac.DetectDriftConfigRequest - 65, // 96: workflow.plugin.external.iac.ResourceDriver.Create:input_type -> workflow.plugin.external.iac.ResourceCreateRequest - 67, // 97: workflow.plugin.external.iac.ResourceDriver.Read:input_type -> workflow.plugin.external.iac.ResourceReadRequest - 69, // 98: workflow.plugin.external.iac.ResourceDriver.Update:input_type -> workflow.plugin.external.iac.ResourceUpdateRequest - 71, // 99: workflow.plugin.external.iac.ResourceDriver.Delete:input_type -> workflow.plugin.external.iac.ResourceDeleteRequest - 73, // 100: workflow.plugin.external.iac.ResourceDriver.Diff:input_type -> workflow.plugin.external.iac.ResourceDiffRequest - 75, // 101: workflow.plugin.external.iac.ResourceDriver.Scale:input_type -> workflow.plugin.external.iac.ResourceScaleRequest - 77, // 102: workflow.plugin.external.iac.ResourceDriver.HealthCheck:input_type -> workflow.plugin.external.iac.ResourceHealthCheckRequest - 79, // 103: workflow.plugin.external.iac.ResourceDriver.SensitiveKeys:input_type -> workflow.plugin.external.iac.SensitiveKeysRequest - 81, // 104: workflow.plugin.external.iac.ResourceDriver.Troubleshoot:input_type -> workflow.plugin.external.iac.TroubleshootRequest - 84, // 105: workflow.plugin.external.iac.IaCStateBackend.Configure:input_type -> workflow.plugin.external.iac.ConfigureRequest - 86, // 106: workflow.plugin.external.iac.IaCStateBackend.GetState:input_type -> workflow.plugin.external.iac.GetStateRequest - 88, // 107: workflow.plugin.external.iac.IaCStateBackend.SaveState:input_type -> workflow.plugin.external.iac.SaveStateRequest - 90, // 108: workflow.plugin.external.iac.IaCStateBackend.ListStates:input_type -> workflow.plugin.external.iac.ListStatesRequest - 92, // 109: workflow.plugin.external.iac.IaCStateBackend.DeleteState:input_type -> workflow.plugin.external.iac.DeleteStateRequest - 94, // 110: workflow.plugin.external.iac.IaCStateBackend.Lock:input_type -> workflow.plugin.external.iac.LockRequest - 96, // 111: workflow.plugin.external.iac.IaCStateBackend.Unlock:input_type -> workflow.plugin.external.iac.UnlockRequest - 98, // 112: workflow.plugin.external.iac.IaCStateBackend.ListBackendNames:input_type -> workflow.plugin.external.iac.ListBackendNamesRequest - 28, // 113: workflow.plugin.external.iac.IaCProviderRequired.Initialize:output_type -> workflow.plugin.external.iac.InitializeResponse - 30, // 114: workflow.plugin.external.iac.IaCProviderRequired.Name:output_type -> workflow.plugin.external.iac.NameResponse - 32, // 115: workflow.plugin.external.iac.IaCProviderRequired.Version:output_type -> workflow.plugin.external.iac.VersionResponse - 34, // 116: workflow.plugin.external.iac.IaCProviderRequired.Capabilities:output_type -> workflow.plugin.external.iac.CapabilitiesResponse - 36, // 117: workflow.plugin.external.iac.IaCProviderRequired.Plan:output_type -> workflow.plugin.external.iac.PlanResponse - 38, // 118: workflow.plugin.external.iac.IaCProviderRequired.Apply:output_type -> workflow.plugin.external.iac.ApplyResponse - 40, // 119: workflow.plugin.external.iac.IaCProviderRequired.Destroy:output_type -> workflow.plugin.external.iac.DestroyResponse - 42, // 120: workflow.plugin.external.iac.IaCProviderRequired.Status:output_type -> workflow.plugin.external.iac.StatusResponse - 44, // 121: workflow.plugin.external.iac.IaCProviderRequired.Import:output_type -> workflow.plugin.external.iac.ImportResponse - 46, // 122: workflow.plugin.external.iac.IaCProviderRequired.ResolveSizing:output_type -> workflow.plugin.external.iac.ResolveSizingResponse - 48, // 123: workflow.plugin.external.iac.IaCProviderRequired.BootstrapStateBackend:output_type -> workflow.plugin.external.iac.BootstrapStateBackendResponse - 50, // 124: workflow.plugin.external.iac.IaCProviderEnumerator.EnumerateAll:output_type -> workflow.plugin.external.iac.EnumerateAllResponse - 52, // 125: workflow.plugin.external.iac.IaCProviderEnumerator.EnumerateByTag:output_type -> workflow.plugin.external.iac.EnumerateByTagResponse - 54, // 126: workflow.plugin.external.iac.IaCProviderDriftDetector.DetectDrift:output_type -> workflow.plugin.external.iac.DetectDriftResponse - 56, // 127: workflow.plugin.external.iac.IaCProviderDriftDetector.DetectDriftWithSpecs:output_type -> workflow.plugin.external.iac.DetectDriftWithSpecsResponse - 58, // 128: workflow.plugin.external.iac.IaCProviderCredentialRevoker.RevokeProviderCredential:output_type -> workflow.plugin.external.iac.RevokeProviderCredentialResponse - 60, // 129: workflow.plugin.external.iac.IaCProviderMigrationRepairer.RepairDirtyMigration:output_type -> workflow.plugin.external.iac.RepairDirtyMigrationResponse - 62, // 130: workflow.plugin.external.iac.IaCProviderValidator.ValidatePlan:output_type -> workflow.plugin.external.iac.ValidatePlanResponse - 64, // 131: workflow.plugin.external.iac.IaCProviderDriftConfigDetector.DetectDriftConfig:output_type -> workflow.plugin.external.iac.DetectDriftConfigResponse - 66, // 132: workflow.plugin.external.iac.ResourceDriver.Create:output_type -> workflow.plugin.external.iac.ResourceCreateResponse - 68, // 133: workflow.plugin.external.iac.ResourceDriver.Read:output_type -> workflow.plugin.external.iac.ResourceReadResponse - 70, // 134: workflow.plugin.external.iac.ResourceDriver.Update:output_type -> workflow.plugin.external.iac.ResourceUpdateResponse - 72, // 135: workflow.plugin.external.iac.ResourceDriver.Delete:output_type -> workflow.plugin.external.iac.ResourceDeleteResponse - 74, // 136: workflow.plugin.external.iac.ResourceDriver.Diff:output_type -> workflow.plugin.external.iac.ResourceDiffResponse - 76, // 137: workflow.plugin.external.iac.ResourceDriver.Scale:output_type -> workflow.plugin.external.iac.ResourceScaleResponse - 78, // 138: workflow.plugin.external.iac.ResourceDriver.HealthCheck:output_type -> workflow.plugin.external.iac.ResourceHealthCheckResponse - 80, // 139: workflow.plugin.external.iac.ResourceDriver.SensitiveKeys:output_type -> workflow.plugin.external.iac.SensitiveKeysResponse - 82, // 140: workflow.plugin.external.iac.ResourceDriver.Troubleshoot:output_type -> workflow.plugin.external.iac.TroubleshootResponse - 85, // 141: workflow.plugin.external.iac.IaCStateBackend.Configure:output_type -> workflow.plugin.external.iac.ConfigureResponse - 87, // 142: workflow.plugin.external.iac.IaCStateBackend.GetState:output_type -> workflow.plugin.external.iac.GetStateResponse - 89, // 143: workflow.plugin.external.iac.IaCStateBackend.SaveState:output_type -> workflow.plugin.external.iac.SaveStateResponse - 91, // 144: workflow.plugin.external.iac.IaCStateBackend.ListStates:output_type -> workflow.plugin.external.iac.ListStatesResponse - 93, // 145: workflow.plugin.external.iac.IaCStateBackend.DeleteState:output_type -> workflow.plugin.external.iac.DeleteStateResponse - 95, // 146: workflow.plugin.external.iac.IaCStateBackend.Lock:output_type -> workflow.plugin.external.iac.LockResponse - 97, // 147: workflow.plugin.external.iac.IaCStateBackend.Unlock:output_type -> workflow.plugin.external.iac.UnlockResponse - 99, // 148: workflow.plugin.external.iac.IaCStateBackend.ListBackendNames:output_type -> workflow.plugin.external.iac.ListBackendNamesResponse - 113, // [113:149] is the sub-list for method output_type - 77, // [77:113] is the sub-list for method input_type - 77, // [77:77] is the sub-list for extension type_name - 77, // [77:77] is the sub-list for extension extendee - 0, // [0:77] is the sub-list for field type_name + 20, // 47: workflow.plugin.external.iac.FinalizeApplyResponse.errors:type_name -> workflow.plugin.external.iac.ActionError + 25, // 48: workflow.plugin.external.iac.RepairDirtyMigrationRequest.request:type_name -> workflow.plugin.external.iac.MigrationRepairRequest + 26, // 49: workflow.plugin.external.iac.RepairDirtyMigrationResponse.result:type_name -> workflow.plugin.external.iac.MigrationRepairResult + 19, // 50: workflow.plugin.external.iac.ValidatePlanRequest.plan:type_name -> workflow.plugin.external.iac.IaCPlan + 17, // 51: workflow.plugin.external.iac.ValidatePlanResponse.diagnostics:type_name -> workflow.plugin.external.iac.PlanDiagnostic + 4, // 52: workflow.plugin.external.iac.DetectDriftConfigRequest.refs:type_name -> workflow.plugin.external.iac.ResourceRef + 109, // 53: workflow.plugin.external.iac.DetectDriftConfigRequest.specs:type_name -> workflow.plugin.external.iac.DetectDriftConfigRequest.SpecsEntry + 13, // 54: workflow.plugin.external.iac.DetectDriftConfigResponse.drifts:type_name -> workflow.plugin.external.iac.DriftResult + 3, // 55: workflow.plugin.external.iac.ResourceCreateRequest.spec:type_name -> workflow.plugin.external.iac.ResourceSpec + 9, // 56: workflow.plugin.external.iac.ResourceCreateResponse.output:type_name -> workflow.plugin.external.iac.ResourceOutput + 4, // 57: workflow.plugin.external.iac.ResourceReadRequest.ref:type_name -> workflow.plugin.external.iac.ResourceRef + 9, // 58: workflow.plugin.external.iac.ResourceReadResponse.output:type_name -> workflow.plugin.external.iac.ResourceOutput + 4, // 59: workflow.plugin.external.iac.ResourceUpdateRequest.ref:type_name -> workflow.plugin.external.iac.ResourceRef + 3, // 60: workflow.plugin.external.iac.ResourceUpdateRequest.spec:type_name -> workflow.plugin.external.iac.ResourceSpec + 9, // 61: workflow.plugin.external.iac.ResourceUpdateResponse.output:type_name -> workflow.plugin.external.iac.ResourceOutput + 4, // 62: workflow.plugin.external.iac.ResourceDeleteRequest.ref:type_name -> workflow.plugin.external.iac.ResourceRef + 3, // 63: workflow.plugin.external.iac.ResourceDiffRequest.desired:type_name -> workflow.plugin.external.iac.ResourceSpec + 9, // 64: workflow.plugin.external.iac.ResourceDiffRequest.current:type_name -> workflow.plugin.external.iac.ResourceOutput + 12, // 65: workflow.plugin.external.iac.ResourceDiffResponse.result:type_name -> workflow.plugin.external.iac.DiffResult + 4, // 66: workflow.plugin.external.iac.ResourceScaleRequest.ref:type_name -> workflow.plugin.external.iac.ResourceRef + 9, // 67: workflow.plugin.external.iac.ResourceScaleResponse.output:type_name -> workflow.plugin.external.iac.ResourceOutput + 4, // 68: workflow.plugin.external.iac.ResourceHealthCheckRequest.ref:type_name -> workflow.plugin.external.iac.ResourceRef + 15, // 69: workflow.plugin.external.iac.ResourceHealthCheckResponse.result:type_name -> workflow.plugin.external.iac.HealthResult + 4, // 70: workflow.plugin.external.iac.TroubleshootRequest.ref:type_name -> workflow.plugin.external.iac.ResourceRef + 16, // 71: workflow.plugin.external.iac.TroubleshootResponse.diagnostics:type_name -> workflow.plugin.external.iac.Diagnostic + 85, // 72: workflow.plugin.external.iac.GetStateResponse.state:type_name -> workflow.plugin.external.iac.IaCState + 85, // 73: workflow.plugin.external.iac.SaveStateRequest.state:type_name -> workflow.plugin.external.iac.IaCState + 110, // 74: workflow.plugin.external.iac.ListStatesRequest.filter:type_name -> workflow.plugin.external.iac.ListStatesRequest.FilterEntry + 85, // 75: workflow.plugin.external.iac.ListStatesResponse.states:type_name -> workflow.plugin.external.iac.IaCState + 3, // 76: workflow.plugin.external.iac.DetectDriftWithSpecsRequest.SpecsEntry.value:type_name -> workflow.plugin.external.iac.ResourceSpec + 3, // 77: workflow.plugin.external.iac.DetectDriftConfigRequest.SpecsEntry.value:type_name -> workflow.plugin.external.iac.ResourceSpec + 27, // 78: workflow.plugin.external.iac.IaCProviderRequired.Initialize:input_type -> workflow.plugin.external.iac.InitializeRequest + 29, // 79: workflow.plugin.external.iac.IaCProviderRequired.Name:input_type -> workflow.plugin.external.iac.NameRequest + 31, // 80: workflow.plugin.external.iac.IaCProviderRequired.Version:input_type -> workflow.plugin.external.iac.VersionRequest + 33, // 81: workflow.plugin.external.iac.IaCProviderRequired.Capabilities:input_type -> workflow.plugin.external.iac.CapabilitiesRequest + 35, // 82: workflow.plugin.external.iac.IaCProviderRequired.Plan:input_type -> workflow.plugin.external.iac.PlanRequest + 37, // 83: workflow.plugin.external.iac.IaCProviderRequired.Apply:input_type -> workflow.plugin.external.iac.ApplyRequest + 39, // 84: workflow.plugin.external.iac.IaCProviderRequired.Destroy:input_type -> workflow.plugin.external.iac.DestroyRequest + 41, // 85: workflow.plugin.external.iac.IaCProviderRequired.Status:input_type -> workflow.plugin.external.iac.StatusRequest + 43, // 86: workflow.plugin.external.iac.IaCProviderRequired.Import:input_type -> workflow.plugin.external.iac.ImportRequest + 45, // 87: workflow.plugin.external.iac.IaCProviderRequired.ResolveSizing:input_type -> workflow.plugin.external.iac.ResolveSizingRequest + 47, // 88: workflow.plugin.external.iac.IaCProviderRequired.BootstrapStateBackend:input_type -> workflow.plugin.external.iac.BootstrapStateBackendRequest + 49, // 89: workflow.plugin.external.iac.IaCProviderEnumerator.EnumerateAll:input_type -> workflow.plugin.external.iac.EnumerateAllRequest + 51, // 90: workflow.plugin.external.iac.IaCProviderEnumerator.EnumerateByTag:input_type -> workflow.plugin.external.iac.EnumerateByTagRequest + 53, // 91: workflow.plugin.external.iac.IaCProviderDriftDetector.DetectDrift:input_type -> workflow.plugin.external.iac.DetectDriftRequest + 55, // 92: workflow.plugin.external.iac.IaCProviderDriftDetector.DetectDriftWithSpecs:input_type -> workflow.plugin.external.iac.DetectDriftWithSpecsRequest + 57, // 93: workflow.plugin.external.iac.IaCProviderCredentialRevoker.RevokeProviderCredential:input_type -> workflow.plugin.external.iac.RevokeProviderCredentialRequest + 59, // 94: workflow.plugin.external.iac.IaCProviderFinalizer.FinalizeApply:input_type -> workflow.plugin.external.iac.FinalizeApplyRequest + 61, // 95: workflow.plugin.external.iac.IaCProviderMigrationRepairer.RepairDirtyMigration:input_type -> workflow.plugin.external.iac.RepairDirtyMigrationRequest + 63, // 96: workflow.plugin.external.iac.IaCProviderValidator.ValidatePlan:input_type -> workflow.plugin.external.iac.ValidatePlanRequest + 65, // 97: workflow.plugin.external.iac.IaCProviderDriftConfigDetector.DetectDriftConfig:input_type -> workflow.plugin.external.iac.DetectDriftConfigRequest + 67, // 98: workflow.plugin.external.iac.ResourceDriver.Create:input_type -> workflow.plugin.external.iac.ResourceCreateRequest + 69, // 99: workflow.plugin.external.iac.ResourceDriver.Read:input_type -> workflow.plugin.external.iac.ResourceReadRequest + 71, // 100: workflow.plugin.external.iac.ResourceDriver.Update:input_type -> workflow.plugin.external.iac.ResourceUpdateRequest + 73, // 101: workflow.plugin.external.iac.ResourceDriver.Delete:input_type -> workflow.plugin.external.iac.ResourceDeleteRequest + 75, // 102: workflow.plugin.external.iac.ResourceDriver.Diff:input_type -> workflow.plugin.external.iac.ResourceDiffRequest + 77, // 103: workflow.plugin.external.iac.ResourceDriver.Scale:input_type -> workflow.plugin.external.iac.ResourceScaleRequest + 79, // 104: workflow.plugin.external.iac.ResourceDriver.HealthCheck:input_type -> workflow.plugin.external.iac.ResourceHealthCheckRequest + 81, // 105: workflow.plugin.external.iac.ResourceDriver.SensitiveKeys:input_type -> workflow.plugin.external.iac.SensitiveKeysRequest + 83, // 106: workflow.plugin.external.iac.ResourceDriver.Troubleshoot:input_type -> workflow.plugin.external.iac.TroubleshootRequest + 86, // 107: workflow.plugin.external.iac.IaCStateBackend.Configure:input_type -> workflow.plugin.external.iac.ConfigureRequest + 88, // 108: workflow.plugin.external.iac.IaCStateBackend.GetState:input_type -> workflow.plugin.external.iac.GetStateRequest + 90, // 109: workflow.plugin.external.iac.IaCStateBackend.SaveState:input_type -> workflow.plugin.external.iac.SaveStateRequest + 92, // 110: workflow.plugin.external.iac.IaCStateBackend.ListStates:input_type -> workflow.plugin.external.iac.ListStatesRequest + 94, // 111: workflow.plugin.external.iac.IaCStateBackend.DeleteState:input_type -> workflow.plugin.external.iac.DeleteStateRequest + 96, // 112: workflow.plugin.external.iac.IaCStateBackend.Lock:input_type -> workflow.plugin.external.iac.LockRequest + 98, // 113: workflow.plugin.external.iac.IaCStateBackend.Unlock:input_type -> workflow.plugin.external.iac.UnlockRequest + 100, // 114: workflow.plugin.external.iac.IaCStateBackend.ListBackendNames:input_type -> workflow.plugin.external.iac.ListBackendNamesRequest + 28, // 115: workflow.plugin.external.iac.IaCProviderRequired.Initialize:output_type -> workflow.plugin.external.iac.InitializeResponse + 30, // 116: workflow.plugin.external.iac.IaCProviderRequired.Name:output_type -> workflow.plugin.external.iac.NameResponse + 32, // 117: workflow.plugin.external.iac.IaCProviderRequired.Version:output_type -> workflow.plugin.external.iac.VersionResponse + 34, // 118: workflow.plugin.external.iac.IaCProviderRequired.Capabilities:output_type -> workflow.plugin.external.iac.CapabilitiesResponse + 36, // 119: workflow.plugin.external.iac.IaCProviderRequired.Plan:output_type -> workflow.plugin.external.iac.PlanResponse + 38, // 120: workflow.plugin.external.iac.IaCProviderRequired.Apply:output_type -> workflow.plugin.external.iac.ApplyResponse + 40, // 121: workflow.plugin.external.iac.IaCProviderRequired.Destroy:output_type -> workflow.plugin.external.iac.DestroyResponse + 42, // 122: workflow.plugin.external.iac.IaCProviderRequired.Status:output_type -> workflow.plugin.external.iac.StatusResponse + 44, // 123: workflow.plugin.external.iac.IaCProviderRequired.Import:output_type -> workflow.plugin.external.iac.ImportResponse + 46, // 124: workflow.plugin.external.iac.IaCProviderRequired.ResolveSizing:output_type -> workflow.plugin.external.iac.ResolveSizingResponse + 48, // 125: workflow.plugin.external.iac.IaCProviderRequired.BootstrapStateBackend:output_type -> workflow.plugin.external.iac.BootstrapStateBackendResponse + 50, // 126: workflow.plugin.external.iac.IaCProviderEnumerator.EnumerateAll:output_type -> workflow.plugin.external.iac.EnumerateAllResponse + 52, // 127: workflow.plugin.external.iac.IaCProviderEnumerator.EnumerateByTag:output_type -> workflow.plugin.external.iac.EnumerateByTagResponse + 54, // 128: workflow.plugin.external.iac.IaCProviderDriftDetector.DetectDrift:output_type -> workflow.plugin.external.iac.DetectDriftResponse + 56, // 129: workflow.plugin.external.iac.IaCProviderDriftDetector.DetectDriftWithSpecs:output_type -> workflow.plugin.external.iac.DetectDriftWithSpecsResponse + 58, // 130: workflow.plugin.external.iac.IaCProviderCredentialRevoker.RevokeProviderCredential:output_type -> workflow.plugin.external.iac.RevokeProviderCredentialResponse + 60, // 131: workflow.plugin.external.iac.IaCProviderFinalizer.FinalizeApply:output_type -> workflow.plugin.external.iac.FinalizeApplyResponse + 62, // 132: workflow.plugin.external.iac.IaCProviderMigrationRepairer.RepairDirtyMigration:output_type -> workflow.plugin.external.iac.RepairDirtyMigrationResponse + 64, // 133: workflow.plugin.external.iac.IaCProviderValidator.ValidatePlan:output_type -> workflow.plugin.external.iac.ValidatePlanResponse + 66, // 134: workflow.plugin.external.iac.IaCProviderDriftConfigDetector.DetectDriftConfig:output_type -> workflow.plugin.external.iac.DetectDriftConfigResponse + 68, // 135: workflow.plugin.external.iac.ResourceDriver.Create:output_type -> workflow.plugin.external.iac.ResourceCreateResponse + 70, // 136: workflow.plugin.external.iac.ResourceDriver.Read:output_type -> workflow.plugin.external.iac.ResourceReadResponse + 72, // 137: workflow.plugin.external.iac.ResourceDriver.Update:output_type -> workflow.plugin.external.iac.ResourceUpdateResponse + 74, // 138: workflow.plugin.external.iac.ResourceDriver.Delete:output_type -> workflow.plugin.external.iac.ResourceDeleteResponse + 76, // 139: workflow.plugin.external.iac.ResourceDriver.Diff:output_type -> workflow.plugin.external.iac.ResourceDiffResponse + 78, // 140: workflow.plugin.external.iac.ResourceDriver.Scale:output_type -> workflow.plugin.external.iac.ResourceScaleResponse + 80, // 141: workflow.plugin.external.iac.ResourceDriver.HealthCheck:output_type -> workflow.plugin.external.iac.ResourceHealthCheckResponse + 82, // 142: workflow.plugin.external.iac.ResourceDriver.SensitiveKeys:output_type -> workflow.plugin.external.iac.SensitiveKeysResponse + 84, // 143: workflow.plugin.external.iac.ResourceDriver.Troubleshoot:output_type -> workflow.plugin.external.iac.TroubleshootResponse + 87, // 144: workflow.plugin.external.iac.IaCStateBackend.Configure:output_type -> workflow.plugin.external.iac.ConfigureResponse + 89, // 145: workflow.plugin.external.iac.IaCStateBackend.GetState:output_type -> workflow.plugin.external.iac.GetStateResponse + 91, // 146: workflow.plugin.external.iac.IaCStateBackend.SaveState:output_type -> workflow.plugin.external.iac.SaveStateResponse + 93, // 147: workflow.plugin.external.iac.IaCStateBackend.ListStates:output_type -> workflow.plugin.external.iac.ListStatesResponse + 95, // 148: workflow.plugin.external.iac.IaCStateBackend.DeleteState:output_type -> workflow.plugin.external.iac.DeleteStateResponse + 97, // 149: workflow.plugin.external.iac.IaCStateBackend.Lock:output_type -> workflow.plugin.external.iac.LockResponse + 99, // 150: workflow.plugin.external.iac.IaCStateBackend.Unlock:output_type -> workflow.plugin.external.iac.UnlockResponse + 101, // 151: workflow.plugin.external.iac.IaCStateBackend.ListBackendNames:output_type -> workflow.plugin.external.iac.ListBackendNamesResponse + 115, // [115:152] is the sub-list for method output_type + 78, // [78:115] is the sub-list for method input_type + 78, // [78:78] is the sub-list for extension type_name + 78, // [78:78] is the sub-list for extension extendee + 0, // [0:78] is the sub-list for field type_name } func init() { file_iac_proto_init() } @@ -6231,9 +6344,9 @@ func file_iac_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_iac_proto_rawDesc), len(file_iac_proto_rawDesc)), NumEnums: 3, - NumMessages: 106, + NumMessages: 108, NumExtensions: 0, - NumServices: 9, + NumServices: 10, }, GoTypes: file_iac_proto_goTypes, DependencyIndexes: file_iac_proto_depIdxs, diff --git a/plugin/external/proto/iac.proto b/plugin/external/proto/iac.proto index 53b4bef9..6bdcf913 100644 --- a/plugin/external/proto/iac.proto +++ b/plugin/external/proto/iac.proto @@ -59,6 +59,17 @@ service IaCProviderCredentialRevoker { rpc RevokeProviderCredential(RevokeProviderCredentialRequest) returns (RevokeProviderCredentialResponse); } +// IaCProviderFinalizer is the optional service plugins implement when +// they need a post-apply-loop finalizer hook under v2 dispatch. +// Use case: DigitalOcean plugin's database trusted_sources deferred-flush +// (see workflow-plugin-digitalocean/internal/provider.go:295-307) which +// runs after the per-action loop completes. Under v2 dispatch wfctl +// bypasses IaCProvider.Apply entirely, so plugins needing post-loop +// work must opt in via this service. Phase 2.5 of workflow#640. +service IaCProviderFinalizer { + rpc FinalizeApply(FinalizeApplyRequest) returns (FinalizeApplyResponse); +} + service IaCProviderMigrationRepairer { rpc RepairDirtyMigration(RepairDirtyMigrationRequest) returns (RepairDirtyMigrationResponse); } @@ -507,6 +518,28 @@ message RevokeProviderCredentialRequest { } message RevokeProviderCredentialResponse {} +// ───────────────────────────────────────────────────────────────────────────── +// IaCProviderFinalizer messages. +// ───────────────────────────────────────────────────────────────────────────── +message FinalizeApplyRequest { + // plan_id is the IaCPlan.ID being finalized (for plugin-side logging / + // correlation; not load-bearing for dispatch). + string plan_id = 1; +} + +message FinalizeApplyResponse { + // errors is the per-driver finalize-side error array. Each entry + // preserves the v1 wrapper's per-driver attribution shape + // (workflow-plugin-digitalocean/internal/provider.go:301-306 uses + // ActionError{Resource: resourceType, Action: "deferred_update", Error: ...}). + // Empty array = success. Non-empty = each error is surfaced to wfctl's + // result.Errors as-is (preserving per-driver attribution), AND the + // outer finalize call returns a wrapped error to the caller. + // + // Tag 2 reserved for Phase 2.3 compensation evidence. + repeated ActionError errors = 1; +} + // ───────────────────────────────────────────────────────────────────────────── // IaCProviderMigrationRepairer messages. // ───────────────────────────────────────────────────────────────────────────── diff --git a/plugin/external/proto/iac_grpc.pb.go b/plugin/external/proto/iac_grpc.pb.go index acf52960..0dd822ad 100644 --- a/plugin/external/proto/iac_grpc.pb.go +++ b/plugin/external/proto/iac_grpc.pb.go @@ -924,6 +924,124 @@ var IaCProviderCredentialRevoker_ServiceDesc = grpc.ServiceDesc{ Metadata: "iac.proto", } +const ( + IaCProviderFinalizer_FinalizeApply_FullMethodName = "/workflow.plugin.external.iac.IaCProviderFinalizer/FinalizeApply" +) + +// IaCProviderFinalizerClient is the client API for IaCProviderFinalizer service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// IaCProviderFinalizer is the optional service plugins implement when +// they need a post-apply-loop finalizer hook under v2 dispatch. +// Use case: DigitalOcean plugin's database trusted_sources deferred-flush +// (see workflow-plugin-digitalocean/internal/provider.go:295-307) which +// runs after the per-action loop completes. Under v2 dispatch wfctl +// bypasses IaCProvider.Apply entirely, so plugins needing post-loop +// work must opt in via this service. Phase 2.5 of workflow#640. +type IaCProviderFinalizerClient interface { + FinalizeApply(ctx context.Context, in *FinalizeApplyRequest, opts ...grpc.CallOption) (*FinalizeApplyResponse, error) +} + +type iaCProviderFinalizerClient struct { + cc grpc.ClientConnInterface +} + +func NewIaCProviderFinalizerClient(cc grpc.ClientConnInterface) IaCProviderFinalizerClient { + return &iaCProviderFinalizerClient{cc} +} + +func (c *iaCProviderFinalizerClient) FinalizeApply(ctx context.Context, in *FinalizeApplyRequest, opts ...grpc.CallOption) (*FinalizeApplyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(FinalizeApplyResponse) + err := c.cc.Invoke(ctx, IaCProviderFinalizer_FinalizeApply_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// IaCProviderFinalizerServer is the server API for IaCProviderFinalizer service. +// All implementations must embed UnimplementedIaCProviderFinalizerServer +// for forward compatibility. +// +// IaCProviderFinalizer is the optional service plugins implement when +// they need a post-apply-loop finalizer hook under v2 dispatch. +// Use case: DigitalOcean plugin's database trusted_sources deferred-flush +// (see workflow-plugin-digitalocean/internal/provider.go:295-307) which +// runs after the per-action loop completes. Under v2 dispatch wfctl +// bypasses IaCProvider.Apply entirely, so plugins needing post-loop +// work must opt in via this service. Phase 2.5 of workflow#640. +type IaCProviderFinalizerServer interface { + FinalizeApply(context.Context, *FinalizeApplyRequest) (*FinalizeApplyResponse, error) + mustEmbedUnimplementedIaCProviderFinalizerServer() +} + +// UnimplementedIaCProviderFinalizerServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedIaCProviderFinalizerServer struct{} + +func (UnimplementedIaCProviderFinalizerServer) FinalizeApply(context.Context, *FinalizeApplyRequest) (*FinalizeApplyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method FinalizeApply not implemented") +} +func (UnimplementedIaCProviderFinalizerServer) mustEmbedUnimplementedIaCProviderFinalizerServer() {} +func (UnimplementedIaCProviderFinalizerServer) testEmbeddedByValue() {} + +// UnsafeIaCProviderFinalizerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to IaCProviderFinalizerServer will +// result in compilation errors. +type UnsafeIaCProviderFinalizerServer interface { + mustEmbedUnimplementedIaCProviderFinalizerServer() +} + +func RegisterIaCProviderFinalizerServer(s grpc.ServiceRegistrar, srv IaCProviderFinalizerServer) { + // If the following call panics, it indicates UnimplementedIaCProviderFinalizerServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&IaCProviderFinalizer_ServiceDesc, srv) +} + +func _IaCProviderFinalizer_FinalizeApply_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FinalizeApplyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IaCProviderFinalizerServer).FinalizeApply(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IaCProviderFinalizer_FinalizeApply_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IaCProviderFinalizerServer).FinalizeApply(ctx, req.(*FinalizeApplyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// IaCProviderFinalizer_ServiceDesc is the grpc.ServiceDesc for IaCProviderFinalizer service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var IaCProviderFinalizer_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "workflow.plugin.external.iac.IaCProviderFinalizer", + HandlerType: (*IaCProviderFinalizerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "FinalizeApply", + Handler: _IaCProviderFinalizer_FinalizeApply_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "iac.proto", +} + const ( IaCProviderMigrationRepairer_RepairDirtyMigration_FullMethodName = "/workflow.plugin.external.iac.IaCProviderMigrationRepairer/RepairDirtyMigration" ) From db81cdd2071b2f8bb2764eae5fe9ed97c3140b67 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 17 May 2026 01:56:40 -0400 Subject: [PATCH 02/11] feat(proto): enforce FinalizeApplyResponse tag-2 reservation + clarify wire-status invariant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `reserved 2;` directive inside FinalizeApplyResponse (protoc-level enforcement; comment-only reservation gave no protection). Raw descriptor now encodes reserved_range [2,3) — future allocator will fail at codegen. - Replace cross-repo line pins (provider.go:295-307 and :301-306) with function/identifier references (DOProvider.Apply + deferredUpdater + FlushDeferredUpdates) that survive DO-side refactors. - Add wire-status invariant on FinalizeApplyResponse: gRPC status MUST be OK when errors[] is populated (mirrors ADR 0040 invariant 2); non-OK status means errors[] is ignored and gRPC error surfaced directly. Eliminates ambiguity between transport failure and per- driver finalize errors. Regen mechanical (godoc + raw descriptor); no struct/symbol drift. Co-Authored-By: Claude Opus 4.7 --- plugin/external/proto/iac.pb.go | 25 +++++++++++++-------- plugin/external/proto/iac.proto | 33 ++++++++++++++++++---------- plugin/external/proto/iac_grpc.pb.go | 20 ++++++++++------- 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/plugin/external/proto/iac.pb.go b/plugin/external/proto/iac.pb.go index 7bf2a30a..257fb12f 100644 --- a/plugin/external/proto/iac.pb.go +++ b/plugin/external/proto/iac.pb.go @@ -3554,14 +3554,21 @@ func (x *FinalizeApplyRequest) GetPlanId() string { type FinalizeApplyResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // errors is the per-driver finalize-side error array. Each entry - // preserves the v1 wrapper's per-driver attribution shape - // (workflow-plugin-digitalocean/internal/provider.go:301-306 uses - // ActionError{Resource: resourceType, Action: "deferred_update", Error: ...}). - // Empty array = success. Non-empty = each error is surfaced to wfctl's - // result.Errors as-is (preserving per-driver attribution), AND the - // outer finalize call returns a wrapped error to the caller. + // preserves the v1 wrapper's per-driver attribution shape (DOProvider.Apply + // post-loop block appends interfaces.ActionError{Resource: resourceType, + // Action: "deferred_update", Error: flushErr.Error()} for each driver + // whose FlushDeferredUpdates fails). Empty array = success. Non-empty = + // each error is surfaced to wfctl's result.Errors as-is (preserving + // per-driver attribution), AND the outer finalize call returns a + // wrapped error to the caller. // - // Tag 2 reserved for Phase 2.3 compensation evidence. + // Wire-status invariant (mirrors ADR 0040 invariant 2): the gRPC status + // MUST be OK whenever the plugin populates errors[] (including the + // empty-success case). A non-OK gRPC status (e.g., codes.Internal from + // a server panic) signals transport-level failure; errors[] is IGNORED + // by wfctl in that case and the gRPC error is surfaced directly. This + // prevents ambiguity between "finalize ran and reported per-driver + // errors" (OK + errors[]) and "finalize call itself failed" (non-OK). Errors []*ActionError `protobuf:"bytes,1,rep,name=errors,proto3" json:"errors,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -5855,9 +5862,9 @@ const file_iac_proto_rawDesc = "" + "\rcredential_id\x18\x02 \x01(\tR\fcredentialId\"\"\n" + " RevokeProviderCredentialResponse\"/\n" + "\x14FinalizeApplyRequest\x12\x17\n" + - "\aplan_id\x18\x01 \x01(\tR\x06planId\"Z\n" + + "\aplan_id\x18\x01 \x01(\tR\x06planId\"`\n" + "\x15FinalizeApplyResponse\x12A\n" + - "\x06errors\x18\x01 \x03(\v2).workflow.plugin.external.iac.ActionErrorR\x06errors\"m\n" + + "\x06errors\x18\x01 \x03(\v2).workflow.plugin.external.iac.ActionErrorR\x06errorsJ\x04\b\x02\x10\x03\"m\n" + "\x1bRepairDirtyMigrationRequest\x12N\n" + "\arequest\x18\x01 \x01(\v24.workflow.plugin.external.iac.MigrationRepairRequestR\arequest\"k\n" + "\x1cRepairDirtyMigrationResponse\x12K\n" + diff --git a/plugin/external/proto/iac.proto b/plugin/external/proto/iac.proto index 6bdcf913..8bf88834 100644 --- a/plugin/external/proto/iac.proto +++ b/plugin/external/proto/iac.proto @@ -62,10 +62,12 @@ service IaCProviderCredentialRevoker { // IaCProviderFinalizer is the optional service plugins implement when // they need a post-apply-loop finalizer hook under v2 dispatch. // Use case: DigitalOcean plugin's database trusted_sources deferred-flush -// (see workflow-plugin-digitalocean/internal/provider.go:295-307) which -// runs after the per-action loop completes. Under v2 dispatch wfctl -// bypasses IaCProvider.Apply entirely, so plugins needing post-loop -// work must opt in via this service. Phase 2.5 of workflow#640. +// (see DOProvider.Apply post-loop block in workflow-plugin-digitalocean +// internal/provider.go, iterating drivers that implement deferredUpdater +// and calling FlushDeferredUpdates) which runs after the per-action loop +// completes. Under v2 dispatch wfctl bypasses IaCProvider.Apply entirely, +// so plugins needing post-loop work must opt in via this service. +// Phase 2.5 of workflow#640. service IaCProviderFinalizer { rpc FinalizeApply(FinalizeApplyRequest) returns (FinalizeApplyResponse); } @@ -528,15 +530,24 @@ message FinalizeApplyRequest { } message FinalizeApplyResponse { + reserved 2; // Phase 2.3 compensation evidence (ACTION_STATUS_COMPENSATED outcomes) + // errors is the per-driver finalize-side error array. Each entry - // preserves the v1 wrapper's per-driver attribution shape - // (workflow-plugin-digitalocean/internal/provider.go:301-306 uses - // ActionError{Resource: resourceType, Action: "deferred_update", Error: ...}). - // Empty array = success. Non-empty = each error is surfaced to wfctl's - // result.Errors as-is (preserving per-driver attribution), AND the - // outer finalize call returns a wrapped error to the caller. + // preserves the v1 wrapper's per-driver attribution shape (DOProvider.Apply + // post-loop block appends interfaces.ActionError{Resource: resourceType, + // Action: "deferred_update", Error: flushErr.Error()} for each driver + // whose FlushDeferredUpdates fails). Empty array = success. Non-empty = + // each error is surfaced to wfctl's result.Errors as-is (preserving + // per-driver attribution), AND the outer finalize call returns a + // wrapped error to the caller. // - // Tag 2 reserved for Phase 2.3 compensation evidence. + // Wire-status invariant (mirrors ADR 0040 invariant 2): the gRPC status + // MUST be OK whenever the plugin populates errors[] (including the + // empty-success case). A non-OK gRPC status (e.g., codes.Internal from + // a server panic) signals transport-level failure; errors[] is IGNORED + // by wfctl in that case and the gRPC error is surfaced directly. This + // prevents ambiguity between "finalize ran and reported per-driver + // errors" (OK + errors[]) and "finalize call itself failed" (non-OK). repeated ActionError errors = 1; } diff --git a/plugin/external/proto/iac_grpc.pb.go b/plugin/external/proto/iac_grpc.pb.go index 0dd822ad..3adbd4d0 100644 --- a/plugin/external/proto/iac_grpc.pb.go +++ b/plugin/external/proto/iac_grpc.pb.go @@ -935,10 +935,12 @@ const ( // IaCProviderFinalizer is the optional service plugins implement when // they need a post-apply-loop finalizer hook under v2 dispatch. // Use case: DigitalOcean plugin's database trusted_sources deferred-flush -// (see workflow-plugin-digitalocean/internal/provider.go:295-307) which -// runs after the per-action loop completes. Under v2 dispatch wfctl -// bypasses IaCProvider.Apply entirely, so plugins needing post-loop -// work must opt in via this service. Phase 2.5 of workflow#640. +// (see DOProvider.Apply post-loop block in workflow-plugin-digitalocean +// internal/provider.go, iterating drivers that implement deferredUpdater +// and calling FlushDeferredUpdates) which runs after the per-action loop +// completes. Under v2 dispatch wfctl bypasses IaCProvider.Apply entirely, +// so plugins needing post-loop work must opt in via this service. +// Phase 2.5 of workflow#640. type IaCProviderFinalizerClient interface { FinalizeApply(ctx context.Context, in *FinalizeApplyRequest, opts ...grpc.CallOption) (*FinalizeApplyResponse, error) } @@ -968,10 +970,12 @@ func (c *iaCProviderFinalizerClient) FinalizeApply(ctx context.Context, in *Fina // IaCProviderFinalizer is the optional service plugins implement when // they need a post-apply-loop finalizer hook under v2 dispatch. // Use case: DigitalOcean plugin's database trusted_sources deferred-flush -// (see workflow-plugin-digitalocean/internal/provider.go:295-307) which -// runs after the per-action loop completes. Under v2 dispatch wfctl -// bypasses IaCProvider.Apply entirely, so plugins needing post-loop -// work must opt in via this service. Phase 2.5 of workflow#640. +// (see DOProvider.Apply post-loop block in workflow-plugin-digitalocean +// internal/provider.go, iterating drivers that implement deferredUpdater +// and calling FlushDeferredUpdates) which runs after the per-action loop +// completes. Under v2 dispatch wfctl bypasses IaCProvider.Apply entirely, +// so plugins needing post-loop work must opt in via this service. +// Phase 2.5 of workflow#640. type IaCProviderFinalizerServer interface { FinalizeApply(context.Context, *FinalizeApplyRequest) (*FinalizeApplyResponse, error) mustEmbedUnimplementedIaCProviderFinalizerServer() From 50e3feb0b5b5e4c3b2d5307393681900d1b736eb Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 17 May 2026 02:22:40 -0400 Subject: [PATCH 03/11] feat(engine): add OnPlanComplete hook + deferred-closure invocation in apply (Phase 2.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ApplyPlanHooks.OnPlanComplete field + deferred-closure invocation inside applyPlanWithEnvProviderAndHooks. Hook fires once after the per-action loop reaches its natural success completion (apply.go:336), matching the v1 DOProvider.Apply wrapper's deferred-flush placement. Changes: - Add OnPlanComplete field with godoc enumerating fire/no-fire exit paths. - Change applyPlanWithEnvProviderAndHooks signature to named returns (result *interfaces.ApplyResult, err error) so the deferred closure can read err for v1 semantic gating and append to result.Errors / reassign err on finalize failure. - Add loopReached flag set immediately before the per-action loop opens; pre-loop preflight failures (apply.go:192) skip finalize because loopReached stays false (no cloud work happened). - Deferred closure gates on err == nil per cycle-1 plan-review C-3 v1 semantic preservation — outer-error exits at per-action hook fatalErr (apply.go:319-324) and post-loop length-invariant violation (333) skip finalize, matching DOProvider.Apply's "return without flushing on top-level err" at internal/provider.go:276-282. - Finalize-side hook errors surface to caller's err (wrapped via fmt.Errorf with %w) AND append result.Errors entry with Resource=""/Action="finalize" so per-driver attribution is preserved alongside the finalize-attributed failure. Tests (apply_hooks_test.go, +5 tests, all PASS race-clean): - FiresOnCleanSuccess: success-exit fires finalize. - FiresOnEmptyPlan: zero-iteration plan still fires finalize (regression guard — v1 wrapper flushes stale-queued state for empty plans too). - SurfacesErrorToCaller: finalize hookErr wraps via %w and adds "" entry to result.Errors. - SkippedOnOuterError: OnResourceApplied hook returning err triggers outer fatalErr at 324; finalize skipped (C-3 invariant). - DoesNotFireOnPreloopError: replace + ResourceReplacer + delete-hook active triggers preflight rejection at 192; loopReached=false; finalize skipped. Backward compat: ApplyPlanHooks zero value (no OnPlanComplete) → defer short-circuits on `hooks.OnPlanComplete == nil`. Existing callers using positional `return result, ...` continue to work unchanged with named returns. Per workflow#695 Phase 2.5 / ADR 0024 / ADR 0040. Co-Authored-By: Claude Opus 4.7 --- iac/wfctlhelpers/apply.go | 73 +++++++++++- iac/wfctlhelpers/apply_hooks_test.go | 168 +++++++++++++++++++++++++++ 2 files changed, 239 insertions(+), 2 deletions(-) diff --git a/iac/wfctlhelpers/apply.go b/iac/wfctlhelpers/apply.go index a0e1f4c8..d18670e7 100644 --- a/iac/wfctlhelpers/apply.go +++ b/iac/wfctlhelpers/apply.go @@ -110,6 +110,31 @@ func ApplyPlan(ctx context.Context, p interfaces.IaCProvider, plan *interfaces.I type ApplyPlanHooks struct { OnResourceApplied func(context.Context, interfaces.ResourceDriver, interfaces.PlanAction, interfaces.ResourceOutput) error OnResourceDeleted func(context.Context, interfaces.PlanAction) error + // OnPlanComplete fires once after the per-action loop reaches its + // natural success completion (apply.go:336), i.e., when the outer + // function is about to return (result, nil). Used by the DO plugin's + // deferred-flush integration via IaCProviderFinalizer.FinalizeApply RPC. + // + // Does NOT fire on: + // - Pre-loop preflight error early-return (apply.go:192) — + // loopReached=false, no cloud work happened. + // - Per-action hook fatalErr bubbling to outer return + // (apply.go:319-324) — outer err != nil. v1 semantic preservation + // per cycle-1 plan-review C-3: DOProvider.Apply skips deferred- + // flush when wfctlhelpers.ApplyPlan returns a top-level err + // (internal/provider.go:276-282). + // - Post-loop length-invariant violation (apply.go:333) — + // outer err != nil. + // + // DOES fire on per-action driver-error paths (best-effort; driver + // errors append to result.Errors but do NOT set fatalErr; the loop + // continues and reaches the success exit at 336). Mirrors v1 + // DOProvider.Apply behavior where the deferred-flush ran whenever + // the wrapped Apply returned nil err, regardless of per-action + // result.Errors entries. + // + // Per workflow#695 Phase 2.5 / ADR 0024 / ADR 0040. + OnPlanComplete func(context.Context) error } // ApplyPlanWithHooks is ApplyPlan plus action-boundary hooks for callers that @@ -140,10 +165,46 @@ func applyPlanWithEnvProviderAndHooks( plan *interfaces.IaCPlan, applyTimeEnv func(string) (string, bool), hooks ApplyPlanHooks, -) (*interfaces.ApplyResult, error) { +) (result *interfaces.ApplyResult, err error) { + // loopReached is set to true immediately before the per-action loop + // opens (below). The deferred OnPlanComplete closure short-circuits + // when loopReached=false so pre-loop preflight failures skip finalize + // (no cloud work happened). On loop-reached exits, the closure + // additionally gates on err == nil per cycle-1 plan-review C-3 v1 + // semantic preservation — see the ApplyPlanHooks.OnPlanComplete + // godoc above. + var loopReached bool + defer func() { + if !loopReached || hooks.OnPlanComplete == nil { + return + } + if err != nil { + // v1 semantic preservation: outer-error exits (per-action + // hook fatalErr at apply.go:319-324, post-loop length- + // invariant at 333) skip finalize, matching DOProvider.Apply's + // "return without flushing on top-level err" at + // internal/provider.go:276-282. + return + } + if hookErr := hooks.OnPlanComplete(ctx); hookErr != nil { + // Append to result.Errors so callers see per-driver + // attribution alongside the finalize-attributed failure. + // Surface to outer err so the caller observes the failure + // at the function boundary (outer err was nil; finalize + // failure now becomes the outer err). + finalizeErr := fmt.Errorf("plan finalize: %w", hookErr) + result.Errors = append(result.Errors, interfaces.ActionError{ + Resource: "", + Action: "finalize", + Error: finalizeErr.Error(), + }) + err = finalizeErr + } + }() + deleteHookActive := hooks.OnResourceDeleted != nil inputNames := snapshotKeys(plan.InputSnapshot) - result := &interfaces.ApplyResult{ + result = &interfaces.ApplyResult{ PlanID: plan.ID, InitialInputSnapshot: inputsnapshot.Snapshot(inputNames, inputsnapshot.OSEnvProvider), } @@ -193,6 +254,14 @@ func applyPlanWithEnvProviderAndHooks( } } + // loopReached gates the deferred OnPlanComplete invocation. Set + // BEFORE the loop opens so a zero-iteration (empty Actions) plan + // still triggers finalize — matches v1 DOProvider.Apply behavior + // where the deferred-flush fires regardless of plan length (stale + // queued state from prior runs is flushed even when the current + // plan has no actions). + loopReached = true + for i := range plan.Actions { action := plan.Actions[i] // Phase 2 (workflow#640 + ADR 0040 invariant 1): per-PlanAction diff --git a/iac/wfctlhelpers/apply_hooks_test.go b/iac/wfctlhelpers/apply_hooks_test.go index cb478ad7..784dcb22 100644 --- a/iac/wfctlhelpers/apply_hooks_test.go +++ b/iac/wfctlhelpers/apply_hooks_test.go @@ -300,3 +300,171 @@ func TestApplyPlanWithHooks_PopulatesActions_PreDispatchDriverError(t *testing.T t.Errorf("expected 1 result.Errors entry (legacy contract), got %d", len(result.Errors)) } } + +// ───────────────────────────────────────────────────────────────────────────── +// OnPlanComplete tests (workflow#695 Phase 2.5). Per plan task 2 + +// cycle-1 plan-review C-3 v1 semantic preservation: OnPlanComplete fires +// ONLY on outer-success exits (apply.go:336), matching the DOProvider.Apply +// v1 wrapper's "return without flushing on top-level err" behavior at +// internal/provider.go:276-282. Outer errors at preflight (192), +// per-action fatalErr (324), and post-loop length-invariant (333) ALL +// skip finalize. +// ───────────────────────────────────────────────────────────────────────────── + +// TestApplyPlanWithHooks_OnPlanComplete_FiresOnCleanSuccess verifies that +// OnPlanComplete fires after a normally-completed apply loop. +func TestApplyPlanWithHooks_OnPlanComplete_FiresOnCleanSuccess(t *testing.T) { + p := newFakeProvider() + plan := &interfaces.IaCPlan{ + ID: "plan-1", + Actions: []interfaces.PlanAction{ + {Action: "create", Resource: interfaces.ResourceSpec{Name: "r1", Type: "infra.test"}}, + }, + } + var fired bool + hooks := ApplyPlanHooks{ + OnPlanComplete: func(_ context.Context) error { + fired = true + return nil + }, + } + _, err := ApplyPlanWithHooks(t.Context(), p, plan, hooks) + if err != nil { + t.Fatalf("top-level err: %v", err) + } + if !fired { + t.Error("OnPlanComplete did not fire on clean success") + } +} + +// TestApplyPlanWithHooks_OnPlanComplete_FiresOnEmptyPlan verifies that +// OnPlanComplete fires even when plan.Actions is empty — loopReached is +// set BEFORE the for-loop opens so a zero-iteration plan still finalizes. +// Regression guard: the v1 DOProvider.Apply wrapper flushes stale-queued +// state even for empty plans (no per-action work needed); the v2 hook +// must preserve that semantic. +func TestApplyPlanWithHooks_OnPlanComplete_FiresOnEmptyPlan(t *testing.T) { + p := newFakeProvider() + plan := &interfaces.IaCPlan{ID: "plan-empty", Actions: nil} + var fired bool + hooks := ApplyPlanHooks{OnPlanComplete: func(_ context.Context) error { + fired = true + return nil + }} + _, err := ApplyPlanWithHooks(t.Context(), p, plan, hooks) + if err != nil { + t.Fatalf("top-level err: %v", err) + } + if !fired { + t.Error("OnPlanComplete did not fire on empty plan (regression: v1 wrapper flushes stale-queued state even for empty plans)") + } +} + +// TestApplyPlanWithHooks_OnPlanComplete_SurfacesErrorToCaller verifies that +// a finalize-side hook error surfaces to the caller (outer err wraps the +// sentinel) AND appends an ActionError with Resource="" to +// result.Errors so per-driver attribution is preserved alongside the +// finalize-attributed failure. +func TestApplyPlanWithHooks_OnPlanComplete_SurfacesErrorToCaller(t *testing.T) { + p := newFakeProvider() + plan := &interfaces.IaCPlan{ + ID: "plan-2", + Actions: []interfaces.PlanAction{ + {Action: "create", Resource: interfaces.ResourceSpec{Name: "r1", Type: "infra.test"}}, + }, + } + sentinel := errors.New("plugin finalize failed") + hooks := ApplyPlanHooks{OnPlanComplete: func(_ context.Context) error { return sentinel }} + result, err := ApplyPlanWithHooks(t.Context(), p, plan, hooks) + if err == nil { + t.Fatal("expected finalize error to surface to caller") + } + if !errors.Is(err, sentinel) { + t.Errorf("expected outer err to wrap sentinel; got: %v", err) + } + if len(result.Errors) == 0 || result.Errors[len(result.Errors)-1].Resource != "" { + t.Errorf("expected last result.Errors entry to have Resource=\"\"; got: %+v", result.Errors) + } + if last := result.Errors[len(result.Errors)-1]; last.Action != "finalize" { + t.Errorf("expected last result.Errors entry to have Action=\"finalize\"; got: %q", last.Action) + } +} + +// TestApplyPlanWithHooks_OnPlanComplete_SkippedOnOuterError verifies the v1 +// semantic-preservation gate (cycle-1 plan-review C-3): when a per-action +// hook returns an error, fatalErr bubbles to outer return at apply.go:324 +// with non-nil err. OnPlanComplete MUST NOT fire on that exit, matching +// DOProvider.Apply's "return without flushing on top-level err" behavior +// at internal/provider.go:276-282. +func TestApplyPlanWithHooks_OnPlanComplete_SkippedOnOuterError(t *testing.T) { + p := newFakeProvider() + plan := &interfaces.IaCPlan{ + ID: "plan-fatal", + Actions: []interfaces.PlanAction{ + {Action: "create", Resource: interfaces.ResourceSpec{Name: "r1", Type: "infra.test"}}, + }, + } + hookSentinel := errors.New("post-apply hook failure") + var finalizeFired bool + hooks := ApplyPlanHooks{ + // OnResourceApplied returning err sets fatalErr → outer err != nil + // (apply.go:308-314 → 319-324). True outer-error path. + OnResourceApplied: func(_ context.Context, _ interfaces.ResourceDriver, _ interfaces.PlanAction, _ interfaces.ResourceOutput) error { + return hookSentinel + }, + OnPlanComplete: func(_ context.Context) error { + finalizeFired = true + return nil + }, + } + _, err := ApplyPlanWithHooks(t.Context(), p, plan, hooks) + if err == nil { + t.Fatal("expected outer err from per-action hook fatalErr path") + } + if !errors.Is(err, hookSentinel) { + t.Errorf("expected outer err to wrap hookSentinel; got: %v", err) + } + if finalizeFired { + t.Error("OnPlanComplete fired on outer-error exit — v1 semantic preservation (C-3) violated") + } +} + +// TestApplyPlanWithHooks_OnPlanComplete_DoesNotFireOnPreloopError verifies +// that pre-loop preflight failures (apply.go:192) skip OnPlanComplete — +// loopReached is still false when preflight returns early, so the deferred +// closure's first short-circuit triggers and finalize never runs. +// Regression guard: no cloud work happened, so no finalize work should +// happen either. +func TestApplyPlanWithHooks_OnPlanComplete_DoesNotFireOnPreloopError(t *testing.T) { + // fakeReplacerDriver implements interfaces.ResourceReplacer (defined in + // apply_replacer_dispatch_test.go); combined with deleteHookActive + // (OnResourceDeleted set), preflightProviderOwnedReplaceWithDeleteHooks + // returns the engine-side rejection error at apply.go:367. + provider := &hookProvider{driver: &fakeReplacerDriver{}} + plan := &interfaces.IaCPlan{ + ID: "plan-preflight-fail", + Actions: []interfaces.PlanAction{{ + Action: "replace", + Resource: interfaces.ResourceSpec{Name: "r1", Type: "infra.test"}, + Current: &interfaces.ResourceState{Name: "r1", Type: "infra.test", ProviderID: "old-id"}, + }}, + } + var finalizeFired bool + hooks := ApplyPlanHooks{ + OnResourceDeleted: func(_ context.Context, _ interfaces.PlanAction) error { return nil }, + OnPlanComplete: func(_ context.Context) error { + finalizeFired = true + return nil + }, + } + _, err := ApplyPlanWithHooks(t.Context(), provider, plan, hooks) + if err == nil { + t.Fatal("expected preflight error (replace + delete-hook active + ResourceReplacer driver)") + } + if !strings.Contains(err.Error(), "driver-owned ResourceReplacer is disabled") { + t.Errorf("expected preflight rejection error; got: %v", err) + } + if finalizeFired { + t.Error("OnPlanComplete fired on pre-loop preflight error — should not fire when loopReached=false") + } +} From dbe23b026f7f198214e414fcd87d3257a57f382c Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 17 May 2026 02:32:19 -0400 Subject: [PATCH 04/11] fix(engine): anchor OnPlanComplete docs by symbol + recover() finalize panics + drop redundant attribution prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three follow-ups on the OnPlanComplete deferred closure (50e3feb0): 1. Replace ~10 stale within-file line refs in ApplyPlanHooks.OnPlanComplete godoc + apply.go inline comments + apply_hooks_test.go test docs with symbol/structural anchors. The original commit drafted refs against pre-insertion plan-doc line numbers; the +95 LOC of inserted code shifted everything down ~80-90 lines, leaving every ref misleading. Now anchors to identifier names that survive line shifts: - "apply.go:336" → "the natural success-exit return at the end of applyPlanWithEnvProviderAndHooks" - "apply.go:192" → "the preflightProviderOwnedReplaceWithDeleteHooks early-return" - "apply.go:319-324" → "the per-action loop's `if fatalErr != nil { return ... }` early-return" - "apply.go:333" → "the post-loop length-invariant check" - "apply.go:367" → "preflightProviderOwnedReplaceWithDeleteHooks's engine-side ResourceReplacer rejection error" - "internal/provider.go:276-282" → "the `if err != nil { return ... }` guard immediately after the wrapped ApplyPlan call in workflow-plugin-digitalocean internal/provider.go" (Pre-existing ref at apply_hooks_test.go:270 left alone — not part of this commit's scope.) 2. Add recover() around hooks.OnPlanComplete(ctx) for symmetry with the drift defer's recover() posture on caller-provided closures. A panic inside the finalize hook would otherwise propagate past the defer chain and prevent the caller from observing result/err. On recover, set hookErr to "OnPlanComplete panicked: " and surface through the existing result.Errors + outer-err pathways. 3. Drop redundant "plan finalize:" prefix from the ActionError.Error field. The entry already carries Resource="" + Action="finalize" — embedding the prefix inside Error caused double-attribution when callers format as "/: ". Outer err keeps the prefix because the outer-err path lacks the structured fields and needs the attribution inline; errors.Is round-trip on the wrapped hookErr is preserved. Tests: - All 5 prior OnPlanComplete tests still PASS (no behavior change for the non-panic, non-attribution-prefix paths). - New TestApplyPlanWithHooks_OnPlanComplete_RecoversFromPanic asserts panic → recovered err mentions "OnPlanComplete panicked" + carries the panic value + appends "" entry. - Full ./iac/wfctlhelpers/ -race -count=1 PASS. Co-Authored-By: Claude Opus 4.7 --- iac/wfctlhelpers/apply.go | 76 +++++++++++++++++---------- iac/wfctlhelpers/apply_hooks_test.go | 77 ++++++++++++++++++++++------ 2 files changed, 110 insertions(+), 43 deletions(-) diff --git a/iac/wfctlhelpers/apply.go b/iac/wfctlhelpers/apply.go index d18670e7..8ad21189 100644 --- a/iac/wfctlhelpers/apply.go +++ b/iac/wfctlhelpers/apply.go @@ -111,24 +111,27 @@ type ApplyPlanHooks struct { OnResourceApplied func(context.Context, interfaces.ResourceDriver, interfaces.PlanAction, interfaces.ResourceOutput) error OnResourceDeleted func(context.Context, interfaces.PlanAction) error // OnPlanComplete fires once after the per-action loop reaches its - // natural success completion (apply.go:336), i.e., when the outer - // function is about to return (result, nil). Used by the DO plugin's - // deferred-flush integration via IaCProviderFinalizer.FinalizeApply RPC. + // natural success-exit return at the end of + // applyPlanWithEnvProviderAndHooks, i.e., when the outer function is + // about to return (result, nil). Used by the DO plugin's deferred- + // flush integration via IaCProviderFinalizer.FinalizeApply RPC. // // Does NOT fire on: - // - Pre-loop preflight error early-return (apply.go:192) — + // - The preflightProviderOwnedReplaceWithDeleteHooks early-return — // loopReached=false, no cloud work happened. - // - Per-action hook fatalErr bubbling to outer return - // (apply.go:319-324) — outer err != nil. v1 semantic preservation - // per cycle-1 plan-review C-3: DOProvider.Apply skips deferred- - // flush when wfctlhelpers.ApplyPlan returns a top-level err - // (internal/provider.go:276-282). - // - Post-loop length-invariant violation (apply.go:333) — - // outer err != nil. + // - The per-action loop's `if fatalErr != nil { return ... }` + // early-return — outer err != nil. v1 semantic preservation per + // cycle-1 plan-review C-3: DOProvider.Apply skips deferred-flush + // when wfctlhelpers.ApplyPlan returns a top-level err (the + // `if err != nil { return ... }` guard in DOProvider.Apply + // immediately after the ApplyPlan call in + // workflow-plugin-digitalocean internal/provider.go). + // - The post-loop length-invariant check that compares + // len(result.Actions) against len(plan.Actions) — outer err != nil. // // DOES fire on per-action driver-error paths (best-effort; driver // errors append to result.Errors but do NOT set fatalErr; the loop - // continues and reaches the success exit at 336). Mirrors v1 + // continues and reaches the natural success-exit return). Mirrors v1 // DOProvider.Apply behavior where the deferred-flush ran whenever // the wrapped Apply returned nil err, regardless of per-action // result.Errors entries. @@ -179,26 +182,47 @@ func applyPlanWithEnvProviderAndHooks( return } if err != nil { - // v1 semantic preservation: outer-error exits (per-action - // hook fatalErr at apply.go:319-324, post-loop length- - // invariant at 333) skip finalize, matching DOProvider.Apply's - // "return without flushing on top-level err" at - // internal/provider.go:276-282. + // v1 semantic preservation: outer-error exits (the per-action + // loop's `if fatalErr != nil { return ... }` early-return and + // the post-loop length-invariant check) skip finalize, + // matching DOProvider.Apply's "return without flushing on + // top-level err" behavior (the `if err != nil { return ... }` + // guard immediately after the wrapped ApplyPlan call in + // workflow-plugin-digitalocean internal/provider.go). return } - if hookErr := hooks.OnPlanComplete(ctx); hookErr != nil { - // Append to result.Errors so callers see per-driver - // attribution alongside the finalize-attributed failure. - // Surface to outer err so the caller observes the failure - // at the function boundary (outer err was nil; finalize - // failure now becomes the outer err). - finalizeErr := fmt.Errorf("plan finalize: %w", hookErr) + // Symmetry with the drift defer below: OnPlanComplete is a + // caller-provided closure (wired by wfctl in Task 5). A panic + // inside it would propagate past the defer chain and prevent + // the caller from observing result/err, so wrap with recover() + // and convert the panic into a finalize-attributed err entry. + var hookErr error + func() { + defer func() { + if r := recover(); r != nil { + hookErr = fmt.Errorf("OnPlanComplete panicked: %v", r) + } + }() + hookErr = hooks.OnPlanComplete(ctx) + }() + if hookErr != nil { + // Append per-driver-attribution entry so callers iterating + // result.Errors see the finalize-attributed failure + // distinctly from per-action driver errors. Pass the raw + // hookErr.Error() — the structured Resource="" + // + Action="finalize" fields already carry the attribution; + // a "plan finalize:" string prefix here would double-attribute + // when callers format as "/: ". result.Errors = append(result.Errors, interfaces.ActionError{ Resource: "", Action: "finalize", - Error: finalizeErr.Error(), + Error: hookErr.Error(), }) - err = finalizeErr + // Outer err carries the "plan finalize:" prefix because the + // outer-err caller path lacks the structured Resource/Action + // fields — the prefix supplies that missing attribution + // inline. errors.Is round-trips on the wrapped hookErr. + err = fmt.Errorf("plan finalize: %w", hookErr) } }() diff --git a/iac/wfctlhelpers/apply_hooks_test.go b/iac/wfctlhelpers/apply_hooks_test.go index 784dcb22..b4aeefec 100644 --- a/iac/wfctlhelpers/apply_hooks_test.go +++ b/iac/wfctlhelpers/apply_hooks_test.go @@ -304,11 +304,14 @@ func TestApplyPlanWithHooks_PopulatesActions_PreDispatchDriverError(t *testing.T // ───────────────────────────────────────────────────────────────────────────── // OnPlanComplete tests (workflow#695 Phase 2.5). Per plan task 2 + // cycle-1 plan-review C-3 v1 semantic preservation: OnPlanComplete fires -// ONLY on outer-success exits (apply.go:336), matching the DOProvider.Apply -// v1 wrapper's "return without flushing on top-level err" behavior at -// internal/provider.go:276-282. Outer errors at preflight (192), -// per-action fatalErr (324), and post-loop length-invariant (333) ALL -// skip finalize. +// ONLY on the natural success-exit return at the end of +// applyPlanWithEnvProviderAndHooks, matching the DOProvider.Apply v1 +// wrapper's "return without flushing on top-level err" behavior (the +// `if err != nil { return ... }` guard immediately after the wrapped +// ApplyPlan call in workflow-plugin-digitalocean internal/provider.go). +// Outer errors at the preflightProviderOwnedReplaceWithDeleteHooks +// early-return, the per-action loop's fatalErr early-return, and the +// post-loop length-invariant check ALL skip finalize. // ───────────────────────────────────────────────────────────────────────────── // TestApplyPlanWithHooks_OnPlanComplete_FiresOnCleanSuccess verifies that @@ -392,10 +395,12 @@ func TestApplyPlanWithHooks_OnPlanComplete_SurfacesErrorToCaller(t *testing.T) { // TestApplyPlanWithHooks_OnPlanComplete_SkippedOnOuterError verifies the v1 // semantic-preservation gate (cycle-1 plan-review C-3): when a per-action -// hook returns an error, fatalErr bubbles to outer return at apply.go:324 -// with non-nil err. OnPlanComplete MUST NOT fire on that exit, matching -// DOProvider.Apply's "return without flushing on top-level err" behavior -// at internal/provider.go:276-282. +// hook returns an error, fatalErr bubbles to the per-action loop's +// `if fatalErr != nil { return ... }` early-return with non-nil outer err. +// OnPlanComplete MUST NOT fire on that exit, matching DOProvider.Apply's +// "return without flushing on top-level err" behavior (the +// `if err != nil { return ... }` guard immediately after the wrapped +// ApplyPlan call in workflow-plugin-digitalocean internal/provider.go). func TestApplyPlanWithHooks_OnPlanComplete_SkippedOnOuterError(t *testing.T) { p := newFakeProvider() plan := &interfaces.IaCPlan{ @@ -407,8 +412,10 @@ func TestApplyPlanWithHooks_OnPlanComplete_SkippedOnOuterError(t *testing.T) { hookSentinel := errors.New("post-apply hook failure") var finalizeFired bool hooks := ApplyPlanHooks{ - // OnResourceApplied returning err sets fatalErr → outer err != nil - // (apply.go:308-314 → 319-324). True outer-error path. + // OnResourceApplied returning err sets fatalErr inside the per- + // action loop's deferred dispatch closure → bubbles to the loop's + // `if fatalErr != nil { return ... }` early-return with non-nil + // outer err. True outer-error path. OnResourceApplied: func(_ context.Context, _ interfaces.ResourceDriver, _ interfaces.PlanAction, _ interfaces.ResourceOutput) error { return hookSentinel }, @@ -430,16 +437,17 @@ func TestApplyPlanWithHooks_OnPlanComplete_SkippedOnOuterError(t *testing.T) { } // TestApplyPlanWithHooks_OnPlanComplete_DoesNotFireOnPreloopError verifies -// that pre-loop preflight failures (apply.go:192) skip OnPlanComplete — -// loopReached is still false when preflight returns early, so the deferred -// closure's first short-circuit triggers and finalize never runs. -// Regression guard: no cloud work happened, so no finalize work should -// happen either. +// that pre-loop preflight failures (the +// preflightProviderOwnedReplaceWithDeleteHooks early-return) skip +// OnPlanComplete — loopReached is still false when preflight returns +// early, so the deferred closure's first short-circuit triggers and +// finalize never runs. Regression guard: no cloud work happened, so no +// finalize work should happen either. func TestApplyPlanWithHooks_OnPlanComplete_DoesNotFireOnPreloopError(t *testing.T) { // fakeReplacerDriver implements interfaces.ResourceReplacer (defined in // apply_replacer_dispatch_test.go); combined with deleteHookActive // (OnResourceDeleted set), preflightProviderOwnedReplaceWithDeleteHooks - // returns the engine-side rejection error at apply.go:367. + // returns its engine-side ResourceReplacer rejection error. provider := &hookProvider{driver: &fakeReplacerDriver{}} plan := &interfaces.IaCPlan{ ID: "plan-preflight-fail", @@ -468,3 +476,38 @@ func TestApplyPlanWithHooks_OnPlanComplete_DoesNotFireOnPreloopError(t *testing. t.Error("OnPlanComplete fired on pre-loop preflight error — should not fire when loopReached=false") } } + +// TestApplyPlanWithHooks_OnPlanComplete_RecoversFromPanic verifies that a +// panic inside the caller-provided OnPlanComplete closure does NOT +// propagate past the deferred closure — it's caught by recover() and +// surfaced as a finalize-attributed err entry on result.Errors plus an +// outer err. Symmetry with the drift defer's recover() (apply.go's +// post-loop input-drift postcondition) which has the same posture for +// caller-provided env-provider closures. +func TestApplyPlanWithHooks_OnPlanComplete_RecoversFromPanic(t *testing.T) { + p := newFakeProvider() + plan := &interfaces.IaCPlan{ + ID: "plan-panic", + Actions: []interfaces.PlanAction{ + {Action: "create", Resource: interfaces.ResourceSpec{Name: "r1", Type: "infra.test"}}, + }, + } + hooks := ApplyPlanHooks{ + OnPlanComplete: func(_ context.Context) error { + panic("finalize impl bug") + }, + } + result, err := ApplyPlanWithHooks(t.Context(), p, plan, hooks) + if err == nil { + t.Fatal("expected outer err from recovered OnPlanComplete panic") + } + if !strings.Contains(err.Error(), "OnPlanComplete panicked") { + t.Errorf("expected err to mention panic recovery; got: %v", err) + } + if !strings.Contains(err.Error(), "finalize impl bug") { + t.Errorf("expected err to carry panic value; got: %v", err) + } + if len(result.Errors) == 0 || result.Errors[len(result.Errors)-1].Resource != "" { + t.Errorf("expected last result.Errors entry to have Resource=\"\"; got: %+v", result.Errors) + } +} From 09fb61ad5f33b063615d167a22fd55541dfee56b Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 17 May 2026 02:44:52 -0400 Subject: [PATCH 05/11] feat(sdk): auto-register IaCProviderFinalizer optional service (Phase 2.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the IaCProviderFinalizer service to registerIaCServicesOnly's optional-services block, placed immediately before the ResourceDriver registration and after IaCProviderDriftConfigDetector — same opt-in posture as every other IaCProvider* optional (per-interface type-assert, skip on miss; absence of registration is the negative signal per ADR 0024). Tests (iacserver_test.go, +2 tests + 1 stub): - TestRegisterAll_RegistersIaCProviderFinalizer: provider embedding pb.UnimplementedIaCProviderFinalizerServer → service in grpcSrv.GetServiceInfo()["workflow.plugin.external.iac.IaCProviderFinalizer"]. - TestRegisterAll_SkipsIaCProviderFinalizerWhenNotImplemented: provider without the finalizer embed → service absent. Locks the negative- signal contract that wfctl-side adapter.Finalizer() (Task 4) relies on. Per workflow#695 Phase 2.5 / ADR 0024. Co-Authored-By: Claude Opus 4.7 --- plugin/external/sdk/iacserver.go | 9 ++++++ plugin/external/sdk/iacserver_test.go | 41 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/plugin/external/sdk/iacserver.go b/plugin/external/sdk/iacserver.go index 39d1a590..5b00c142 100644 --- a/plugin/external/sdk/iacserver.go +++ b/plugin/external/sdk/iacserver.go @@ -156,6 +156,15 @@ func registerIaCServicesOnly(s *grpc.Server, provider any) error { if v, ok := provider.(pb.IaCProviderDriftConfigDetectorServer); ok { pb.RegisterIaCProviderDriftConfigDetectorServer(s, v) } + // IaCProviderFinalizer is the workflow#695 Phase 2.5 optional service + // for plugins needing a post-apply-loop finalizer hook under v2 + // dispatch. Per ADR 0024 the absence of this registration IS the + // negative signal (no compat shim, no NotSupported flag) — wfctl-side + // adapter.Finalizer() probes service-presence to gate OnPlanComplete + // wiring (Task 4 + Task 5). + if v, ok := provider.(pb.IaCProviderFinalizerServer); ok { + pb.RegisterIaCProviderFinalizerServer(s, v) + } if v, ok := provider.(pb.ResourceDriverServer); ok { pb.RegisterResourceDriverServer(s, v) } diff --git a/plugin/external/sdk/iacserver_test.go b/plugin/external/sdk/iacserver_test.go index 1be7d915..77fdadde 100644 --- a/plugin/external/sdk/iacserver_test.go +++ b/plugin/external/sdk/iacserver_test.go @@ -138,6 +138,47 @@ type stateBackendProviderStub struct { pb.UnimplementedIaCStateBackendServer } +// TestRegisterAll_RegistersIaCProviderFinalizer asserts that a provider whose +// type also satisfies pb.IaCProviderFinalizerServer gets the +// IaCProviderFinalizer service auto-registered — same opt-in posture as the +// other IaCProvider* optionals. Per workflow#695 Phase 2.5 / ADR 0024 +// (absence of registration IS the negative signal; no compat shim). +func TestRegisterAll_RegistersIaCProviderFinalizer(t *testing.T) { + grpcSrv := grpc.NewServer() + provider := &finalizerProviderStub{} + if err := sdk.RegisterAllIaCProviderServices(grpcSrv, provider); err != nil { + t.Fatalf("unexpected error: %v", err) + } + info := grpcSrv.GetServiceInfo() + if _, ok := info["workflow.plugin.external.iac.IaCProviderFinalizer"]; !ok { + t.Fatalf("IaCProviderFinalizer service NOT registered despite provider satisfying interface; have: %v", serviceNames(info)) + } +} + +// TestRegisterAll_SkipsIaCProviderFinalizerWhenNotImplemented locks the +// negative signal contract: a provider that does NOT satisfy +// pb.IaCProviderFinalizerServer MUST NOT have the service registered. +// Per ADR 0024 + ADR 0040 invariant on optional services. +func TestRegisterAll_SkipsIaCProviderFinalizerWhenNotImplemented(t *testing.T) { + grpcSrv := grpc.NewServer() + provider := &fullProviderStub{} // no finalizer embed + if err := sdk.RegisterAllIaCProviderServices(grpcSrv, provider); err != nil { + t.Fatalf("unexpected error: %v", err) + } + info := grpcSrv.GetServiceInfo() + if _, ok := info["workflow.plugin.external.iac.IaCProviderFinalizer"]; ok { + t.Fatalf("IaCProviderFinalizer service WAS registered despite provider not satisfying interface; have: %v", serviceNames(info)) + } +} + +// finalizerProviderStub satisfies IaCProviderRequired (the required minimum +// for ServeIaCPlugin) AND IaCProviderFinalizer — representative of the DO +// plugin shape under workflow#695 Phase 2.5. +type finalizerProviderStub struct { + pb.UnimplementedIaCProviderRequiredServer + pb.UnimplementedIaCProviderFinalizerServer +} + func serviceNames(info map[string]grpc.ServiceInfo) []string { out := make([]string, 0, len(info)) for k := range info { From 09b84f92f86e17bfd10f485d1878421fc24de1fc Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 17 May 2026 02:52:19 -0400 Subject: [PATCH 06/11] fix(sdk): extend allCapabilitiesStub for Finalizer + regroup stub + drop roadmap-task IDs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three follow-ups on the IaCProviderFinalizer auto-registration (09fb61ad): 1. Extend the "all optionals" coverage contract for the 7th optional. allCapabilitiesStub previously embedded 6 optionals; under T3 it silently shrank from "every optional" to "6-of-7". Adds: - pb.UnimplementedIaCProviderFinalizerServer embed to allCapabilitiesStub. - "workflow.plugin.external.iac.IaCProviderFinalizer" to wantServices in TestRegisterAllIaCProviderServices_AllOptionals_AllRegistered. - Godoc update: "all 7 typed services (Required + 6 optional)" → "all 8 typed services (Required + 7 optional)". AllOptionals_AllRegistered now also locks the auto-registration contract for the new optional (alongside the dedicated positive + negative tests). 2. Move finalizerProviderStub from between the SkipsIaCProviderFinalizerWhenNotImplemented test and the serviceNames helper into the stubs block (right after enumeratorOnlyStub, where single-optional-capability stubs live). Restores the file's stubs-grouped-together convention. 3. Drop "(Task 4 + Task 5)" parenthetical from the registration block comment in iacserver.go. Roadmap-task IDs become historical artifacts after the cascade lands; replaced with stable pointers — the wfctl-side typed adapter file path (cmd/wfctl/iac_typed_adapter.go) and Finalizer() accessor symbol name. Same anchor-by-identifier pattern applied in T1/T2 fix-ups. Tests still 3/3 PASS (AllOptionals + Registers + Skips); full package race-clean. Co-Authored-By: Claude Opus 4.7 --- plugin/external/sdk/iacserver.go | 7 ++++--- plugin/external/sdk/iacserver_test.go | 20 +++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/plugin/external/sdk/iacserver.go b/plugin/external/sdk/iacserver.go index 5b00c142..ee149c78 100644 --- a/plugin/external/sdk/iacserver.go +++ b/plugin/external/sdk/iacserver.go @@ -159,9 +159,10 @@ func registerIaCServicesOnly(s *grpc.Server, provider any) error { // IaCProviderFinalizer is the workflow#695 Phase 2.5 optional service // for plugins needing a post-apply-loop finalizer hook under v2 // dispatch. Per ADR 0024 the absence of this registration IS the - // negative signal (no compat shim, no NotSupported flag) — wfctl-side - // adapter.Finalizer() probes service-presence to gate OnPlanComplete - // wiring (Task 4 + Task 5). + // negative signal (no compat shim, no NotSupported flag) — the + // wfctl-side typed adapter (cmd/wfctl/iac_typed_adapter.go) + // service-presence-probes via its Finalizer() accessor and gates + // the ApplyPlanHooks.OnPlanComplete wiring on a non-nil return. if v, ok := provider.(pb.IaCProviderFinalizerServer); ok { pb.RegisterIaCProviderFinalizerServer(s, v) } diff --git a/plugin/external/sdk/iacserver_test.go b/plugin/external/sdk/iacserver_test.go index 77fdadde..cd5203b1 100644 --- a/plugin/external/sdk/iacserver_test.go +++ b/plugin/external/sdk/iacserver_test.go @@ -88,7 +88,7 @@ func TestRegisterAllIaCProviderServices_TypedNilPointer_ReturnsError(t *testing. // TestRegisterAllIaCProviderServices_AllOptionals_AllRegistered // asserts that a provider satisfying every optional + required interface -// triggers registration of all 7 typed services (Required + 6 optional) +// triggers registration of all 8 typed services (Required + 7 optional) // plus the ResourceDriver. func TestRegisterAllIaCProviderServices_AllOptionals_AllRegistered(t *testing.T) { grpcSrv := grpc.NewServer() @@ -105,6 +105,7 @@ func TestRegisterAllIaCProviderServices_AllOptionals_AllRegistered(t *testing.T) "workflow.plugin.external.iac.IaCProviderMigrationRepairer", "workflow.plugin.external.iac.IaCProviderValidator", "workflow.plugin.external.iac.IaCProviderDriftConfigDetector", + "workflow.plugin.external.iac.IaCProviderFinalizer", "workflow.plugin.external.iac.ResourceDriver", } for _, name := range wantServices { @@ -171,14 +172,6 @@ func TestRegisterAll_SkipsIaCProviderFinalizerWhenNotImplemented(t *testing.T) { } } -// finalizerProviderStub satisfies IaCProviderRequired (the required minimum -// for ServeIaCPlugin) AND IaCProviderFinalizer — representative of the DO -// plugin shape under workflow#695 Phase 2.5. -type finalizerProviderStub struct { - pb.UnimplementedIaCProviderRequiredServer - pb.UnimplementedIaCProviderFinalizerServer -} - func serviceNames(info map[string]grpc.ServiceInfo) []string { out := make([]string, 0, len(info)) for k := range info { @@ -201,6 +194,14 @@ type enumeratorOnlyStub struct { pb.UnimplementedIaCProviderEnumeratorServer } +// finalizerProviderStub satisfies IaCProviderRequired (the required minimum +// for ServeIaCPlugin) AND IaCProviderFinalizer — representative of the DO +// plugin shape under workflow#695 Phase 2.5. +type finalizerProviderStub struct { + pb.UnimplementedIaCProviderRequiredServer + pb.UnimplementedIaCProviderFinalizerServer +} + // allCapabilitiesStub satisfies every required + optional IaC service plus // ResourceDriver — used to assert auto-registration covers the full surface. type allCapabilitiesStub struct { @@ -211,6 +212,7 @@ type allCapabilitiesStub struct { pb.UnimplementedIaCProviderMigrationRepairerServer pb.UnimplementedIaCProviderValidatorServer pb.UnimplementedIaCProviderDriftConfigDetectorServer + pb.UnimplementedIaCProviderFinalizerServer pb.UnimplementedResourceDriverServer } From 3d88d701f8ae2e119a469a2757b41ec8cf02943c Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 17 May 2026 03:14:11 -0400 Subject: [PATCH 07/11] feat(wfctl): typedIaCAdapter Finalizer() accessor for IaCProviderFinalizer optional service (Phase 2.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds 4 sub-changes to cmd/wfctl/iac_typed_adapter.go mirroring the existing optional-client pattern (Enumerator/DriftDetector/etc.): (a) iacServiceFinalizer constant "workflow.plugin.external.iac.IaCProviderFinalizer" — placed in the iacService* constant block between iacServiceDriftConfigDetect and iacServiceResourceDriver to keep the block's registration-order ordering stable with the SDK's auto-registration block. (b) finalizer pb.IaCProviderFinalizerClient field on typedIaCAdapter struct, placed in the optional-clients section between driftCfg and resourceDriv (matches accessor + constructor ordering). (c) Constructor branch in newTypedIaCAdapter: if registered[iacServiceFinalizer] { a.finalizer = pb.NewIaCProviderFinalizerClient(conn) } Same shape as the 6 sibling optional-client conditionals. (d) Finalizer() pb.IaCProviderFinalizerClient accessor returning a.finalizer. Godoc enumerates the wfctl-side consumer (statePersistenceHooks helper in cmd/wfctl/infra_apply.go) that gates OnPlanComplete wiring on a non-nil return, citing ADR 0024's negative-signal contract and workflow#695 Phase 2.5. Tests (cmd/wfctl/iac_typed_adapter_test.go, +2 tests + 1 helper): - TestTypedAdapter_Finalizer_PopulatedWhenRegistered: exercises the constructor branch (map[iacServiceFinalizer]=true → accessor non-nil). - TestTypedAdapter_Finalizer_NilWhenNotRegistered: locks the negative signal (map without finalizer key → accessor nil) — contract the downstream OnPlanComplete wiring relies on. - dialLazyConn helper: returns a real *grpc.ClientConn from grpc.NewClient targeting an unreachable loopback port. The underlying handshake is deferred to first RPC; the conn is valid for adapter field-wiring assertions without spawning a server. t.Cleanup drains the dial pool between tests. Per workflow#695 Phase 2.5 / ADR 0024. Co-Authored-By: Claude Opus 4.7 --- cmd/wfctl/iac_typed_adapter.go | 16 +++++++++ cmd/wfctl/iac_typed_adapter_test.go | 51 +++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/cmd/wfctl/iac_typed_adapter.go b/cmd/wfctl/iac_typed_adapter.go index b4f9752d..2ec75d73 100644 --- a/cmd/wfctl/iac_typed_adapter.go +++ b/cmd/wfctl/iac_typed_adapter.go @@ -54,6 +54,7 @@ const ( iacServiceMigrationRepairer = "workflow.plugin.external.iac.IaCProviderMigrationRepairer" iacServiceValidator = "workflow.plugin.external.iac.IaCProviderValidator" iacServiceDriftConfigDetect = "workflow.plugin.external.iac.IaCProviderDriftConfigDetector" + iacServiceFinalizer = "workflow.plugin.external.iac.IaCProviderFinalizer" iacServiceResourceDriver = "workflow.plugin.external.iac.ResourceDriver" ) @@ -78,6 +79,7 @@ type typedIaCAdapter struct { repairer pb.IaCProviderMigrationRepairerClient validator pb.IaCProviderValidatorClient driftCfg pb.IaCProviderDriftConfigDetectorClient + finalizer pb.IaCProviderFinalizerClient resourceDriv pb.ResourceDriverClient // cachedCaps memoizes the plugin's CapabilitiesResponse. Access via @@ -116,6 +118,9 @@ func newTypedIaCAdapter(conn *grpc.ClientConn, registered map[string]bool) *type if registered[iacServiceDriftConfigDetect] { a.driftCfg = pb.NewIaCProviderDriftConfigDetectorClient(conn) } + if registered[iacServiceFinalizer] { + a.finalizer = pb.NewIaCProviderFinalizerClient(conn) + } if registered[iacServiceResourceDriver] { a.resourceDriv = pb.NewResourceDriverClient(conn) } @@ -220,6 +225,17 @@ func (a *typedIaCAdapter) ResourceDriverClient() pb.ResourceDriverClient { return a.resourceDriv } +// Finalizer returns the typed pb.IaCProviderFinalizerClient or nil when +// the plugin did not register IaCProviderFinalizer. Used by the v2 apply +// path (cmd/wfctl/infra_apply.go's statePersistenceHooks helper) to gate +// the ApplyPlanHooks.OnPlanComplete wiring on service-presence — a nil +// return means no FinalizeApply RPC is invoked. Per ADR 0024 the absence +// of the registration is the negative signal (no compat shim, no +// NotSupported flag). Per workflow#695 Phase 2.5. +func (a *typedIaCAdapter) Finalizer() pb.IaCProviderFinalizerClient { + return a.finalizer +} + // 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 diff --git a/cmd/wfctl/iac_typed_adapter_test.go b/cmd/wfctl/iac_typed_adapter_test.go index ec44149b..e7757b6b 100644 --- a/cmd/wfctl/iac_typed_adapter_test.go +++ b/cmd/wfctl/iac_typed_adapter_test.go @@ -359,6 +359,57 @@ func (p *countingCapabilitiesProvider) Capabilities(_ context.Context, _ *pb.Cap return &pb.CapabilitiesResponse{ComputePlanVersion: p.computePlanVersion}, nil } +// ─── IaCProviderFinalizer accessor tests (workflow#695 Phase 2.5) ────────── + +// TestTypedAdapter_Finalizer_PopulatedWhenRegistered verifies that +// newTypedIaCAdapter wires the pb.IaCProviderFinalizerClient when the +// plugin's ContractRegistry advertised the IaCProviderFinalizer service. +// Per workflow#695 Phase 2.5 / ADR 0024 (service-presence is the opt-in +// signal — no NotSupported flag, no compat shim). +func TestTypedAdapter_Finalizer_PopulatedWhenRegistered(t *testing.T) { + conn := dialLazyConn(t) + adapter := newTypedIaCAdapter(conn, map[string]bool{ + iacServiceFinalizer: true, + }) + if adapter.Finalizer() == nil { + t.Error("Finalizer() returned nil when IaCProviderFinalizer is in registered set") + } +} + +// TestTypedAdapter_Finalizer_NilWhenNotRegistered verifies the negative +// signal — when the plugin did not advertise IaCProviderFinalizer, the +// accessor returns nil so the wfctl-side OnPlanComplete wiring (Task 5) +// stays unset and no finalize RPC is invoked. Locks the contract that +// downstream consumers gate on. +func TestTypedAdapter_Finalizer_NilWhenNotRegistered(t *testing.T) { + conn := dialLazyConn(t) + adapter := newTypedIaCAdapter(conn, map[string]bool{ + iacServiceEnumerator: true, // arbitrary other service, no Finalizer + }) + if adapter.Finalizer() != nil { + t.Error("Finalizer() returned non-nil when IaCProviderFinalizer not registered") + } +} + +// dialLazyConn returns a real *grpc.ClientConn that has NOT performed any +// I/O. grpc.NewClient defers the connection-establish handshake to the +// first RPC, so dialing a non-listening loopback port returns a valid +// (lazy) conn without error — sufficient for tests that only assert +// adapter field-wiring (no actual RPC). Caller schedules Close via +// t.Cleanup so the dial pool drains between tests. +func dialLazyConn(t *testing.T) *grpc.ClientConn { + t.Helper() + conn, err := grpc.NewClient( + "127.0.0.1:1", // unreachable port; we never RPC, so no dial happens + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + t.Fatalf("grpc.NewClient: %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + return conn +} + // 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 From 7c3e9b6fc5f60eb542e58c25fd75ee66ae74185b Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 17 May 2026 03:24:23 -0400 Subject: [PATCH 08/11] fix(wfctl): soften Finalizer godoc tense + anchor test godoc by symbol + harden dialLazyConn against grpc-go behavior drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three follow-up Minors on the Finalizer accessor (3d88d701) — no blockers, all author-discretion quality fixes consistent with T1/T2/T3 cleanup posture: 1. Finalizer accessor godoc: "Used by the v2 apply path..." → "Intended for use by the v2 apply path...". statePersistenceHooks in cmd/wfctl/infra_apply.go does NOT yet call adapter.Finalizer() — that wiring lands in the next implement task. Future-tense closes the per-commit-window where the claim is verifiably false. 2. TestTypedAdapter_Finalizer_NilWhenNotRegistered godoc: dropped "(Task 5)" roadmap-task ID; anchored to "statePersistenceHooks (cmd/wfctl/infra_apply.go)" — stable symbol + file path that survives cascade closure (same fix applied to T3's iacserver.go godoc in 09b84f92). 3. dialLazyConn helper: switched from grpc.NewClient against an unreachable port (relying on grpc-go's NewClient-defers-dial semantic, which is documented but not stability-guaranteed) to a real net.Listen + grpc.NewServer with zero services registered. Conn now dials a live but service-empty server, so field-wiring tests survive a future grpc-go release switching to eager-dial. t.Cleanup drains both server + conn for test isolation. Tests still 2/2 PASS; full package race-clean. Co-Authored-By: Claude Opus 4.7 --- cmd/wfctl/iac_typed_adapter.go | 10 ++++---- cmd/wfctl/iac_typed_adapter_test.go | 37 ++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/cmd/wfctl/iac_typed_adapter.go b/cmd/wfctl/iac_typed_adapter.go index 2ec75d73..9326af3b 100644 --- a/cmd/wfctl/iac_typed_adapter.go +++ b/cmd/wfctl/iac_typed_adapter.go @@ -226,11 +226,11 @@ func (a *typedIaCAdapter) ResourceDriverClient() pb.ResourceDriverClient { } // Finalizer returns the typed pb.IaCProviderFinalizerClient or nil when -// the plugin did not register IaCProviderFinalizer. Used by the v2 apply -// path (cmd/wfctl/infra_apply.go's statePersistenceHooks helper) to gate -// the ApplyPlanHooks.OnPlanComplete wiring on service-presence — a nil -// return means no FinalizeApply RPC is invoked. Per ADR 0024 the absence -// of the registration is the negative signal (no compat shim, no +// the plugin did not register IaCProviderFinalizer. Intended for use by +// the v2 apply path's statePersistenceHooks helper (cmd/wfctl/infra_apply.go) +// to gate the ApplyPlanHooks.OnPlanComplete wiring on service-presence — +// a nil return means no FinalizeApply RPC is invoked. Per ADR 0024 the +// absence of the registration is the negative signal (no compat shim, no // NotSupported flag). Per workflow#695 Phase 2.5. func (a *typedIaCAdapter) Finalizer() pb.IaCProviderFinalizerClient { return a.finalizer diff --git a/cmd/wfctl/iac_typed_adapter_test.go b/cmd/wfctl/iac_typed_adapter_test.go index e7757b6b..1b1d2f86 100644 --- a/cmd/wfctl/iac_typed_adapter_test.go +++ b/cmd/wfctl/iac_typed_adapter_test.go @@ -378,9 +378,10 @@ func TestTypedAdapter_Finalizer_PopulatedWhenRegistered(t *testing.T) { // TestTypedAdapter_Finalizer_NilWhenNotRegistered verifies the negative // signal — when the plugin did not advertise IaCProviderFinalizer, the -// accessor returns nil so the wfctl-side OnPlanComplete wiring (Task 5) -// stays unset and no finalize RPC is invoked. Locks the contract that -// downstream consumers gate on. +// accessor returns nil so the wfctl-side OnPlanComplete wiring in +// statePersistenceHooks (cmd/wfctl/infra_apply.go) stays unset and no +// finalize RPC is invoked. Locks the contract that downstream consumers +// gate on. func TestTypedAdapter_Finalizer_NilWhenNotRegistered(t *testing.T) { conn := dialLazyConn(t) adapter := newTypedIaCAdapter(conn, map[string]bool{ @@ -391,22 +392,36 @@ func TestTypedAdapter_Finalizer_NilWhenNotRegistered(t *testing.T) { } } -// dialLazyConn returns a real *grpc.ClientConn that has NOT performed any -// I/O. grpc.NewClient defers the connection-establish handshake to the -// first RPC, so dialing a non-listening loopback port returns a valid -// (lazy) conn without error — sufficient for tests that only assert -// adapter field-wiring (no actual RPC). Caller schedules Close via -// t.Cleanup so the dial pool drains between tests. +// dialLazyConn returns a real *grpc.ClientConn pointing at an in-process +// gRPC server with zero services registered. Used by adapter field-wiring +// tests that need newTypedIaCAdapter's `pb.NewXxxClient(conn)` calls to +// succeed without invoking any RPC. Spinning up a real listener (vs +// relying on grpc-go's NewClient-defers-dial behavior) keeps the helper +// robust against future grpc-go releases that might switch to eager +// dialing — the conn dials a live but service-empty server, so the +// field-wiring assertion always represents what we mean to test (a real +// dial-back conn) rather than a happens-to-be-deferred sentinel. +// t.Cleanup drains both server + conn so test isolation is preserved. func dialLazyConn(t *testing.T) *grpc.ClientConn { t.Helper() + lis, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("net.Listen: %v", err) + } + srv := grpc.NewServer() + go func() { _ = srv.Serve(lis) }() conn, err := grpc.NewClient( - "127.0.0.1:1", // unreachable port; we never RPC, so no dial happens + lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { + srv.Stop() t.Fatalf("grpc.NewClient: %v", err) } - t.Cleanup(func() { _ = conn.Close() }) + t.Cleanup(func() { + _ = conn.Close() + srv.Stop() + }) return conn } From 461de7574dafc6d38afc2a3a5556453158278bf1 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 17 May 2026 03:39:37 -0400 Subject: [PATCH 09/11] feat(wfctl): wire OnPlanComplete hook to IaCProviderFinalizer via shared statePersistenceHooks helper (Phase 2.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the SHARED statePersistenceHooks helper in cmd/wfctl/infra_apply.go to wire the workflow#695 Phase 2.5 OnPlanComplete hook through the v2 dispatch path. Per cycle-1 plan-review I-2, the single helper edit covers BOTH v2 dispatch sites — both call sites pass plan.ID through the new planID string parameter. Changes: - Add planID string parameter to statePersistenceHooks (positioned between providerType and hydratedOut to minimize positional churn). - Add OnPlanComplete closure to the returned ApplyPlanHooks. The closure: * Type-asserts provider to *typedIaCAdapter; no-ops on non-adapter shapes (in-process fakes, legacy providers) for backward compat. * Calls adapter.Finalizer() — returns nil when the plugin did not register IaCProviderFinalizer (ADR 0024 negative signal). No-op in that case preserves pre-Phase-2.5 behavior for plugins that don't opt in. * Invokes FinalizeApply RPC with the plan ID. Wraps gRPC transport errors with "FinalizeApply gRPC: %w". * Aggregates per-driver errors from FinalizeApplyResponse.errors into a single err message preserving the v1 wrapper's Resource/Action/Error attribution shape (per ADR 0040). The engine-side defer in apply.go's OnPlanComplete handler appends the err to result.Errors as the "" entry and surfaces wrapped to outer caller err. - Update both v2-dispatch call sites to pass plan.ID through the new parameter. - Add pb "github.com/GoCodeAlone/workflow/plugin/external/proto" import. Verification: - GOWORK=off go build ./... → clean - GOWORK=off go test ./iac/wfctlhelpers/ ./plugin/external/sdk/ ./cmd/wfctl/ -race -count=1 → 3/3 packages PASS - GOWORK=off golangci-lint run --timeout=10m → 0 issues Per workflow#695 Phase 2.5 / ADR 0024 / ADR 0040. Co-Authored-By: Claude Opus 4.7 --- cmd/wfctl/infra_apply.go | 51 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/cmd/wfctl/infra_apply.go b/cmd/wfctl/infra_apply.go index aefef0aa..da688cbc 100644 --- a/cmd/wfctl/infra_apply.go +++ b/cmd/wfctl/infra_apply.go @@ -19,6 +19,7 @@ import ( "github.com/GoCodeAlone/workflow/iac/wfctlhelpers" "github.com/GoCodeAlone/workflow/interfaces" "github.com/GoCodeAlone/workflow/platform" + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" "github.com/GoCodeAlone/workflow/secrets" ) @@ -469,7 +470,7 @@ func applyWithProviderAndStore(ctx context.Context, provider interfaces.IaCProvi // call site (per dispatch.go contract). if wfctlhelpers.DispatchVersionFor(provider) == wfctlhelpers.DispatchVersionV2 { usedV2Dispatch = true - hooks := statePersistenceHooks(store, secretsProvider, provider, providerType, hydratedOut) + hooks := statePersistenceHooks(store, secretsProvider, provider, providerType, plan.ID, hydratedOut) result, err = applyV2ApplyPlanWithHooksFn(ctx, provider, &plan, hooks) // printDriftReportIfAny was added unwired in W-3a/T3.1.5; the // v2 dispatch is the production caller that surfaces input @@ -594,6 +595,7 @@ func statePersistenceHooks( secretsProvider secrets.Provider, provider interfaces.IaCProvider, providerType string, + planID string, hydratedOut map[string]string, ) wfctlhelpers.ApplyPlanHooks { return wfctlhelpers.ApplyPlanHooks{ @@ -613,6 +615,51 @@ func statePersistenceHooks( OnResourceDeleted: func(ctx context.Context, action interfaces.PlanAction) error { return deleteStateAfterCloudDelete(store, action.Resource.Name) }, + // OnPlanComplete is the workflow#695 Phase 2.5 hook that bridges + // the v2 apply path to the plugin's optional IaCProviderFinalizer + // service. Fires exactly once on the natural success-exit return + // of applyPlanWithEnvProviderAndHooks (v1 semantic preservation + // per cycle-1 plan-review C-3 — see ApplyPlanHooks.OnPlanComplete + // godoc for fire/no-fire enumeration). + // + // No-op paths (preserve pre-Phase-2.5 behavior): + // - provider is not a *typedIaCAdapter (in-process fakes, + // legacy provider shapes): no Finalizer() accessor available; + // skip silently. + // - adapter.Finalizer() returns nil (plugin did not register + // IaCProviderFinalizer per ADR 0024): skip silently. Plugins + // opt in via service registration; absence = no firing. + // + // Fire path: invoke FinalizeApply RPC; on gRPC transport error + // surface wrapped; on per-driver errors in response, aggregate + // the per-driver attribution into a single err message that + // preserves the Resource/Action/Error shape from the v1 wrapper + // (per ADR 0040 invariant on per-driver attribution). The engine + // closure in apply.go's deferred OnPlanComplete handler appends + // the returned err to result.Errors as the "" + // entry and surfaces wrapped to outer caller err. + OnPlanComplete: func(ctx context.Context) error { + adapter, ok := provider.(*typedIaCAdapter) + if !ok { + return nil + } + fin := adapter.Finalizer() + if fin == nil { + return nil + } + resp, callErr := fin.FinalizeApply(ctx, &pb.FinalizeApplyRequest{PlanId: planID}) + if callErr != nil { + return fmt.Errorf("FinalizeApply gRPC: %w", callErr) + } + if len(resp.GetErrors()) > 0 { + msgs := make([]string, 0, len(resp.GetErrors())) + for _, e := range resp.GetErrors() { + msgs = append(msgs, fmt.Sprintf("%s/%s: %s", e.GetResource(), e.GetAction(), e.GetError())) + } + return fmt.Errorf("plugin finalize: %d driver(s) failed: %s", len(resp.GetErrors()), strings.Join(msgs, "; ")) + } + return nil + }, } } @@ -1606,7 +1653,7 @@ func applyPrecomputedPlanWithStore(ctx context.Context, plan interfaces.IaCPlan, var usedV2Dispatch bool if wfctlhelpers.DispatchVersionFor(provider) == wfctlhelpers.DispatchVersionV2 { usedV2Dispatch = true - hooks := statePersistenceHooks(store, secretsProvider, provider, providerType, hydratedOut) + hooks := statePersistenceHooks(store, secretsProvider, provider, providerType, plan.ID, hydratedOut) result, err = applyV2ApplyPlanWithHooksFn(ctx, provider, &plan, hooks) if result != nil { printDriftReportIfAny(w, result) From 3f4ba2c8184d7ec38510c736870359cf2dd55e41 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 17 May 2026 03:55:49 -0400 Subject: [PATCH 10/11] test(wfctl): cover OnPlanComplete closure branches + flip Finalizer godoc + comment per-driver aggregator field order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three follow-ups on T5 (461de757): 1. Add cmd/wfctl/infra_apply_finalizer_test.go (~225 LOC) with 6 branch-coverage tests for the statePersistenceHooks OnPlanComplete closure — the closure carries load-bearing logic with a wire-visible error-format contract that no prior test exercised: - NonAdapterProviderNoOps: provider not *typedIaCAdapter → nil (Branch A, backward compat for in-process fakes / legacy provider shapes). - NilFinalizerNoOps: adapter.Finalizer() returns nil → nil (Branch B, ADR 0024 negative-signal preservation). - GRPCTransportError: FinalizeApply RPC fails → wrapped "FinalizeApply gRPC: %w" + status.FromError round-trip recovers codes.Internal (Branch C, retry/classifier contract). - AggregatesPerDriverErrors: response with errors[] → aggregated "plugin finalize: N driver(s) failed: R1/A1: E1; R2/A2: E2" with "; " separator (Branch D, ADR 0040 per-driver attribution + locked consumer-visible format string). - SuccessReturnsNil: empty errors[] response → nil (Branch E, clean success exit). - GRPCErrorPreservesErrorsIs: separate sanity-lock for errors.Is round-trip on the gRPC wrap (callers may classify via errors.Is in addition to status.FromError). Test scaffolding: - stubFinalizerServer satisfies pb.IaCProviderFinalizerServer with a caller-provided handler. - newFinalizerAdapter spins up an in-process gRPC server (Required + Finalizer registered) and returns a real *typedIaCAdapter wired via newTypedIaCAdapter. t.Cleanup drains server + conn. - Reuses dialLazyConn (defined in iac_typed_adapter_test.go) for Branch B which only needs a no-Finalizer adapter shape. 2. Flip Finalizer() accessor godoc from T4's interim "Intended for use by..." back to "Used by..." now that T5 has landed the statePersistenceHooks wiring. Closes the per-commit-window where the forward-tense claim made the godoc technically inaccurate. 3. Add inline comment at the OnPlanComplete closure's per-driver aggregator (Resource/Action field order) noting the pre-existing file-level inconsistency with the older applyWithProviderAndStore aggregator (Action/Resource order). The newer site matches proto field declaration order + apply.go's ActionError construction; reconciling the older site is tracked separately. Comment prevents a future contributor from "fixing" the new site back to the inconsistent ordering. Verification: - GOWORK=off go test ./cmd/wfctl/ -run 'TestStatePersistenceHooks_OnPlanComplete' -v -count=1 → 6/6 PASS - GOWORK=off go test ./iac/wfctlhelpers/ ./plugin/external/sdk/ ./cmd/wfctl/ -race -count=1 → 3/3 packages PASS race-clean (cmd/wfctl 96.5s) - GOWORK=off golangci-lint run --timeout=10m → 0 issues Co-Authored-By: Claude Opus 4.7 --- cmd/wfctl/iac_typed_adapter.go | 10 +- cmd/wfctl/infra_apply.go | 9 + cmd/wfctl/infra_apply_finalizer_test.go | 290 ++++++++++++++++++++++++ 3 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 cmd/wfctl/infra_apply_finalizer_test.go diff --git a/cmd/wfctl/iac_typed_adapter.go b/cmd/wfctl/iac_typed_adapter.go index 9326af3b..a5be02ba 100644 --- a/cmd/wfctl/iac_typed_adapter.go +++ b/cmd/wfctl/iac_typed_adapter.go @@ -226,11 +226,11 @@ func (a *typedIaCAdapter) ResourceDriverClient() pb.ResourceDriverClient { } // Finalizer returns the typed pb.IaCProviderFinalizerClient or nil when -// the plugin did not register IaCProviderFinalizer. Intended for use by -// the v2 apply path's statePersistenceHooks helper (cmd/wfctl/infra_apply.go) -// to gate the ApplyPlanHooks.OnPlanComplete wiring on service-presence — -// a nil return means no FinalizeApply RPC is invoked. Per ADR 0024 the -// absence of the registration is the negative signal (no compat shim, no +// the plugin did not register IaCProviderFinalizer. Used by the v2 apply +// path's statePersistenceHooks helper (cmd/wfctl/infra_apply.go) to gate +// the ApplyPlanHooks.OnPlanComplete wiring on service-presence — a nil +// return means no FinalizeApply RPC is invoked. Per ADR 0024 the absence +// of the registration is the negative signal (no compat shim, no // NotSupported flag). Per workflow#695 Phase 2.5. func (a *typedIaCAdapter) Finalizer() pb.IaCProviderFinalizerClient { return a.finalizer diff --git a/cmd/wfctl/infra_apply.go b/cmd/wfctl/infra_apply.go index da688cbc..133bb32f 100644 --- a/cmd/wfctl/infra_apply.go +++ b/cmd/wfctl/infra_apply.go @@ -654,6 +654,15 @@ func statePersistenceHooks( if len(resp.GetErrors()) > 0 { msgs := make([]string, 0, len(resp.GetErrors())) for _, e := range resp.GetErrors() { + // Field order is Resource/Action (matches proto field + // declaration order in ActionError + apply.go's + // ActionError construction). NOTE: applyWithProviderAndStore's + // existing per-resource aggregator above uses the inverse + // Action/Resource order — pre-existing file-level + // inconsistency, not introduced here. Reconciliation + // (flipping the older site to Resource/Action canonical + // order) is tracked separately; do NOT "fix" this site + // back to Action/Resource without flipping the other too. msgs = append(msgs, fmt.Sprintf("%s/%s: %s", e.GetResource(), e.GetAction(), e.GetError())) } return fmt.Errorf("plugin finalize: %d driver(s) failed: %s", len(resp.GetErrors()), strings.Join(msgs, "; ")) diff --git a/cmd/wfctl/infra_apply_finalizer_test.go b/cmd/wfctl/infra_apply_finalizer_test.go new file mode 100644 index 00000000..9ea6c26c --- /dev/null +++ b/cmd/wfctl/infra_apply_finalizer_test.go @@ -0,0 +1,290 @@ +package main + +// infra_apply_finalizer_test.go — branch-coverage tests for the +// OnPlanComplete closure built by statePersistenceHooks (workflow#695 +// Phase 2.5). Locks the wfctl-side wiring contract that the engine-side +// apply.go deferred handler relies on: +// +// - Branch A: provider is not a *typedIaCAdapter → no-op nil +// (backward compat — in-process fakes, legacy provider shapes). +// - Branch B: adapter.Finalizer() returns nil → no-op nil +// (ADR 0024 negative signal — plugin did not register +// IaCProviderFinalizer; no compat shim). +// - Branch C: FinalizeApply RPC returns a transport error → +// wrapped as "FinalizeApply gRPC: %w" (errors.Is round-trips). +// - Branch D: response has errors[] → aggregated into the +// consumer-visible format string +// "plugin finalize: N driver(s) failed: /: ; ..." +// (ADR 0040 per-driver attribution preservation). +// - Branch E: success path (no errors) → nil. +// +// The error-format string in Branch D is a load-bearing wire-visible +// contract — downstream code in apply.go's deferred handler renders it +// into result.Errors as the "" entry; operator-facing +// diagnostic format would silently drift if rewritten. Locked here. + +import ( + "context" + "errors" + "fmt" + "net" + "strings" + "testing" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" +) + +// stubFinalizerServer satisfies pb.IaCProviderFinalizerServer with a +// caller-provided handler. nil handler → empty response (Branch E success). +type stubFinalizerServer struct { + pb.UnimplementedIaCProviderFinalizerServer + handler func(*pb.FinalizeApplyRequest) (*pb.FinalizeApplyResponse, error) +} + +func (s *stubFinalizerServer) FinalizeApply(_ context.Context, req *pb.FinalizeApplyRequest) (*pb.FinalizeApplyResponse, error) { + if s.handler == nil { + return &pb.FinalizeApplyResponse{}, nil + } + return s.handler(req) +} + +// requiredFinalizerStub satisfies pb.IaCProviderRequiredServer with the +// Unimplemented embed — the in-process gRPC server needs Required +// registered for the adapter's RequiredClient construction; no RPC +// against Required fires in these tests, so unimplemented is fine. +// Local to this file because the sdk_test package's analogous +// fullProviderStub lives in plugin/external/sdk and isn't reachable +// from cmd/wfctl/main. +type requiredFinalizerStub struct { + pb.UnimplementedIaCProviderRequiredServer +} + +// newFinalizerAdapter spins up an in-process gRPC server that registers +// stubFinalizerServer (with the supplied handler) plus the Required stub, +// dials it, and returns a *typedIaCAdapter with iacServiceFinalizer in +// its registered set so adapter.Finalizer() returns a live client. +// Cleanup drains the server + conn. +func newFinalizerAdapter(t *testing.T, handler func(*pb.FinalizeApplyRequest) (*pb.FinalizeApplyResponse, error)) *typedIaCAdapter { + t.Helper() + lis, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("net.Listen: %v", err) + } + srv := grpc.NewServer() + // Register only the bare minimum: Required (so RequiredClient + // construction works) + Finalizer (the unit under test). No other + // optional services — keeps the surface minimal. + pb.RegisterIaCProviderRequiredServer(srv, &requiredFinalizerStub{}) + pb.RegisterIaCProviderFinalizerServer(srv, &stubFinalizerServer{handler: handler}) + go func() { _ = srv.Serve(lis) }() + conn, err := grpc.NewClient( + lis.Addr().String(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + srv.Stop() + t.Fatalf("grpc.NewClient: %v", err) + } + t.Cleanup(func() { + _ = conn.Close() + srv.Stop() + }) + return newTypedIaCAdapter(conn, map[string]bool{ + iacServiceFinalizer: true, + }) +} + +// TestStatePersistenceHooks_OnPlanComplete_NonAdapterProviderNoOps +// covers Branch A: provider that doesn't type-assert to *typedIaCAdapter +// → closure returns nil (backward compat for in-process fakes and any +// future non-adapter IaCProvider shapes). Locks the pre-Phase-2.5 +// behavior preservation for plugins that don't go through wfctl's +// typed adapter. +func TestStatePersistenceHooks_OnPlanComplete_NonAdapterProviderNoOps(t *testing.T) { + hooks := statePersistenceHooks( + &noopStateStore{}, + &fakeSecretsProvider{stored: map[string]string{}}, + inProcessFakeProvider{}, // satisfies interfaces.IaCProvider but is NOT *typedIaCAdapter + "do", + "plan-id-noop", + nil, + ) + if hooks.OnPlanComplete == nil { + t.Fatal("OnPlanComplete closure must be wired even for non-adapter providers") + } + if err := hooks.OnPlanComplete(t.Context()); err != nil { + t.Errorf("non-adapter provider: expected nil err (no-op), got %v", err) + } +} + +// TestStatePersistenceHooks_OnPlanComplete_NilFinalizerNoOps covers +// Branch B: real *typedIaCAdapter but its Finalizer() returns nil +// because the plugin did not register IaCProviderFinalizer. Per +// ADR 0024 the absence of registration is the negative signal — closure +// silently no-ops so plugins that don't opt in preserve their pre- +// Phase-2.5 behavior. +func TestStatePersistenceHooks_OnPlanComplete_NilFinalizerNoOps(t *testing.T) { + // Build an adapter WITHOUT iacServiceFinalizer in the registered + // set so adapter.Finalizer() returns nil. dialLazyConn (defined in + // iac_typed_adapter_test.go) gives us a real conn against an empty + // in-process server. + adapter := newTypedIaCAdapter(dialLazyConn(t), map[string]bool{ + // no iacServiceFinalizer entry + }) + if adapter.Finalizer() != nil { + t.Fatal("test fixture invariant: adapter.Finalizer() should be nil with empty registered set") + } + hooks := statePersistenceHooks( + &noopStateStore{}, + &fakeSecretsProvider{stored: map[string]string{}}, + adapter, + "do", + "plan-id-nofin", + nil, + ) + if err := hooks.OnPlanComplete(t.Context()); err != nil { + t.Errorf("nil Finalizer: expected nil err (ADR 0024 negative-signal no-op), got %v", err) + } +} + +// TestStatePersistenceHooks_OnPlanComplete_GRPCTransportError covers +// Branch C: FinalizeApply RPC fails at the transport layer (e.g., a +// codes.Internal panic on the server side). Closure must wrap with +// "FinalizeApply gRPC: %w" so callers can both read the prefix AND +// recover the underlying gRPC status via the unwrap chain. +func TestStatePersistenceHooks_OnPlanComplete_GRPCTransportError(t *testing.T) { + sentinel := status.Error(codes.Internal, "server blew up") + adapter := newFinalizerAdapter(t, func(_ *pb.FinalizeApplyRequest) (*pb.FinalizeApplyResponse, error) { + return nil, sentinel + }) + hooks := statePersistenceHooks( + &noopStateStore{}, + &fakeSecretsProvider{stored: map[string]string{}}, + adapter, + "do", + "plan-id-grpc-err", + nil, + ) + err := hooks.OnPlanComplete(t.Context()) + if err == nil { + t.Fatal("expected wrapped gRPC transport error") + } + if !strings.HasPrefix(err.Error(), "FinalizeApply gRPC: ") { + t.Errorf("expected 'FinalizeApply gRPC:' prefix; got: %v", err) + } + // Status round-trip — the wrap uses %w so status.FromError can recover + // the underlying codes.Internal from the unwrap chain. Locks the + // caller-side classification contract (retry/backoff/etc.). + st, ok := status.FromError(err) + if !ok || st.Code() != codes.Internal { + t.Errorf("expected status.FromError to recover codes.Internal; ok=%v code=%v", ok, st.Code()) + } +} + +// TestStatePersistenceHooks_OnPlanComplete_AggregatesPerDriverErrors +// covers Branch D — THE load-bearing wire-visible error-format contract. +// FinalizeApplyResponse.errors[] carries per-driver attribution +// (Resource/Action/Error). Closure aggregates into a single err message +// preserving each entry's shape so the operator-facing diagnostic in +// result.Errors[""] keeps per-driver detail. +// +// Locked format: "plugin finalize: N driver(s) failed: R1/A1: E1; R2/A2: E2" +func TestStatePersistenceHooks_OnPlanComplete_AggregatesPerDriverErrors(t *testing.T) { + adapter := newFinalizerAdapter(t, func(_ *pb.FinalizeApplyRequest) (*pb.FinalizeApplyResponse, error) { + return &pb.FinalizeApplyResponse{ + Errors: []*pb.ActionError{ + {Resource: "infra.database", Action: "deferred_update", Error: "trusted_sources flush failed: 504"}, + {Resource: "infra.spaces", Action: "deferred_update", Error: "cors update rejected"}, + }, + }, nil + }) + hooks := statePersistenceHooks( + &noopStateStore{}, + &fakeSecretsProvider{stored: map[string]string{}}, + adapter, + "do", + "plan-id-agg", + nil, + ) + err := hooks.OnPlanComplete(t.Context()) + if err == nil { + t.Fatal("expected aggregated err from response with errors[]") + } + got := err.Error() + wantPrefix := "plugin finalize: 2 driver(s) failed: " + if !strings.HasPrefix(got, wantPrefix) { + t.Errorf("expected prefix %q; got: %q", wantPrefix, got) + } + // Per-driver entries must appear with Resource/Action/Error in that + // order — locks the format-string field ordering (ADR 0040 invariant). + wantEntries := []string{ + "infra.database/deferred_update: trusted_sources flush failed: 504", + "infra.spaces/deferred_update: cors update rejected", + } + for _, want := range wantEntries { + if !strings.Contains(got, want) { + t.Errorf("expected per-driver entry %q in err; got: %q", want, got) + } + } + // Separator between entries — "; " keeps the aggregate parseable on + // one line for log scrapers. + if !strings.Contains(got, "; ") { + t.Errorf("expected '; ' separator between per-driver entries; got: %q", got) + } +} + +// TestStatePersistenceHooks_OnPlanComplete_SuccessReturnsNil covers +// Branch E: the plugin's FinalizeApply succeeded with empty errors[] +// → closure returns nil (clean success exit). The engine-side defer +// then proceeds without appending a "" entry to +// result.Errors and the outer return remains (result, nil). +func TestStatePersistenceHooks_OnPlanComplete_SuccessReturnsNil(t *testing.T) { + adapter := newFinalizerAdapter(t, func(req *pb.FinalizeApplyRequest) (*pb.FinalizeApplyResponse, error) { + if req.GetPlanId() != "plan-id-success" { + return nil, fmt.Errorf("unexpected plan_id: %q", req.GetPlanId()) + } + return &pb.FinalizeApplyResponse{}, nil // empty errors[] + }) + hooks := statePersistenceHooks( + &noopStateStore{}, + &fakeSecretsProvider{stored: map[string]string{}}, + adapter, + "do", + "plan-id-success", + nil, + ) + if err := hooks.OnPlanComplete(t.Context()); err != nil { + t.Errorf("success path: expected nil err, got %v", err) + } +} + +// Sentinel sanity — guard against future refactors that drop the +// errors.Is round-trip on the gRPC wrap. (TestGRPCTransportError above +// asserts status.FromError; this one asserts the simpler errors.Is form +// callers may use for classification.) +func TestStatePersistenceHooks_OnPlanComplete_GRPCErrorPreservesErrorsIs(t *testing.T) { + sentinel := status.Error(codes.Unavailable, "no route to host") + adapter := newFinalizerAdapter(t, func(_ *pb.FinalizeApplyRequest) (*pb.FinalizeApplyResponse, error) { + return nil, sentinel + }) + hooks := statePersistenceHooks( + &noopStateStore{}, + &fakeSecretsProvider{stored: map[string]string{}}, + adapter, + "do", + "plan-id-isstatus", + nil, + ) + err := hooks.OnPlanComplete(t.Context()) + if err == nil { + t.Fatal("expected wrapped error") + } + if !errors.Is(err, sentinel) { + t.Errorf("expected errors.Is(err, sentinel) to round-trip; err=%v", err) + } +} From ccc32eba62e43933a0950c4ed24b5b611d6824a7 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 17 May 2026 04:15:40 -0400 Subject: [PATCH 11/11] test(wftest/bdd): add IaCProviderFinalizer row to iacServiceChecks CI surfaced TestIaCServiceChecks_CoversEveryProtoService failure on PR #697: the regression-gate test walks iac.proto + asserts every service has a row in wftest/bdd/strict_iac.go's iacServiceChecks list. Phase 2.5 adds IaCProviderFinalizer (PR1 Task 1 proto extend) but the gate row was missed. One-line addition restores invariant. Co-Authored-By: Claude Opus 4.7 (1M context) --- wftest/bdd/strict_iac.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wftest/bdd/strict_iac.go b/wftest/bdd/strict_iac.go index 3bbf3ad9..c170ee0c 100644 --- a/wftest/bdd/strict_iac.go +++ b/wftest/bdd/strict_iac.go @@ -61,6 +61,10 @@ var iacServiceChecks = []iacServiceCheck{ _, ok := p.(pb.IaCProviderValidatorServer) return ok }}, + {"workflow.plugin.external.iac.IaCProviderFinalizer", func(p any) bool { + _, ok := p.(pb.IaCProviderFinalizerServer) + return ok + }}, {"workflow.plugin.external.iac.IaCProviderDriftConfigDetector", func(p any) bool { _, ok := p.(pb.IaCProviderDriftConfigDetectorServer) return ok