Skip to content

Update how operations use backend config state in context of PSS #37248

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: pss/store-pss-in-planfile
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions internal/backend/backendrun/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 8 additions & 1 deletion internal/backend/local/backend_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions internal/command/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(".")
Expand Down
5 changes: 3 additions & 2 deletions internal/command/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 55 additions & 20 deletions internal/command/meta_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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("{}"),
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -412,13 +421,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:
// It is valid for neither to be set.
// So we do nothing here.
case m.backendConfigState != nil && m.stateStoreConfigState != nil:
// Both set
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 {
// 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()
Expand All @@ -438,7 +468,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,
Expand Down Expand Up @@ -578,10 +613,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
}
}()

Expand Down
36 changes: 21 additions & 15 deletions internal/command/meta_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down Expand Up @@ -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
Expand All @@ -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())
}
Expand Down Expand Up @@ -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())
}
Expand Down