Skip to content

Commit 2349481

Browse files
authored
Update data stored in plan files to enable using PSS with saved plans (#37246)
* Implement `ForPlan` method on `StateStoreConfigState`, add `Planner` interface * Rename `ForPlan` method to `Plan` * Allow plan files to contain information about state stores * Add code needed for representing a state store in the Plan struct, which is used when handling plan files * Add ability to read/write either a backend or state store's data in a plan file. Add some test coverage. * Update plan's `ProviderAddrs` method to include the provider used for PSS, if present * Split interfaces * Apply feedback from code review * Refactor `SetVersion` to use appropriate constructor * Split `ProviderAddrs` method test into two * Fix method call after rename * Fix test * Remove change to `(p *Plan) ProviderAddrs()` We may re-add this when we implement PSS for use during apply commands with plan files * Remove changes to test, now that the plan doesn't report the provider used for PSS anymore.
1 parent ab04e5c commit 2349481

File tree

10 files changed

+543
-158
lines changed

10 files changed

+543
-158
lines changed

internal/command/meta_backend.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType) *backendrun.O
411411
// here first is a bug, so panic.
412412
panic(fmt.Sprintf("invalid workspace: %s", err))
413413
}
414-
planOutBackend, err := m.backendState.ForPlan(schema, workspace)
414+
planOutBackend, err := m.backendState.PlanData(schema, workspace)
415415
if err != nil {
416416
// Always indicates an implementation error in practice, because
417417
// errors here indicate invalid encoding of the backend configuration

internal/command/workdir/backend_config_state.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import (
1515
"github.com/hashicorp/terraform/internal/plans"
1616
)
1717

18-
var _ ConfigState[BackendConfigState] = &BackendConfigState{}
18+
var _ ConfigState = &BackendConfigState{}
19+
var _ DeepCopier[BackendConfigState] = &BackendConfigState{}
20+
var _ PlanDataProvider[plans.Backend] = &BackendConfigState{}
1921

2022
// BackendConfigState describes the physical storage format for the backend state
2123
// in a working directory, and provides the lowest-level API for decoding it.
@@ -64,13 +66,13 @@ func (s *BackendConfigState) SetConfig(val cty.Value, schema *configschema.Block
6466
return nil
6567
}
6668

67-
// ForPlan produces an alternative representation of the receiver that is
69+
// PlanData produces an alternative representation of the receiver that is
6870
// suitable for storing in a plan. The current workspace must additionally
6971
// be provided, to be stored alongside the backend configuration.
7072
//
7173
// The backend configuration schema is required in order to properly
7274
// encode the backend-specific configuration settings.
73-
func (s *BackendConfigState) ForPlan(schema *configschema.Block, workspaceName string) (*plans.Backend, error) {
75+
func (s *BackendConfigState) PlanData(schema *configschema.Block, workspaceName string) (*plans.Backend, error) {
7476
if s == nil {
7577
return nil, nil
7678
}

internal/command/workdir/config_state.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,24 @@ package workdir
55

66
import (
77
"github.com/hashicorp/terraform/internal/configs/configschema"
8-
"github.com/hashicorp/terraform/internal/plans"
98
"github.com/zclconf/go-cty/cty"
109
)
1110

1211
// ConfigState describes a configuration block, and is used to make that config block stateful.
13-
type ConfigState[T any] interface {
12+
type ConfigState interface {
1413
Empty() bool
15-
Config(*configschema.Block) (cty.Value, error)
16-
SetConfig(cty.Value, *configschema.Block) error
17-
ForPlan(*configschema.Block, string) (*plans.Backend, error)
14+
Config(schema *configschema.Block) (cty.Value, error)
15+
SetConfig(val cty.Value, schema *configschema.Block) error
16+
}
17+
18+
// DeepCopier implementations can return deep copies of themselves for use elsewhere
19+
// without mutating the original value.
20+
type DeepCopier[T any] interface {
1821
DeepCopy() *T
1922
}
23+
24+
// PlanDataProvider implementations can return a representation of their data that's
25+
// appropriate for storing in a plan file.
26+
type PlanDataProvider[T any] interface {
27+
PlanData(schema *configschema.Block, workspaceName string) (*T, error)
28+
}

internal/command/workdir/statestore_config_state.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import (
1616
ctyjson "github.com/zclconf/go-cty/cty/json"
1717
)
1818

19-
var _ ConfigState[StateStoreConfigState] = &StateStoreConfigState{}
19+
var _ ConfigState = &StateStoreConfigState{}
20+
var _ DeepCopier[StateStoreConfigState] = &StateStoreConfigState{}
21+
var _ PlanDataProvider[plans.StateStore] = &StateStoreConfigState{}
2022

2123
// StateStoreConfigState describes the physical storage format for the state store
2224
type StateStoreConfigState struct {
@@ -98,19 +100,26 @@ func (s *StateStoreConfigState) SetConfig(val cty.Value, schema *configschema.Bl
98100
return nil
99101
}
100102

101-
// ForPlan produces an alternative representation of the receiver that is
103+
// PlanData produces an alternative representation of the receiver that is
102104
// suitable for storing in a plan. The current workspace must additionally
103105
// be provided, to be stored alongside the state store configuration.
104106
//
105107
// The state_store configuration schema is required in order to properly
106108
// encode the state store-specific configuration settings.
107-
func (s *StateStoreConfigState) ForPlan(schema *configschema.Block, workspaceName string) (*plans.Backend, error) {
109+
func (s *StateStoreConfigState) PlanData(schema *configschema.Block, workspaceName string) (*plans.StateStore, error) {
108110
if s == nil {
109111
return nil, nil
110112
}
111-
// TODO
112-
// What should a pluggable state store look like in a plan?
113-
return nil, nil
113+
114+
if err := s.Validate(); err != nil {
115+
return nil, fmt.Errorf("error when preparing state store config for planfile: %s", err)
116+
}
117+
118+
configVal, err := s.Config(schema)
119+
if err != nil {
120+
return nil, fmt.Errorf("failed to decode state_store config: %w", err)
121+
}
122+
return plans.NewStateStore(s.Type, s.Provider.Version, &s.Provider.Source, configVal, schema, workspaceName)
114123
}
115124

116125
func (s *StateStoreConfigState) DeepCopy() *StateStoreConfigState {

internal/plans/plan.go

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99

1010
"github.com/zclconf/go-cty/cty"
1111

12+
version "github.com/hashicorp/go-version"
13+
tfaddr "github.com/hashicorp/terraform-registry-address"
1214
"github.com/hashicorp/terraform/internal/addrs"
1315
"github.com/hashicorp/terraform/internal/collections"
1416
"github.com/hashicorp/terraform/internal/configs/configschema"
@@ -69,7 +71,9 @@ type Plan struct {
6971
DeferredResources []*DeferredResourceInstanceChangeSrc
7072
TargetAddrs []addrs.Targetable
7173
ForceReplaceAddrs []addrs.AbsResourceInstance
72-
Backend Backend
74+
75+
Backend Backend
76+
StateStore StateStore
7377

7478
// Complete is true if Terraform considers this to be a "complete" plan,
7579
// which is to say that it includes a planned action (even if no-op)
@@ -228,3 +232,76 @@ func NewBackend(typeName string, config cty.Value, configSchema *configschema.Bl
228232
Workspace: workspaceName,
229233
}, nil
230234
}
235+
236+
// StateStore represents the state store-related configuration and other data as it
237+
// existed when a plan was created.
238+
type StateStore struct {
239+
// Type is the type of state store that the plan will apply against.
240+
Type string
241+
242+
Provider *Provider
243+
244+
// Config is the configuration of the backend, whose schema is decided by
245+
// the backend Type.
246+
Config DynamicValue
247+
248+
// Workspace is the name of the workspace that was active when the plan
249+
// was created. It is illegal to apply a plan created for one workspace
250+
// to the state of another workspace.
251+
// (This constraint is already enforced by the statefile lineage mechanism,
252+
// but storing this explicitly allows us to return a better error message
253+
// in the situation where the user has the wrong workspace selected.)
254+
Workspace string
255+
}
256+
257+
type Provider struct {
258+
Version *version.Version // The specific provider version used for the state store. Should be set using a getproviders.Version, etc.
259+
Source *tfaddr.Provider // The FQN/fully-qualified name of the provider.
260+
}
261+
262+
func NewStateStore(typeName string, ver *version.Version, source *tfaddr.Provider, config cty.Value, configSchema *configschema.Block, workspaceName string) (*StateStore, error) {
263+
dv, err := NewDynamicValue(config, configSchema.ImpliedType())
264+
if err != nil {
265+
return nil, err
266+
}
267+
268+
provider := &Provider{
269+
Version: ver,
270+
Source: source,
271+
}
272+
273+
return &StateStore{
274+
Type: typeName,
275+
Provider: provider,
276+
Config: dv,
277+
Workspace: workspaceName,
278+
}, nil
279+
}
280+
281+
// SetVersion includes logic for parsing a string representation of a version,
282+
// for example data read from a plan file.
283+
// If an error occurs it is returned and the receiver's Version field is unchanged.
284+
// If there are no errors then the receiver's Version field is updated.
285+
func (p *Provider) SetVersion(input string) error {
286+
ver, err := version.NewVersion(input)
287+
if err != nil {
288+
return err
289+
}
290+
291+
p.Version = ver
292+
return nil
293+
}
294+
295+
// SetSource includes logic for parsing a string representation of a provider source,
296+
// for example data read from a plan file.
297+
// If an error occurs it is returned and the receiver's Source field is unchanged.
298+
// If there are no errors then the receiver's Source field is updated.
299+
func (p *Provider) SetSource(input string) error {
300+
source, diags := addrs.ParseProviderSourceString(input)
301+
if diags.HasErrors() {
302+
return diags.ErrWithWarnings()
303+
}
304+
305+
p.Source = &source
306+
return nil
307+
}

internal/plans/plan_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
func TestProviderAddrs(t *testing.T) {
1515

16+
// Prepare plan
1617
plan := &Plan{
1718
VariableValues: map[string]DynamicValue{},
1819
Changes: &ChangesSrc{
@@ -57,11 +58,12 @@ func TestProviderAddrs(t *testing.T) {
5758

5859
got := plan.ProviderAddrs()
5960
want := []addrs.AbsProviderConfig{
60-
addrs.AbsProviderConfig{
61+
// Providers used for managed resources
62+
{
6163
Module: addrs.RootModule.Child("foo"),
6264
Provider: addrs.NewDefaultProvider("test"),
6365
},
64-
addrs.AbsProviderConfig{
66+
{
6567
Module: addrs.RootModule,
6668
Provider: addrs.NewDefaultProvider("test"),
6769
},

internal/plans/planfile/tfplan.go

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,17 @@ func readTfplan(r io.Reader) (*plans.Plan, error) {
179179
)
180180
}
181181

182-
if rawBackend := rawPlan.Backend; rawBackend == nil {
183-
return nil, fmt.Errorf("plan file has no backend settings; backend settings are required")
184-
} else {
182+
switch {
183+
case rawPlan.Backend == nil && rawPlan.StateStore == nil:
184+
// Similar validation in writeTfPlan should prevent this occurring
185+
return nil,
186+
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.")
187+
case rawPlan.Backend != nil && rawPlan.StateStore != nil:
188+
// Similar validation in writeTfPlan should prevent this occurring
189+
return nil,
190+
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.")
191+
case rawPlan.Backend != nil:
192+
rawBackend := rawPlan.Backend
185193
config, err := valueFromTfplan(rawBackend.Config)
186194
if err != nil {
187195
return nil, fmt.Errorf("plan file has invalid backend configuration: %s", err)
@@ -191,6 +199,28 @@ func readTfplan(r io.Reader) (*plans.Plan, error) {
191199
Config: config,
192200
Workspace: rawBackend.Workspace,
193201
}
202+
case rawPlan.StateStore != nil:
203+
rawStateStore := rawPlan.StateStore
204+
config, err := valueFromTfplan(rawStateStore.Config)
205+
if err != nil {
206+
return nil, fmt.Errorf("plan file has invalid state_store configuration: %s", err)
207+
}
208+
provider := &plans.Provider{}
209+
err = provider.SetSource(rawStateStore.Provider.Source)
210+
if err != nil {
211+
return nil, fmt.Errorf("plan file has invalid state_store provider source: %s", err)
212+
}
213+
err = provider.SetVersion(rawStateStore.Provider.Version)
214+
if err != nil {
215+
return nil, fmt.Errorf("plan file has invalid state_store provider version: %s", err)
216+
}
217+
218+
plan.StateStore = plans.StateStore{
219+
Type: rawStateStore.Type,
220+
Provider: provider,
221+
Config: config,
222+
Workspace: rawStateStore.Workspace,
223+
}
194224
}
195225

196226
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 {
636666
)
637667
}
638668

639-
if plan.Backend.Type == "" || plan.Backend.Config == nil {
669+
// Store details about accessing state
670+
backendInUse := plan.Backend.Type != "" && plan.Backend.Config != nil
671+
stateStoreInUse := plan.StateStore.Type != "" && plan.StateStore.Config != nil
672+
switch {
673+
case !backendInUse && !stateStoreInUse:
640674
// This suggests a bug in the code that created the plan, since it
641-
// ought to always have a backend populated, even if it's the default
675+
// ought to always have either a backend or state_store populated, even if it's the default
642676
// "local" backend with a local state file.
643-
return fmt.Errorf("plan does not have a backend configuration")
644-
}
645-
646-
rawPlan.Backend = &planproto.Backend{
647-
Type: plan.Backend.Type,
648-
Config: valueToTfplan(plan.Backend.Config),
649-
Workspace: plan.Backend.Workspace,
677+
return fmt.Errorf("plan does not have a backend or state_store configuration")
678+
case backendInUse && stateStoreInUse:
679+
// This suggests a bug in the code that created the plan, since it
680+
// should never have both a backend and state_store populated.
681+
return fmt.Errorf("plan contains both backend and state_store configurations, only one is expected")
682+
case backendInUse:
683+
rawPlan.Backend = &planproto.Backend{
684+
Type: plan.Backend.Type,
685+
Config: valueToTfplan(plan.Backend.Config),
686+
Workspace: plan.Backend.Workspace,
687+
}
688+
case stateStoreInUse:
689+
rawPlan.StateStore = &planproto.StateStore{
690+
Type: plan.StateStore.Type,
691+
Provider: &planproto.Provider{
692+
Version: plan.StateStore.Provider.Version.String(),
693+
Source: plan.StateStore.Provider.Source.String(),
694+
},
695+
Config: valueToTfplan(plan.StateStore.Config),
696+
Workspace: plan.StateStore.Workspace,
697+
}
650698
}
651699

652700
rawPlan.Timestamp = plan.Timestamp.Format(time.RFC3339)

0 commit comments

Comments
 (0)