From e5701a9b89d4d40f2940c243c187c10de2c53d29 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 16 Jun 2025 13:10:15 +0100 Subject: [PATCH 01/12] Implement `ForPlan` method on `StateStoreConfigState`, add `Planner` interface --- internal/command/workdir/backend_config_state.go | 1 + internal/command/workdir/config_state.go | 6 ++++-- .../command/workdir/statestore_config_state.go | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/internal/command/workdir/backend_config_state.go b/internal/command/workdir/backend_config_state.go index fb8521089bcb..2a40d6c36133 100644 --- a/internal/command/workdir/backend_config_state.go +++ b/internal/command/workdir/backend_config_state.go @@ -15,6 +15,7 @@ import ( ) var _ ConfigState[BackendConfigState] = &BackendConfigState{} +var _ Planner[plans.Backend] = &BackendConfigState{} // BackendConfigState describes the physical storage format for the backend state // in a working directory, and provides the lowest-level API for decoding it. diff --git a/internal/command/workdir/config_state.go b/internal/command/workdir/config_state.go index f674ba78fd1b..41e142ba9b9e 100644 --- a/internal/command/workdir/config_state.go +++ b/internal/command/workdir/config_state.go @@ -5,7 +5,6 @@ package workdir import ( "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/plans" "github.com/zclconf/go-cty/cty" ) @@ -14,6 +13,9 @@ type ConfigState[T any] interface { Empty() bool Config(*configschema.Block) (cty.Value, error) SetConfig(cty.Value, *configschema.Block) error - ForPlan(*configschema.Block, string) (*plans.Backend, error) DeepCopy() *T } + +type Planner[T any] interface { + ForPlan(*configschema.Block, string) (*T, error) +} diff --git a/internal/command/workdir/statestore_config_state.go b/internal/command/workdir/statestore_config_state.go index a2a9be0bebb3..f4858bf9c154 100644 --- a/internal/command/workdir/statestore_config_state.go +++ b/internal/command/workdir/statestore_config_state.go @@ -16,6 +16,7 @@ import ( ) var _ ConfigState[StateStoreConfigState] = &StateStoreConfigState{} +var _ Planner[plans.StateStore] = &StateStoreConfigState{} // StateStoreConfigState describes the physical storage format for the state store type StateStoreConfigState struct { @@ -100,13 +101,20 @@ func (s *StateStoreConfigState) SetConfig(val cty.Value, schema *configschema.Bl // // The state_store configuration schema is required in order to properly // encode the state store-specific configuration settings. -func (s *StateStoreConfigState) ForPlan(schema *configschema.Block, workspaceName string) (*plans.Backend, error) { +func (s *StateStoreConfigState) ForPlan(schema *configschema.Block, workspaceName string) (*plans.StateStore, error) { if s == nil { return nil, nil } - // TODO - // What should a pluggable state store look like in a plan? - return nil, nil + + if err := s.Validate(); err != nil { + return nil, fmt.Errorf("error when preparing state store config for planfile: %s", err) + } + + configVal, err := s.Config(schema) + if err != nil { + return nil, fmt.Errorf("failed to decode state_store config: %w", err) + } + return plans.NewStateStore(s.Type, s.Provider.Version, &s.Provider.Source, configVal, schema, workspaceName) } func (s *StateStoreConfigState) DeepCopy() *StateStoreConfigState { From fd7182af667565356d1c6e7967a1cad3dad53bd7 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 16 Jun 2025 13:10:48 +0100 Subject: [PATCH 02/12] Rename `ForPlan` method to `Plan` --- internal/command/meta_backend.go | 2 +- internal/command/workdir/backend_config_state.go | 4 ++-- internal/command/workdir/config_state.go | 2 +- internal/command/workdir/statestore_config_state.go | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 43debbf9110c..f017433986f1 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -412,7 +412,7 @@ func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType) *backendrun.O // here first is a bug, so panic. panic(fmt.Sprintf("invalid workspace: %s", err)) } - planOutBackend, err := m.backendState.ForPlan(schema, workspace) + planOutBackend, err := m.backendState.Plan(schema, workspace) if err != nil { // Always indicates an implementation error in practice, because // errors here indicate invalid encoding of the backend configuration diff --git a/internal/command/workdir/backend_config_state.go b/internal/command/workdir/backend_config_state.go index 2a40d6c36133..2c691e67d897 100644 --- a/internal/command/workdir/backend_config_state.go +++ b/internal/command/workdir/backend_config_state.go @@ -61,13 +61,13 @@ func (s *BackendConfigState) SetConfig(val cty.Value, schema *configschema.Block return nil } -// ForPlan produces an alternative representation of the receiver that is +// Plan produces an alternative representation of the receiver that is // suitable for storing in a plan. The current workspace must additionally // be provided, to be stored alongside the backend configuration. // // The backend configuration schema is required in order to properly // encode the backend-specific configuration settings. -func (s *BackendConfigState) ForPlan(schema *configschema.Block, workspaceName string) (*plans.Backend, error) { +func (s *BackendConfigState) Plan(schema *configschema.Block, workspaceName string) (*plans.Backend, error) { if s == nil { return nil, nil } diff --git a/internal/command/workdir/config_state.go b/internal/command/workdir/config_state.go index 41e142ba9b9e..3c8c33fdafed 100644 --- a/internal/command/workdir/config_state.go +++ b/internal/command/workdir/config_state.go @@ -17,5 +17,5 @@ type ConfigState[T any] interface { } type Planner[T any] interface { - ForPlan(*configschema.Block, string) (*T, error) + Plan(*configschema.Block, string) (*T, error) } diff --git a/internal/command/workdir/statestore_config_state.go b/internal/command/workdir/statestore_config_state.go index f4858bf9c154..31dd982613c8 100644 --- a/internal/command/workdir/statestore_config_state.go +++ b/internal/command/workdir/statestore_config_state.go @@ -95,13 +95,13 @@ func (s *StateStoreConfigState) SetConfig(val cty.Value, schema *configschema.Bl return nil } -// ForPlan produces an alternative representation of the receiver that is +// Plan produces an alternative representation of the receiver that is // suitable for storing in a plan. The current workspace must additionally // be provided, to be stored alongside the state store configuration. // // The state_store configuration schema is required in order to properly // encode the state store-specific configuration settings. -func (s *StateStoreConfigState) ForPlan(schema *configschema.Block, workspaceName string) (*plans.StateStore, error) { +func (s *StateStoreConfigState) Plan(schema *configschema.Block, workspaceName string) (*plans.StateStore, error) { if s == nil { return nil, nil } From ee6e9da0769e6dcb475e436927b3d973d0c99d1f Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 17 Jun 2025 12:05:34 +0100 Subject: [PATCH 03/12] Allow plan files to contain information about state stores --- internal/plans/planproto/planfile.pb.go | 401 ++++++++++++++++-------- internal/plans/planproto/planfile.proto | 17 + 2 files changed, 294 insertions(+), 124 deletions(-) diff --git a/internal/plans/planproto/planfile.pb.go b/internal/plans/planproto/planfile.pb.go index da02e50f36aa..23a2ae86995a 100644 --- a/internal/plans/planproto/planfile.pb.go +++ b/internal/plans/planproto/planfile.pb.go @@ -340,7 +340,7 @@ func (x CheckResults_Status) Number() protoreflect.EnumNumber { // Deprecated: Use CheckResults_Status.Descriptor instead. func (CheckResults_Status) EnumDescriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{6, 0} + return file_planfile_proto_rawDescGZIP(), []int{8, 0} } type CheckResults_ObjectKind int32 @@ -395,7 +395,7 @@ func (x CheckResults_ObjectKind) Number() protoreflect.EnumNumber { // Deprecated: Use CheckResults_ObjectKind.Descriptor instead. func (CheckResults_ObjectKind) EnumDescriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{6, 1} + return file_planfile_proto_rawDescGZIP(), []int{8, 1} } // Plan is the root message type for the tfplan file @@ -486,6 +486,9 @@ type Plan struct { // Backend is a description of the backend configuration and other related // settings at the time the plan was created. Backend *Backend `protobuf:"bytes,13,opt,name=backend,proto3" json:"backend,omitempty"` + // StateStore is a description of the state_store configuration and other related + // settings at the time the plan was created. + StateStore *StateStore `protobuf:"bytes,29,opt,name=state_store,json=stateStore,proto3" json:"state_store,omitempty"` // RelevantAttributes lists individual resource attributes from // ResourceDrift which may have contributed to the plan changes. RelevantAttributes []*PlanResourceAttr `protobuf:"bytes,15,rep,name=relevant_attributes,json=relevantAttributes,proto3" json:"relevant_attributes,omitempty"` @@ -638,6 +641,13 @@ func (x *Plan) GetBackend() *Backend { return nil } +func (x *Plan) GetStateStore() *StateStore { + if x != nil { + return x.StateStore + } + return nil +} + func (x *Plan) GetRelevantAttributes() []*PlanResourceAttr { if x != nil { return x.RelevantAttributes @@ -720,6 +730,127 @@ func (x *Backend) GetWorkspace() string { return "" } +// StateStore is a description of state_store configuration and other related settings. +type StateStore struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` + Provider *Provider `protobuf:"bytes,4,opt,name=provider,proto3" json:"provider,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StateStore) Reset() { + *x = StateStore{} + mi := &file_planfile_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StateStore) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StateStore) ProtoMessage() {} + +func (x *StateStore) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[2] + 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 StateStore.ProtoReflect.Descriptor instead. +func (*StateStore) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{2} +} + +func (x *StateStore) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *StateStore) GetConfig() *DynamicValue { + if x != nil { + return x.Config + } + return nil +} + +func (x *StateStore) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +func (x *StateStore) GetProvider() *Provider { + if x != nil { + return x.Provider + } + return nil +} + +type Provider struct { + state protoimpl.MessageState `protogen:"open.v1"` + Source string `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Provider) Reset() { + *x = Provider{} + mi := &file_planfile_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Provider) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Provider) ProtoMessage() {} + +func (x *Provider) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[3] + 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 Provider.ProtoReflect.Descriptor instead. +func (*Provider) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{3} +} + +func (x *Provider) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *Provider) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + // Change represents a change made to some object, transforming it from an old // state to a new state. type Change struct { @@ -761,7 +892,7 @@ type Change struct { func (x *Change) Reset() { *x = Change{} - mi := &file_planfile_proto_msgTypes[2] + mi := &file_planfile_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -773,7 +904,7 @@ func (x *Change) String() string { func (*Change) ProtoMessage() {} func (x *Change) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[2] + mi := &file_planfile_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -786,7 +917,7 @@ func (x *Change) ProtoReflect() protoreflect.Message { // Deprecated: Use Change.ProtoReflect.Descriptor instead. func (*Change) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{2} + return file_planfile_proto_rawDescGZIP(), []int{4} } func (x *Change) GetAction() Action { @@ -887,7 +1018,7 @@ type ResourceInstanceChange struct { func (x *ResourceInstanceChange) Reset() { *x = ResourceInstanceChange{} - mi := &file_planfile_proto_msgTypes[3] + mi := &file_planfile_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -899,7 +1030,7 @@ func (x *ResourceInstanceChange) String() string { func (*ResourceInstanceChange) ProtoMessage() {} func (x *ResourceInstanceChange) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[3] + mi := &file_planfile_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -912,7 +1043,7 @@ func (x *ResourceInstanceChange) ProtoReflect() protoreflect.Message { // Deprecated: Use ResourceInstanceChange.ProtoReflect.Descriptor instead. func (*ResourceInstanceChange) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{3} + return file_planfile_proto_rawDescGZIP(), []int{5} } func (x *ResourceInstanceChange) GetAddr() string { @@ -988,7 +1119,7 @@ type DeferredResourceInstanceChange struct { func (x *DeferredResourceInstanceChange) Reset() { *x = DeferredResourceInstanceChange{} - mi := &file_planfile_proto_msgTypes[4] + mi := &file_planfile_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1000,7 +1131,7 @@ func (x *DeferredResourceInstanceChange) String() string { func (*DeferredResourceInstanceChange) ProtoMessage() {} func (x *DeferredResourceInstanceChange) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[4] + mi := &file_planfile_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1013,7 +1144,7 @@ func (x *DeferredResourceInstanceChange) ProtoReflect() protoreflect.Message { // Deprecated: Use DeferredResourceInstanceChange.ProtoReflect.Descriptor instead. func (*DeferredResourceInstanceChange) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{4} + return file_planfile_proto_rawDescGZIP(), []int{6} } func (x *DeferredResourceInstanceChange) GetDeferred() *Deferred { @@ -1047,7 +1178,7 @@ type OutputChange struct { func (x *OutputChange) Reset() { *x = OutputChange{} - mi := &file_planfile_proto_msgTypes[5] + mi := &file_planfile_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1059,7 +1190,7 @@ func (x *OutputChange) String() string { func (*OutputChange) ProtoMessage() {} func (x *OutputChange) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[5] + mi := &file_planfile_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1072,7 +1203,7 @@ func (x *OutputChange) ProtoReflect() protoreflect.Message { // Deprecated: Use OutputChange.ProtoReflect.Descriptor instead. func (*OutputChange) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{5} + return file_planfile_proto_rawDescGZIP(), []int{7} } func (x *OutputChange) GetName() string { @@ -1113,7 +1244,7 @@ type CheckResults struct { func (x *CheckResults) Reset() { *x = CheckResults{} - mi := &file_planfile_proto_msgTypes[6] + mi := &file_planfile_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1125,7 +1256,7 @@ func (x *CheckResults) String() string { func (*CheckResults) ProtoMessage() {} func (x *CheckResults) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[6] + mi := &file_planfile_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1138,7 +1269,7 @@ func (x *CheckResults) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckResults.ProtoReflect.Descriptor instead. func (*CheckResults) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{6} + return file_planfile_proto_rawDescGZIP(), []int{8} } func (x *CheckResults) GetKind() CheckResults_ObjectKind { @@ -1182,7 +1313,7 @@ type FunctionCallHash struct { func (x *FunctionCallHash) Reset() { *x = FunctionCallHash{} - mi := &file_planfile_proto_msgTypes[7] + mi := &file_planfile_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1194,7 +1325,7 @@ func (x *FunctionCallHash) String() string { func (*FunctionCallHash) ProtoMessage() {} func (x *FunctionCallHash) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[7] + mi := &file_planfile_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1207,7 +1338,7 @@ func (x *FunctionCallHash) ProtoReflect() protoreflect.Message { // Deprecated: Use FunctionCallHash.ProtoReflect.Descriptor instead. func (*FunctionCallHash) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{7} + return file_planfile_proto_rawDescGZIP(), []int{9} } func (x *FunctionCallHash) GetKey() []byte { @@ -1245,7 +1376,7 @@ type DynamicValue struct { func (x *DynamicValue) Reset() { *x = DynamicValue{} - mi := &file_planfile_proto_msgTypes[8] + mi := &file_planfile_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1257,7 +1388,7 @@ func (x *DynamicValue) String() string { func (*DynamicValue) ProtoMessage() {} func (x *DynamicValue) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[8] + mi := &file_planfile_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1270,7 +1401,7 @@ func (x *DynamicValue) ProtoReflect() protoreflect.Message { // Deprecated: Use DynamicValue.ProtoReflect.Descriptor instead. func (*DynamicValue) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{8} + return file_planfile_proto_rawDescGZIP(), []int{10} } func (x *DynamicValue) GetMsgpack() []byte { @@ -1292,7 +1423,7 @@ type Path struct { func (x *Path) Reset() { *x = Path{} - mi := &file_planfile_proto_msgTypes[9] + mi := &file_planfile_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1304,7 +1435,7 @@ func (x *Path) String() string { func (*Path) ProtoMessage() {} func (x *Path) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[9] + mi := &file_planfile_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1317,7 +1448,7 @@ func (x *Path) ProtoReflect() protoreflect.Message { // Deprecated: Use Path.ProtoReflect.Descriptor instead. func (*Path) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{9} + return file_planfile_proto_rawDescGZIP(), []int{11} } func (x *Path) GetSteps() []*Path_Step { @@ -1343,7 +1474,7 @@ type Importing struct { func (x *Importing) Reset() { *x = Importing{} - mi := &file_planfile_proto_msgTypes[10] + mi := &file_planfile_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1355,7 +1486,7 @@ func (x *Importing) String() string { func (*Importing) ProtoMessage() {} func (x *Importing) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[10] + mi := &file_planfile_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1368,7 +1499,7 @@ func (x *Importing) ProtoReflect() protoreflect.Message { // Deprecated: Use Importing.ProtoReflect.Descriptor instead. func (*Importing) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{10} + return file_planfile_proto_rawDescGZIP(), []int{12} } func (x *Importing) GetId() string { @@ -1403,7 +1534,7 @@ type Deferred struct { func (x *Deferred) Reset() { *x = Deferred{} - mi := &file_planfile_proto_msgTypes[11] + mi := &file_planfile_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1415,7 +1546,7 @@ func (x *Deferred) String() string { func (*Deferred) ProtoMessage() {} func (x *Deferred) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[11] + mi := &file_planfile_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1428,7 +1559,7 @@ func (x *Deferred) ProtoReflect() protoreflect.Message { // Deprecated: Use Deferred.ProtoReflect.Descriptor instead. func (*Deferred) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{11} + return file_planfile_proto_rawDescGZIP(), []int{13} } func (x *Deferred) GetReason() DeferredReason { @@ -1448,7 +1579,7 @@ type PlanResourceAttr struct { func (x *PlanResourceAttr) Reset() { *x = PlanResourceAttr{} - mi := &file_planfile_proto_msgTypes[13] + mi := &file_planfile_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1460,7 +1591,7 @@ func (x *PlanResourceAttr) String() string { func (*PlanResourceAttr) ProtoMessage() {} func (x *PlanResourceAttr) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[13] + mi := &file_planfile_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1501,7 +1632,7 @@ type CheckResults_ObjectResult struct { func (x *CheckResults_ObjectResult) Reset() { *x = CheckResults_ObjectResult{} - mi := &file_planfile_proto_msgTypes[14] + mi := &file_planfile_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1513,7 +1644,7 @@ func (x *CheckResults_ObjectResult) String() string { func (*CheckResults_ObjectResult) ProtoMessage() {} func (x *CheckResults_ObjectResult) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[14] + mi := &file_planfile_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1526,7 +1657,7 @@ func (x *CheckResults_ObjectResult) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckResults_ObjectResult.ProtoReflect.Descriptor instead. func (*CheckResults_ObjectResult) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{6, 0} + return file_planfile_proto_rawDescGZIP(), []int{8, 0} } func (x *CheckResults_ObjectResult) GetObjectAddr() string { @@ -1563,7 +1694,7 @@ type Path_Step struct { func (x *Path_Step) Reset() { *x = Path_Step{} - mi := &file_planfile_proto_msgTypes[15] + mi := &file_planfile_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1575,7 +1706,7 @@ func (x *Path_Step) String() string { func (*Path_Step) ProtoMessage() {} func (x *Path_Step) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[15] + mi := &file_planfile_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1588,7 +1719,7 @@ func (x *Path_Step) ProtoReflect() protoreflect.Message { // Deprecated: Use Path_Step.ProtoReflect.Descriptor instead. func (*Path_Step) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{9, 0} + return file_planfile_proto_rawDescGZIP(), []int{11, 0} } func (x *Path_Step) GetSelector() isPath_Step_Selector { @@ -1640,7 +1771,7 @@ var File_planfile_proto protoreflect.FileDescriptor var file_planfile_proto_rawDesc = string([]byte{ 0x0a, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x12, 0x06, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x22, 0xe3, 0x08, 0x0a, 0x04, 0x50, 0x6c, 0x61, + 0x12, 0x06, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x22, 0x98, 0x09, 0x0a, 0x04, 0x50, 0x6c, 0x61, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x07, 0x75, 0x69, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x74, @@ -1689,35 +1820,52 @@ var file_planfile_proto_rawDesc = string([]byte{ 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x0a, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x52, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x4b, 0x0a, 0x13, 0x72, - 0x65, 0x6c, 0x65, 0x76, 0x61, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, - 0x6e, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, - 0x61, 0x74, 0x74, 0x72, 0x52, 0x12, 0x72, 0x65, 0x6c, 0x65, 0x76, 0x61, 0x6e, 0x74, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x43, 0x0a, 0x10, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x16, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x18, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x48, 0x61, 0x73, 0x68, 0x52, 0x0f, 0x66, 0x75, 0x6e, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x1a, 0x52, 0x0a, 0x0e, 0x56, - 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, - 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, - 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, - 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, - 0x4d, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, - 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x04, - 0x61, 0x74, 0x74, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x04, 0x61, 0x74, 0x74, 0x72, 0x22, 0x69, - 0x0a, 0x07, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, - 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, - 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0xbc, 0x03, 0x0a, 0x06, 0x43, 0x68, + 0x6e, 0x64, 0x52, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x33, 0x0a, 0x0b, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x12, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, + 0x74, 0x6f, 0x72, 0x65, 0x52, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, + 0x12, 0x4b, 0x0a, 0x13, 0x72, 0x65, 0x6c, 0x65, 0x76, 0x61, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x2e, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x52, 0x12, 0x72, 0x65, 0x6c, 0x65, 0x76, + 0x61, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1c, 0x0a, + 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x43, 0x0a, 0x10, 0x66, + 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, + 0x16, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x46, + 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x48, 0x61, 0x73, 0x68, 0x52, + 0x0f, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, + 0x1a, 0x52, 0x0a, 0x0e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x4d, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x61, 0x74, 0x74, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x12, 0x20, 0x0a, 0x04, 0x61, 0x74, 0x74, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x04, 0x61, + 0x74, 0x74, 0x72, 0x22, 0x69, 0x0a, 0x07, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x12, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, + 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x1c, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x9a, + 0x01, 0x0a, 0x0a, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x12, 0x2c, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, + 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x1c, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x2c, 0x0a, + 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x22, 0x3c, 0x0a, 0x08, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xbc, 0x03, 0x0a, 0x06, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x06, @@ -1912,7 +2060,7 @@ func file_planfile_proto_rawDescGZIP() []byte { } var file_planfile_proto_enumTypes = make([]protoimpl.EnumInfo, 6) -var file_planfile_proto_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_planfile_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_planfile_proto_goTypes = []any{ (Mode)(0), // 0: tfplan.Mode (Action)(0), // 1: tfplan.Action @@ -1922,61 +2070,66 @@ var file_planfile_proto_goTypes = []any{ (CheckResults_ObjectKind)(0), // 5: tfplan.CheckResults.ObjectKind (*Plan)(nil), // 6: tfplan.Plan (*Backend)(nil), // 7: tfplan.Backend - (*Change)(nil), // 8: tfplan.Change - (*ResourceInstanceChange)(nil), // 9: tfplan.ResourceInstanceChange - (*DeferredResourceInstanceChange)(nil), // 10: tfplan.DeferredResourceInstanceChange - (*OutputChange)(nil), // 11: tfplan.OutputChange - (*CheckResults)(nil), // 12: tfplan.CheckResults - (*FunctionCallHash)(nil), // 13: tfplan.FunctionCallHash - (*DynamicValue)(nil), // 14: tfplan.DynamicValue - (*Path)(nil), // 15: tfplan.Path - (*Importing)(nil), // 16: tfplan.Importing - (*Deferred)(nil), // 17: tfplan.Deferred - nil, // 18: tfplan.Plan.VariablesEntry - (*PlanResourceAttr)(nil), // 19: tfplan.Plan.resource_attr - (*CheckResults_ObjectResult)(nil), // 20: tfplan.CheckResults.ObjectResult - (*Path_Step)(nil), // 21: tfplan.Path.Step + (*StateStore)(nil), // 8: tfplan.StateStore + (*Provider)(nil), // 9: tfplan.Provider + (*Change)(nil), // 10: tfplan.Change + (*ResourceInstanceChange)(nil), // 11: tfplan.ResourceInstanceChange + (*DeferredResourceInstanceChange)(nil), // 12: tfplan.DeferredResourceInstanceChange + (*OutputChange)(nil), // 13: tfplan.OutputChange + (*CheckResults)(nil), // 14: tfplan.CheckResults + (*FunctionCallHash)(nil), // 15: tfplan.FunctionCallHash + (*DynamicValue)(nil), // 16: tfplan.DynamicValue + (*Path)(nil), // 17: tfplan.Path + (*Importing)(nil), // 18: tfplan.Importing + (*Deferred)(nil), // 19: tfplan.Deferred + nil, // 20: tfplan.Plan.VariablesEntry + (*PlanResourceAttr)(nil), // 21: tfplan.Plan.resource_attr + (*CheckResults_ObjectResult)(nil), // 22: tfplan.CheckResults.ObjectResult + (*Path_Step)(nil), // 23: tfplan.Path.Step } var file_planfile_proto_depIdxs = []int32{ 0, // 0: tfplan.Plan.ui_mode:type_name -> tfplan.Mode - 18, // 1: tfplan.Plan.variables:type_name -> tfplan.Plan.VariablesEntry - 9, // 2: tfplan.Plan.resource_changes:type_name -> tfplan.ResourceInstanceChange - 9, // 3: tfplan.Plan.resource_drift:type_name -> tfplan.ResourceInstanceChange - 10, // 4: tfplan.Plan.deferred_changes:type_name -> tfplan.DeferredResourceInstanceChange - 11, // 5: tfplan.Plan.output_changes:type_name -> tfplan.OutputChange - 12, // 6: tfplan.Plan.check_results:type_name -> tfplan.CheckResults + 20, // 1: tfplan.Plan.variables:type_name -> tfplan.Plan.VariablesEntry + 11, // 2: tfplan.Plan.resource_changes:type_name -> tfplan.ResourceInstanceChange + 11, // 3: tfplan.Plan.resource_drift:type_name -> tfplan.ResourceInstanceChange + 12, // 4: tfplan.Plan.deferred_changes:type_name -> tfplan.DeferredResourceInstanceChange + 13, // 5: tfplan.Plan.output_changes:type_name -> tfplan.OutputChange + 14, // 6: tfplan.Plan.check_results:type_name -> tfplan.CheckResults 7, // 7: tfplan.Plan.backend:type_name -> tfplan.Backend - 19, // 8: tfplan.Plan.relevant_attributes:type_name -> tfplan.Plan.resource_attr - 13, // 9: tfplan.Plan.function_results:type_name -> tfplan.FunctionCallHash - 14, // 10: tfplan.Backend.config:type_name -> tfplan.DynamicValue - 1, // 11: tfplan.Change.action:type_name -> tfplan.Action - 14, // 12: tfplan.Change.values:type_name -> tfplan.DynamicValue - 15, // 13: tfplan.Change.before_sensitive_paths:type_name -> tfplan.Path - 15, // 14: tfplan.Change.after_sensitive_paths:type_name -> tfplan.Path - 16, // 15: tfplan.Change.importing:type_name -> tfplan.Importing - 14, // 16: tfplan.Change.before_identity:type_name -> tfplan.DynamicValue - 14, // 17: tfplan.Change.after_identity:type_name -> tfplan.DynamicValue - 8, // 18: tfplan.ResourceInstanceChange.change:type_name -> tfplan.Change - 15, // 19: tfplan.ResourceInstanceChange.required_replace:type_name -> tfplan.Path - 2, // 20: tfplan.ResourceInstanceChange.action_reason:type_name -> tfplan.ResourceInstanceActionReason - 17, // 21: tfplan.DeferredResourceInstanceChange.deferred:type_name -> tfplan.Deferred - 9, // 22: tfplan.DeferredResourceInstanceChange.change:type_name -> tfplan.ResourceInstanceChange - 8, // 23: tfplan.OutputChange.change:type_name -> tfplan.Change - 5, // 24: tfplan.CheckResults.kind:type_name -> tfplan.CheckResults.ObjectKind - 4, // 25: tfplan.CheckResults.status:type_name -> tfplan.CheckResults.Status - 20, // 26: tfplan.CheckResults.objects:type_name -> tfplan.CheckResults.ObjectResult - 21, // 27: tfplan.Path.steps:type_name -> tfplan.Path.Step - 14, // 28: tfplan.Importing.identity:type_name -> tfplan.DynamicValue - 3, // 29: tfplan.Deferred.reason:type_name -> tfplan.DeferredReason - 14, // 30: tfplan.Plan.VariablesEntry.value:type_name -> tfplan.DynamicValue - 15, // 31: tfplan.Plan.resource_attr.attr:type_name -> tfplan.Path - 4, // 32: tfplan.CheckResults.ObjectResult.status:type_name -> tfplan.CheckResults.Status - 14, // 33: tfplan.Path.Step.element_key:type_name -> tfplan.DynamicValue - 34, // [34:34] is the sub-list for method output_type - 34, // [34:34] is the sub-list for method input_type - 34, // [34:34] is the sub-list for extension type_name - 34, // [34:34] is the sub-list for extension extendee - 0, // [0:34] is the sub-list for field type_name + 8, // 8: tfplan.Plan.state_store:type_name -> tfplan.StateStore + 21, // 9: tfplan.Plan.relevant_attributes:type_name -> tfplan.Plan.resource_attr + 15, // 10: tfplan.Plan.function_results:type_name -> tfplan.FunctionCallHash + 16, // 11: tfplan.Backend.config:type_name -> tfplan.DynamicValue + 16, // 12: tfplan.StateStore.config:type_name -> tfplan.DynamicValue + 9, // 13: tfplan.StateStore.provider:type_name -> tfplan.Provider + 1, // 14: tfplan.Change.action:type_name -> tfplan.Action + 16, // 15: tfplan.Change.values:type_name -> tfplan.DynamicValue + 17, // 16: tfplan.Change.before_sensitive_paths:type_name -> tfplan.Path + 17, // 17: tfplan.Change.after_sensitive_paths:type_name -> tfplan.Path + 18, // 18: tfplan.Change.importing:type_name -> tfplan.Importing + 16, // 19: tfplan.Change.before_identity:type_name -> tfplan.DynamicValue + 16, // 20: tfplan.Change.after_identity:type_name -> tfplan.DynamicValue + 10, // 21: tfplan.ResourceInstanceChange.change:type_name -> tfplan.Change + 17, // 22: tfplan.ResourceInstanceChange.required_replace:type_name -> tfplan.Path + 2, // 23: tfplan.ResourceInstanceChange.action_reason:type_name -> tfplan.ResourceInstanceActionReason + 19, // 24: tfplan.DeferredResourceInstanceChange.deferred:type_name -> tfplan.Deferred + 11, // 25: tfplan.DeferredResourceInstanceChange.change:type_name -> tfplan.ResourceInstanceChange + 10, // 26: tfplan.OutputChange.change:type_name -> tfplan.Change + 5, // 27: tfplan.CheckResults.kind:type_name -> tfplan.CheckResults.ObjectKind + 4, // 28: tfplan.CheckResults.status:type_name -> tfplan.CheckResults.Status + 22, // 29: tfplan.CheckResults.objects:type_name -> tfplan.CheckResults.ObjectResult + 23, // 30: tfplan.Path.steps:type_name -> tfplan.Path.Step + 16, // 31: tfplan.Importing.identity:type_name -> tfplan.DynamicValue + 3, // 32: tfplan.Deferred.reason:type_name -> tfplan.DeferredReason + 16, // 33: tfplan.Plan.VariablesEntry.value:type_name -> tfplan.DynamicValue + 17, // 34: tfplan.Plan.resource_attr.attr:type_name -> tfplan.Path + 4, // 35: tfplan.CheckResults.ObjectResult.status:type_name -> tfplan.CheckResults.Status + 16, // 36: tfplan.Path.Step.element_key:type_name -> tfplan.DynamicValue + 37, // [37:37] is the sub-list for method output_type + 37, // [37:37] is the sub-list for method input_type + 37, // [37:37] is the sub-list for extension type_name + 37, // [37:37] is the sub-list for extension extendee + 0, // [0:37] is the sub-list for field type_name } func init() { file_planfile_proto_init() } @@ -1984,7 +2137,7 @@ func file_planfile_proto_init() { if File_planfile_proto != nil { return } - file_planfile_proto_msgTypes[15].OneofWrappers = []any{ + file_planfile_proto_msgTypes[17].OneofWrappers = []any{ (*Path_Step_AttributeName)(nil), (*Path_Step_ElementKey)(nil), } @@ -1994,7 +2147,7 @@ func file_planfile_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_planfile_proto_rawDesc), len(file_planfile_proto_rawDesc)), NumEnums: 6, - NumMessages: 16, + NumMessages: 18, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/plans/planproto/planfile.proto b/internal/plans/planproto/planfile.proto index fc2c216e3920..4caf72f592d4 100644 --- a/internal/plans/planproto/planfile.proto +++ b/internal/plans/planproto/planfile.proto @@ -111,6 +111,10 @@ message Plan { // settings at the time the plan was created. Backend backend = 13; + // StateStore is a description of the state_store configuration and other related + // settings at the time the plan was created. + StateStore state_store = 29; + message resource_attr { string resource = 1; Path attr= 2; @@ -140,6 +144,19 @@ message Backend { string workspace = 3; } +// StateStore is a description of state_store configuration and other related settings. +message StateStore { + string type = 1; + DynamicValue config = 2; + string workspace = 3; + Provider provider = 4; +} + +message Provider { + string source = 1; + string version = 2; +} + // Action describes the type of action planned for an object. // Not all action values are valid for all object types. enum Action { From 1bb87e8cab7bad359fc4c93ad4ae74cafa93a56a Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 17 Jun 2025 12:08:32 +0100 Subject: [PATCH 04/12] Add code needed for representing a state store in the Plan struct, which is used when handling plan files --- internal/plans/plan.go | 51 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/internal/plans/plan.go b/internal/plans/plan.go index c6b03bd35641..402c006c9ac6 100644 --- a/internal/plans/plan.go +++ b/internal/plans/plan.go @@ -9,6 +9,8 @@ import ( "github.com/zclconf/go-cty/cty" + version "github.com/hashicorp/go-version" + tfaddr "github.com/hashicorp/terraform-registry-address" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs/configschema" @@ -69,7 +71,9 @@ type Plan struct { DeferredResources []*DeferredResourceInstanceChangeSrc TargetAddrs []addrs.Targetable ForceReplaceAddrs []addrs.AbsResourceInstance - Backend Backend + + Backend Backend + StateStore StateStore // Complete is true if Terraform considers this to be a "complete" plan, // which is to say that it includes a planned action (even if no-op) @@ -228,3 +232,48 @@ func NewBackend(typeName string, config cty.Value, configSchema *configschema.Bl Workspace: workspaceName, }, nil } + +// StateStore represents the state store-related configuration and other data as it +// existed when a plan was created. +type StateStore struct { + // Type is the type of state store that the plan will apply against. + Type string + + Provider *Provider + + // Config is the configuration of the backend, whose schema is decided by + // the backend Type. + Config DynamicValue + + // Workspace is the name of the workspace that was active when the plan + // was created. It is illegal to apply a plan created for one workspace + // to the state of another workspace. + // (This constraint is already enforced by the statefile lineage mechanism, + // but storing this explicitly allows us to return a better error message + // in the situation where the user has the wrong workspace selected.) + Workspace string +} + +type Provider struct { + Version *version.Version // The specific provider version used for the state store. Should be set using a getproviders.Version, etc. + Source *tfaddr.Provider // The FQN/fully-qualified name of the provider. +} + +func NewStateStore(typeName string, ver *version.Version, source *tfaddr.Provider, config cty.Value, configSchema *configschema.Block, workspaceName string) (*StateStore, error) { + dv, err := NewDynamicValue(config, configSchema.ImpliedType()) + if err != nil { + return nil, err + } + + provider := &Provider{ + Version: ver, + Source: source, + } + + return &StateStore{ + Type: typeName, + Provider: provider, + Config: dv, + Workspace: workspaceName, + }, nil +} From 9e076d8e4ac05f7180c3c74194be5a9737fa384e Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 17 Jun 2025 16:25:55 +0100 Subject: [PATCH 05/12] Add ability to read/write either a backend or state store's data in a plan file. Add some test coverage. --- internal/plans/plan.go | 29 ++++ internal/plans/planfile/tfplan.go | 72 ++++++++-- internal/plans/planfile/tfplan_test.go | 180 +++++++++++++++++++++---- 3 files changed, 243 insertions(+), 38 deletions(-) diff --git a/internal/plans/plan.go b/internal/plans/plan.go index 402c006c9ac6..682779443123 100644 --- a/internal/plans/plan.go +++ b/internal/plans/plan.go @@ -277,3 +277,32 @@ func NewStateStore(typeName string, ver *version.Version, source *tfaddr.Provide Workspace: workspaceName, }, nil } + +// SetVersion includes logic for parsing a string representation of a version, +// for example data read from a plan file. +// If an error occurs it is returned and the receiver's Version field is unchanged. +// If there are no errors then the receiver's Version field is updated. +func (p *Provider) SetVersion(input string) error { + ver := version.Version{} + err := ver.UnmarshalText([]byte(input)) + if err != nil { + return err + } + + p.Version = &ver + return nil +} + +// SetSource includes logic for parsing a string representation of a provider source, +// for example data read from a plan file. +// If an error occurs it is returned and the receiver's Source field is unchanged. +// If there are no errors then the receiver's Source field is updated. +func (p *Provider) SetSource(input string) error { + source, diags := addrs.ParseProviderSourceString(input) + if diags.HasErrors() { + return diags.ErrWithWarnings() + } + + p.Source = &source + return nil +} diff --git a/internal/plans/planfile/tfplan.go b/internal/plans/planfile/tfplan.go index 5b30d65317fe..3255b8fbb497 100644 --- a/internal/plans/planfile/tfplan.go +++ b/internal/plans/planfile/tfplan.go @@ -179,9 +179,17 @@ func readTfplan(r io.Reader) (*plans.Plan, error) { ) } - if rawBackend := rawPlan.Backend; rawBackend == nil { - return nil, fmt.Errorf("plan file has no backend settings; backend settings are required") - } else { + switch { + case rawPlan.Backend == nil && rawPlan.StateStore == nil: + // Similar validation in writeTfPlan should prevent this occurring + return nil, + fmt.Errorf("plan file has neither backend nor state_store settings; one of these settings is required. This is a bug in Terraform and should be reported.") + case rawPlan.Backend != nil && rawPlan.StateStore != nil: + // Similar validation in writeTfPlan should prevent this occurring + return nil, + fmt.Errorf("plan file contains both backend and state_store settings when only one of these settings should be set. This is a bug in Terraform and should be reported.") + case rawPlan.Backend != nil: + rawBackend := rawPlan.Backend config, err := valueFromTfplan(rawBackend.Config) if err != nil { return nil, fmt.Errorf("plan file has invalid backend configuration: %s", err) @@ -191,6 +199,28 @@ func readTfplan(r io.Reader) (*plans.Plan, error) { Config: config, Workspace: rawBackend.Workspace, } + case rawPlan.StateStore != nil: + rawStateStore := rawPlan.StateStore + config, err := valueFromTfplan(rawStateStore.Config) + if err != nil { + return nil, fmt.Errorf("plan file has invalid state_store configuration: %s", err) + } + provider := &plans.Provider{} + err = provider.SetSource(rawStateStore.Provider.Source) + if err != nil { + return nil, fmt.Errorf("plan file has invalid state_store provider source: %s", err) + } + err = provider.SetVersion(rawStateStore.Provider.Version) + if err != nil { + return nil, fmt.Errorf("plan file has invalid state_store provider version: %s", err) + } + + plan.StateStore = plans.StateStore{ + Type: rawStateStore.Type, + Provider: provider, + Config: config, + Workspace: rawStateStore.Workspace, + } } if plan.Timestamp, err = time.Parse(time.RFC3339, rawPlan.Timestamp); err != nil { @@ -636,17 +666,35 @@ func writeTfplan(plan *plans.Plan, w io.Writer) error { ) } - if plan.Backend.Type == "" || plan.Backend.Config == nil { + // Store details about accessing state + backendInUse := plan.Backend.Type != "" && plan.Backend.Config != nil + stateStoreInUse := plan.StateStore.Type != "" && plan.StateStore.Config != nil + switch { + case !backendInUse && !stateStoreInUse: // This suggests a bug in the code that created the plan, since it - // ought to always have a backend populated, even if it's the default + // ought to always have either a backend or state_store populated, even if it's the default // "local" backend with a local state file. - return fmt.Errorf("plan does not have a backend configuration") - } - - rawPlan.Backend = &planproto.Backend{ - Type: plan.Backend.Type, - Config: valueToTfplan(plan.Backend.Config), - Workspace: plan.Backend.Workspace, + return fmt.Errorf("plan does not have a backend or state_store configuration") + case backendInUse && stateStoreInUse: + // This suggests a bug in the code that created the plan, since it + // should never have both a backend and state_store populated. + return fmt.Errorf("plan contains conflicting backend and state_store configurations") + case backendInUse: + rawPlan.Backend = &planproto.Backend{ + Type: plan.Backend.Type, + Config: valueToTfplan(plan.Backend.Config), + Workspace: plan.Backend.Workspace, + } + case stateStoreInUse: + rawPlan.StateStore = &planproto.StateStore{ + Type: plan.StateStore.Type, + Provider: &planproto.Provider{ + Version: plan.StateStore.Provider.Version.String(), + Source: plan.StateStore.Provider.Source.String(), + }, + Config: valueToTfplan(plan.StateStore.Config), + Workspace: plan.StateStore.Workspace, + } } rawPlan.Timestamp = plan.Timestamp.Format(time.RFC3339) diff --git a/internal/plans/planfile/tfplan_test.go b/internal/plans/planfile/tfplan_test.go index a596864c7d35..f8340fdafc66 100644 --- a/internal/plans/planfile/tfplan_test.go +++ b/internal/plans/planfile/tfplan_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/go-test/deep" + version "github.com/hashicorp/go-version" + tfaddr "github.com/hashicorp/terraform-registry-address" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" @@ -20,14 +22,165 @@ import ( "github.com/hashicorp/terraform/internal/states" ) +// TestTFPlanRoundTrip writes a plan to a planfile, reads the contents of the planfile, +// and asserts that the read data matches the written data. func TestTFPlanRoundTrip(t *testing.T) { + cases := map[string]struct { + plan *plans.Plan + }{ + "round trip with backend": { + plan: func() *plans.Plan { + rawPlan := examplePlanForTest(t) + return rawPlan + }(), + }, + "round trip with state store": { + plan: func() *plans.Plan { + rawPlan := examplePlanForTest(t) + // remove backend data from example plan + rawPlan.Backend = plans.Backend{} + ver, err := version.NewVersion("9.9.9") + if err != nil { + t.Fatalf("error encountered during test setup: %s", err) + } + + // add state store instead + rawPlan.StateStore = plans.StateStore{ + Type: "foo_bar", + Provider: &plans.Provider{ + Version: ver, + Source: &tfaddr.Provider{ + Hostname: tfaddr.DefaultProviderRegistryHost, + Namespace: "foobar", + Type: "foo", + }, + }, + Config: mustNewDynamicValue( + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }), + cty.Object(map[string]cty.Type{ + "foo": cty.String, + }), + ), + Workspace: "default", + } + return rawPlan + }(), + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + var buf bytes.Buffer + err := writeTfplan(tc.plan, &buf) + if err != nil { + t.Fatalf("unexpected err: %s", err) + } + + newPlan, err := readTfplan(&buf) + if err != nil { + t.Fatal(err) + } + + { + oldDepth := deep.MaxDepth + oldCompare := deep.CompareUnexportedFields + deep.MaxDepth = 20 + deep.CompareUnexportedFields = true + defer func() { + deep.MaxDepth = oldDepth + deep.CompareUnexportedFields = oldCompare + }() + } + for _, problem := range deep.Equal(newPlan, tc.plan) { + t.Error(problem) + } + }) + } +} + +func Test_writeTfplan_validation(t *testing.T) { + cases := map[string]struct { + plan *plans.Plan + wantWriteErrMsg string + }{ + "error when missing both backend and state store": { + plan: func() *plans.Plan { + rawPlan := examplePlanForTest(t) + // remove backend from example plan + rawPlan.Backend.Type = "" + rawPlan.Backend.Config = nil + return rawPlan + }(), + wantWriteErrMsg: "plan does not have a backend or state_store configuration", + }, + "error when got both backend and state store": { + plan: func() *plans.Plan { + rawPlan := examplePlanForTest(t) + // Backend is already set on example plan + + // Add state store in parallel + ver, err := version.NewVersion("9.9.9") + if err != nil { + t.Fatalf("error encountered during test setup: %s", err) + } + rawPlan.StateStore = plans.StateStore{ + Type: "foo_bar", + Provider: &plans.Provider{ + Version: ver, + Source: &tfaddr.Provider{ + Hostname: tfaddr.DefaultProviderRegistryHost, + Namespace: "foobar", + Type: "foo", + }, + }, + Config: mustNewDynamicValue( + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }), + cty.Object(map[string]cty.Type{ + "foo": cty.String, + }), + ), + Workspace: "default", + } + return rawPlan + }(), + wantWriteErrMsg: "plan contains conflicting backend and state_store configurations", + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + var buf bytes.Buffer + err := writeTfplan(tc.plan, &buf) + + if err != nil && tc.wantWriteErrMsg == "" { + t.Fatalf("unexpected err: %s", err) + } + if err == nil && tc.wantWriteErrMsg != "" { + t.Fatal("expected error but got none") + } + if err.Error() != tc.wantWriteErrMsg { + t.Fatalf("unexpected error message: wanted %q, got %q", tc.wantWriteErrMsg, err) + } + }) + } +} + +// examplePlanForTest returns a plans.Plan struct pointer that can be used +// when setting up tests. The returned plan can be mutated depending on the +// test case. +func examplePlanForTest(t *testing.T) *plans.Plan { + t.Helper() objTy := cty.Object(map[string]cty.Type{ "id": cty.String, }) applyTimeVariables := collections.NewSetCmp[string]() applyTimeVariables.Add("bar") - plan := &plans.Plan{ + return &plans.Plan{ Applyable: true, Complete: true, VariableValues: map[string]plans.DynamicValue{ @@ -323,31 +476,6 @@ func TestTFPlanRoundTrip(t *testing.T) { Workspace: "default", }, } - - var buf bytes.Buffer - err := writeTfplan(plan, &buf) - if err != nil { - t.Fatal(err) - } - - newPlan, err := readTfplan(&buf) - if err != nil { - t.Fatal(err) - } - - { - oldDepth := deep.MaxDepth - oldCompare := deep.CompareUnexportedFields - deep.MaxDepth = 20 - deep.CompareUnexportedFields = true - defer func() { - deep.MaxDepth = oldDepth - deep.CompareUnexportedFields = oldCompare - }() - } - for _, problem := range deep.Equal(newPlan, plan) { - t.Error(problem) - } } func mustDynamicOutputValue(val string) plans.DynamicValue { From 44ce79b4c3bd38bf67353628054562e74accee06 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 17 Jun 2025 17:58:39 +0100 Subject: [PATCH 06/12] Update plan's `ProviderAddrs` method to include the provider used for PSS, if present --- internal/plans/plan.go | 9 +++++++++ internal/plans/plan_test.go | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/internal/plans/plan.go b/internal/plans/plan.go index 682779443123..860b785566e2 100644 --- a/internal/plans/plan.go +++ b/internal/plans/plan.go @@ -186,6 +186,15 @@ func (p *Plan) ProviderAddrs() []addrs.AbsProviderConfig { return nil } + // TODO: Is this needed? Are there issues if the same provider is used for PSS and resource management also? + if p.StateStore.Provider != nil { + m[p.StateStore.Provider.Source.String()] = addrs.AbsProviderConfig{ + Module: addrs.RootModule, // state_store block is only in Root + Provider: *p.StateStore.Provider.Source, + // No aliases used for PSS provider. + } + } + // This is mainly just so we'll get stable results for testing purposes. keys := make([]string, 0, len(m)) for k := range m { diff --git a/internal/plans/plan_test.go b/internal/plans/plan_test.go index 2897afbf627a..e2a91cdc7628 100644 --- a/internal/plans/plan_test.go +++ b/internal/plans/plan_test.go @@ -7,13 +7,39 @@ import ( "testing" "github.com/go-test/deep" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" ) func TestProviderAddrs(t *testing.T) { + // Inputs for plan + provider := &Provider{} + err := provider.SetSource("registry.terraform.io/hashicorp/pluggable") + if err != nil { + panic(err) + } + err = provider.SetVersion("9.9.9") + if err != nil { + panic(err) + } + config, err := NewDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }), cty.Object(map[string]cty.Type{ + "foo": cty.String, + })) + if err != nil { + panic(err) + } + // Prepare plan plan := &Plan{ + StateStore: StateStore{ + Type: "pluggable_foobar", + Provider: provider, + Config: config, + Workspace: "default", + }, VariableValues: map[string]DynamicValue{}, Changes: &ChangesSrc{ Resources: []*ResourceInstanceChangeSrc{ @@ -57,14 +83,20 @@ func TestProviderAddrs(t *testing.T) { got := plan.ProviderAddrs() want := []addrs.AbsProviderConfig{ - addrs.AbsProviderConfig{ + // Providers used for managed resources + { Module: addrs.RootModule.Child("foo"), Provider: addrs.NewDefaultProvider("test"), }, - addrs.AbsProviderConfig{ + { Module: addrs.RootModule, Provider: addrs.NewDefaultProvider("test"), }, + // Provider used for pluggable state storage + { + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("pluggable"), + }, } for _, problem := range deep.Equal(got, want) { From a2acc33ed8aa8afe392641f295eee81f10a52503 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 17 Jun 2025 19:33:42 +0100 Subject: [PATCH 07/12] Split interfaces --- internal/command/workdir/backend_config_state.go | 3 ++- internal/command/workdir/config_state.go | 9 ++++++++- internal/command/workdir/statestore_config_state.go | 3 ++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/command/workdir/backend_config_state.go b/internal/command/workdir/backend_config_state.go index 2c691e67d897..21c6af31f943 100644 --- a/internal/command/workdir/backend_config_state.go +++ b/internal/command/workdir/backend_config_state.go @@ -14,7 +14,8 @@ import ( "github.com/hashicorp/terraform/internal/plans" ) -var _ ConfigState[BackendConfigState] = &BackendConfigState{} +var _ ConfigState = &BackendConfigState{} +var _ DeepCopier[BackendConfigState] = &BackendConfigState{} var _ Planner[plans.Backend] = &BackendConfigState{} // BackendConfigState describes the physical storage format for the backend state diff --git a/internal/command/workdir/config_state.go b/internal/command/workdir/config_state.go index 3c8c33fdafed..e42b1945173f 100644 --- a/internal/command/workdir/config_state.go +++ b/internal/command/workdir/config_state.go @@ -9,13 +9,20 @@ import ( ) // ConfigState describes a configuration block, and is used to make that config block stateful. -type ConfigState[T any] interface { +type ConfigState interface { Empty() bool Config(*configschema.Block) (cty.Value, error) SetConfig(cty.Value, *configschema.Block) error +} + +// DeepCopier implementations can return deep copies of themselves for use elsewhere +// without mutating the original value. +type DeepCopier[T any] interface { DeepCopy() *T } +// Planner implementations can return a representation of their data that's +// appropriate for storing in a plan file. type Planner[T any] interface { Plan(*configschema.Block, string) (*T, error) } diff --git a/internal/command/workdir/statestore_config_state.go b/internal/command/workdir/statestore_config_state.go index 31dd982613c8..83886b1f95d0 100644 --- a/internal/command/workdir/statestore_config_state.go +++ b/internal/command/workdir/statestore_config_state.go @@ -15,7 +15,8 @@ import ( ctyjson "github.com/zclconf/go-cty/cty/json" ) -var _ ConfigState[StateStoreConfigState] = &StateStoreConfigState{} +var _ ConfigState = &StateStoreConfigState{} +var _ DeepCopier[StateStoreConfigState] = &StateStoreConfigState{} var _ Planner[plans.StateStore] = &StateStoreConfigState{} // StateStoreConfigState describes the physical storage format for the state store From 2479cdcd7acb906858859f9fa89a9052339f642a Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 17 Jun 2025 19:42:43 +0100 Subject: [PATCH 08/12] WIP --- internal/backend/backendrun/operation.go | 9 +++-- internal/backend/local/backend_plan.go | 9 ++++- internal/command/meta.go | 5 ++- internal/command/meta_backend.go | 50 ++++++++++++++++++------ 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/internal/backend/backendrun/operation.go b/internal/backend/backendrun/operation.go index 94c72ab5ad80..f58e8a592da7 100644 --- a/internal/backend/backendrun/operation.go +++ b/internal/backend/backendrun/operation.go @@ -75,10 +75,11 @@ type Operation struct { // // PlanOutBackend is the backend to store with the plan. This is the // backend that will be used when applying the plan. - PlanId string - PlanRefresh bool // PlanRefresh will do a refresh before a plan - PlanOutPath string // PlanOutPath is the path to save the plan - PlanOutBackend *plans.Backend + PlanId string + PlanRefresh bool // PlanRefresh will do a refresh before a plan + PlanOutPath string // PlanOutPath is the path to save the plan + PlanOutBackend *plans.Backend + PlanOutStateStore *plans.StateStore // ConfigDir is the path to the directory containing the configuration's // root module. diff --git a/internal/backend/local/backend_plan.go b/internal/backend/local/backend_plan.go index 3937a4948b3a..f41b72ca8e1b 100644 --- a/internal/backend/local/backend_plan.go +++ b/internal/backend/local/backend_plan.go @@ -146,7 +146,14 @@ func (b *Local) opPlan( op.ReportResult(runningOp, diags) return } - plan.Backend = *op.PlanOutBackend + switch { + case op.PlanOutBackend != nil: + plan.Backend = *op.PlanOutBackend + case op.PlanOutStateStore != nil: + plan.StateStore = *op.PlanOutStateStore + default: + panic("backend and state_store configuration data missing from Operation") + } // We may have updated the state in the refresh step above, but we // will freeze that updated state in the plan file for now and diff --git a/internal/command/meta.go b/internal/command/meta.go index d1d0bbb51314..32adc081d5a6 100644 --- a/internal/command/meta.go +++ b/internal/command/meta.go @@ -201,8 +201,9 @@ type Meta struct { // It is initialized on first use. configLoader *configload.Loader - // backendState is the currently active backend state - backendState *workdir.BackendConfigState + // backendConfigState is the currently active backend state + backendConfigState *workdir.BackendConfigState + stateStoreConfigState *workdir.StateStoreConfigState // Variables for the context (private) variableArgs arguments.FlagNameValueSlice diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index f017433986f1..0bdd7470d1b4 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -196,13 +196,13 @@ func (m *Meta) Backend(opts *BackendOpts) (backendrun.OperationsBackend, tfdiags // the user, since the local backend should only be used when learning or // in exceptional cases and so it's better to help the user learn that // by introducing it as a concept. - if m.backendState == nil { + if m.backendConfigState == nil { // NOTE: This synthetic object is intentionally _not_ retained in the // on-disk record of the backend configuration, which was already dealt // with inside backendFromConfig, because we still need that codepath // to be able to recognize the lack of a config as distinct from // explicitly setting local until we do some more refactoring here. - m.backendState = &workdir.BackendConfigState{ + m.backendConfigState = &workdir.BackendConfigState{ Type: "local", ConfigRaw: json.RawMessage("{}"), } @@ -412,13 +412,34 @@ func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType) *backendrun.O // here first is a bug, so panic. panic(fmt.Sprintf("invalid workspace: %s", err)) } - planOutBackend, err := m.backendState.Plan(schema, workspace) - if err != nil { - // Always indicates an implementation error in practice, because - // errors here indicate invalid encoding of the backend configuration - // in memory, and we should always have validated that by the time - // we get here. - panic(fmt.Sprintf("failed to encode backend configuration for plan: %s", err)) + + var planOutBackend *plans.Backend + var planOutStateStore *plans.StateStore + switch { + case m.backendConfigState == nil && m.stateStoreConfigState == nil: + // Neither set + panic(fmt.Sprintf("failed to encode backend configuration for plan: neither backend nor state_store data present")) + case m.backendConfigState != nil && m.stateStoreConfigState != nil: + // Both set + panic(fmt.Sprintf("failed to encode backend configuration for plan: both backend and state_store data present but they are mutually exclusive")) + case m.backendConfigState != nil: + planOutBackend, err = m.backendConfigState.Plan(schema, workspace) + if err != nil { + // Always indicates an implementation error in practice, because + // errors here indicate invalid encoding of the backend configuration + // in memory, and we should always have validated that by the time + // we get here. + panic(fmt.Sprintf("failed to encode backend configuration for plan: %s", err)) + } + case m.stateStoreConfigState != nil: + planOutStateStore, err = m.stateStoreConfigState.Plan(schema, workspace) + if err != nil { + // Always indicates an implementation error in practice, because + // errors here indicate invalid encoding of the state_store configuration + // in memory, and we should always have validated that by the time + // we get here. + panic(fmt.Sprintf("failed to encode state_store configuration for plan: %s", err)) + } } stateLocker := clistate.NewNoopLocker() @@ -438,7 +459,12 @@ func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType) *backendrun.O } return &backendrun.Operation{ - PlanOutBackend: planOutBackend, + + // These two fields are mutually exclusive, + // and one is being assigned a nil value below. + PlanOutBackend: planOutBackend, + PlanOutStateStore: planOutStateStore, + Targets: m.targets, UIIn: m.UIInput(), UIOut: m.Ui, @@ -578,10 +604,10 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // Upon return, we want to set the state we're using in-memory so that // we can access it for commands. - m.backendState = nil + m.backendConfigState = nil defer func() { if s := sMgr.State(); s != nil && !s.Backend.Empty() { - m.backendState = s.Backend + m.backendConfigState = s.Backend } }() From 5b825b2be47df1334802adc56ece54e7d3555264 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Tue, 17 Jun 2025 20:02:43 +0100 Subject: [PATCH 09/12] WIP - allowing apply operation to use plan file content about backend/state_store --- internal/command/apply.go | 6 +++--- internal/command/meta_backend.go | 25 +++++++++++++++++-------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/internal/command/apply.go b/internal/command/apply.go index be6b8daa63a7..c0c029c22996 100644 --- a/internal/command/apply.go +++ b/internal/command/apply.go @@ -210,16 +210,16 @@ func (c *ApplyCommand) PrepareBackend(planFile *planfile.WrappedPlanFile, args * )) return nil, diags } - if plan.Backend.Config == nil { + if plan.Backend.Config == nil && plan.StateStore.Config == nil { // Should never happen; always indicates a bug in the creation of the plan file diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Failed to read plan from plan file", - "The given plan file does not have a valid backend configuration. This is a bug in the Terraform command that generated this plan file.", + "The given plan file does not have either a valid backend or state_store configuration. This is a bug in the Terraform command that generated this plan file.", )) return nil, diags } - be, beDiags = c.BackendForLocalPlan(plan.Backend) + be, beDiags = c.BackendForLocalPlan(plan) } else { // Both new plans and saved cloud plans load their backend from config. backendConfig, configDiags := c.loadBackendConfig(".") diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 0bdd7470d1b4..a361e8062583 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -302,19 +302,28 @@ func (m *Meta) selectWorkspace(b backend.Backend) error { // The current workspace name is also stored as part of the plan, and so this // method will check that it matches the currently-selected workspace name // and produce error diagnostics if not. -func (m *Meta) BackendForLocalPlan(settings plans.Backend) (backendrun.OperationsBackend, tfdiags.Diagnostics) { +func (m *Meta) BackendForLocalPlan(plan *plans.Plan) (backendrun.OperationsBackend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - f := backendInit.Backend(settings.Type) - if f == nil { - diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), settings.Type)) - return nil, diags + var b backend.Backend + var config plans.DynamicValue + if plan.StateStore.Config != nil { + // TODO - code for state_store + } else if plan.Backend.Config != nil { + settings := plan.Backend + config = plan.Backend.Config + + f := backendInit.Backend(settings.Type) + if f == nil { + diags = diags.Append(fmt.Errorf(strings.TrimSpace(errBackendSavedUnknown), settings.Type)) + return nil, diags + } + b = f() + log.Printf("[TRACE] Meta.BackendForLocalPlan: instantiated backend of type %T", b) } - b := f() - log.Printf("[TRACE] Meta.BackendForLocalPlan: instantiated backend of type %T", b) schema := b.ConfigSchema() - configVal, err := settings.Config.Decode(schema.ImpliedType()) + configVal, err := config.Decode(schema.ImpliedType()) if err != nil { diags = diags.Append(fmt.Errorf("saved backend configuration is invalid: %w", err)) return nil, diags From 19ede22b38f25bef507ad177872e55d2d3ac5f0d Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 18 Jun 2025 10:35:04 +0100 Subject: [PATCH 10/12] Fix - it is valid to not have a backend config in the plan file --- internal/command/meta_backend.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index a361e8062583..570843c05116 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -426,8 +426,8 @@ func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType) *backendrun.O var planOutStateStore *plans.StateStore switch { case m.backendConfigState == nil && m.stateStoreConfigState == nil: - // Neither set - panic(fmt.Sprintf("failed to encode backend configuration for plan: neither backend nor state_store data present")) + // It is valid for neither to be set. + // So we do nothing here. case m.backendConfigState != nil && m.stateStoreConfigState != nil: // Both set panic(fmt.Sprintf("failed to encode backend configuration for plan: both backend and state_store data present but they are mutually exclusive")) From 571bffcb1a8242bafa6babbb1ad7d1a58b04ceb5 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 18 Jun 2025 10:38:30 +0100 Subject: [PATCH 11/12] Update test following change to BackendForLocalPlan argument --- internal/command/meta_backend_test.go | 36 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index e0a08750cc7e..d7e5ca03827c 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -1543,17 +1543,19 @@ func TestMetaBackend_planLocal(t *testing.T) { if err != nil { t.Fatal(err) } - backendConfig := plans.Backend{ - Type: "local", - Config: backendConfigRaw, - Workspace: "default", + plan := &plans.Plan{ + Backend: plans.Backend{ + Type: "local", + Config: backendConfigRaw, + Workspace: "default", + }, } // Setup the meta m := testMetaBackend(t, nil) // Get the backend - b, diags := m.BackendForLocalPlan(backendConfig) + b, diags := m.BackendForLocalPlan(plan) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1634,10 +1636,12 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) { if err != nil { t.Fatal(err) } - plannedBackend := plans.Backend{ - Type: "local", - Config: backendConfigRaw, - Workspace: "default", + plan := &plans.Plan{ + Backend: plans.Backend{ + Type: "local", + Config: backendConfigRaw, + Workspace: "default", + }, } // Create an alternate output path @@ -1654,7 +1658,7 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) { m.stateOutPath = statePath // Get the backend - b, diags := m.BackendForLocalPlan(plannedBackend) + b, diags := m.BackendForLocalPlan(plan) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1733,17 +1737,19 @@ func TestMetaBackend_planLocalMatch(t *testing.T) { if err != nil { t.Fatal(err) } - backendConfig := plans.Backend{ - Type: "local", - Config: backendConfigRaw, - Workspace: "default", + plan := &plans.Plan{ + Backend: plans.Backend{ + Type: "local", + Config: backendConfigRaw, + Workspace: "default", + }, } // Setup the meta m := testMetaBackend(t, nil) // Get the backend - b, diags := m.BackendForLocalPlan(backendConfig) + b, diags := m.BackendForLocalPlan(plan) if diags.HasErrors() { t.Fatal(diags.Err()) } From a5a439fd1813b256c564ee13cae6343b956d4cdb Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 18 Jun 2025 11:43:32 +0100 Subject: [PATCH 12/12] Fix code consistency issue --- internal/command/meta_backend.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 570843c05116..ae4996015bc6 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -430,7 +430,7 @@ func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType) *backendrun.O // So we do nothing here. case m.backendConfigState != nil && m.stateStoreConfigState != nil: // Both set - panic(fmt.Sprintf("failed to encode backend configuration for plan: both backend and state_store data present but they are mutually exclusive")) + panic("failed to encode backend configuration for plan: both backend and state_store data present but they are mutually exclusive") case m.backendConfigState != nil: planOutBackend, err = m.backendConfigState.Plan(schema, workspace) if err != nil {