Skip to content

[DNM/WIP] PSS: Add logic for initialising a new working directory with PSS. #37323

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 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a758e30
Store the FQN of the provider used in PSS in representations of the p…
SarahFrench Jul 10, 2025
ce84718
Update affected tests, improve error diagnostic
SarahFrench Jul 11, 2025
7323ab0
Begin enabling method to return a backend.Backend made using state_st…
SarahFrench Jul 11, 2025
1d30b5c
Update code's logic to include possibility of a state store being in use
SarahFrench Jul 11, 2025
7425707
Add empty cases for handle all broad init scenarios involving PSS
SarahFrench Jul 11, 2025
34ac3cd
Update default case's error to report state store variables
SarahFrench Jul 11, 2025
0ccbea8
Improve how we resolve the builtin terraform provider's address
SarahFrench Jul 11, 2025
9ba1c91
Add test that hits the code path for adding a state store to a new (o…
SarahFrench Jul 11, 2025
8e17bce
Add test for use of `-reconfigure` flag; show that it hits the code p…
SarahFrench Jul 11, 2025
b73f7b0
Add test that hits the code path for removing use of a state store, m…
SarahFrench Jul 11, 2025
b282889
Add test that hits the code path for changing a state store's configu…
SarahFrench Jul 11, 2025
f6016d3
Update existing test names to be backend-specific
SarahFrench Jul 11, 2025
4e86d59
Add tests that hits the code path for migrating between PSS and backends
SarahFrench Jul 11, 2025
81e0862
Consolidate PSS-related tests at end of the file
SarahFrench Jul 11, 2025
db73ce9
Fix log text
SarahFrench Jul 14, 2025
e97ed3c
Add test showing that using variables is disallowed with state_store …
SarahFrench Jul 14, 2025
602a37c
Update test name
SarahFrench Jul 14, 2025
206f923
Fix test cases
SarahFrench Jul 14, 2025
ea1f705
Make init command output report if a backend or state store is being …
SarahFrench Jul 14, 2025
d06b639
Allow backend or state_store config to be passed via BackendOpts from…
SarahFrench Jul 14, 2025
8ca1b58
Only process -backend-config flags for use with backend if the user h…
SarahFrench Jul 14, 2025
f3221a2
Implement initialising an uninitialised dir with PSS
SarahFrench Jul 14, 2025
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: 9 additions & 0 deletions internal/backend/pluggable/pluggable.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ func (p *Pluggable) ConfigSchema() *configschema.Block {
return val.Body
}

// ProviderSchema returns the schema for the provider implementing the state store.
//
// This isn't part of the backend.Backend interface but is needed in calling code.
// When it's used the backend.Backend will need to be cast to a Pluggable.
func (p *Pluggable) ProviderSchema() *configschema.Block {
schemaResp := p.provider.GetProviderSchema()
return schemaResp.Provider.Body
}

// PrepareConfig validates configuration for the state store in
// the state storage provider. The configuration sent from Terraform core
// will not include any values from environment variables; it is the
Expand Down
171 changes: 152 additions & 19 deletions internal/command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"errors"
"fmt"
"log"
"maps"
"reflect"
"slices"
"sort"
"strings"

Expand All @@ -28,6 +30,8 @@ import (
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/didyoumean"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/providercache"
"github.com/hashicorp/terraform/internal/states"
Expand Down Expand Up @@ -184,7 +188,11 @@ func (c *InitCommand) Run(args []string) int {
case initArgs.Cloud && rootModEarly.CloudConfig != nil:
back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
case initArgs.Backend:
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view)
// This handles case when config contains either backend or state_store blocks.
// This is valid as either can be implementations of backend.Backend, which is what we
// obtain here.
var locks *depsfile.Locks
back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, locks, view)
default:
// load the previously-stored backend config
back, backDiags = c.Meta.backendFromState(ctx)
Expand Down Expand Up @@ -417,16 +425,129 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra
return back, true, diags
}

func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, viewType arguments.ViewType, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, viewType arguments.ViewType, locks *depsfile.Locks, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
ctx, span := tracer.Start(ctx, "initialize backend")
_ = ctx // prevent staticcheck from complaining to avoid a maintenence hazard of having the wrong ctx in scope here
defer span.End()

view.Output(views.InitializingBackendMessage)
if root.StateStore != nil {
view.Output(views.InitializingStateStoreMessage)
} else {
view.Output(views.InitializingBackendMessage)
}

var opts *BackendOpts
switch {
case root.StateStore != nil && root.Backend != nil:
// We expect validation during config parsing to prevent mutually exclusive backend and state_store blocks,
// but checking here just in case.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Conflicting backend and state_store configurations present during init",
Detail: fmt.Sprintf("When initializing the backend there was configuration data present for both backend %q and state store %q. This is a bug in Terraform and should be reported.",
root.Backend.Type,
root.StateStore.Type,
),
Subject: &root.Backend.TypeRange,
})
return nil, true, diags
case root.StateStore != nil:
// state_store config present
// Access provider factories
ctxOpts, err := c.contextOpts()
if err != nil {
diags = diags.Append(err)
return nil, true, diags
}

var backendConfig *configs.Backend
var backendConfigOverride hcl.Body
if root.Backend != nil {
if root.StateStore.ProviderAddr.IsZero() {
// This should not happen; this data is populated when parsing config,
// even for builtin providers
panic(fmt.Sprintf("unknown provider while beginning to initialize state store %q from provider %q",
root.StateStore.Type,
root.StateStore.Provider.Name))
}

var exists bool
factory, exists := ctxOpts.Providers[root.StateStore.ProviderAddr]
if !exists {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Provider unavailable",
Detail: fmt.Sprintf("The provider %s (%q) is required to initialize the %q state store, but the matching provider factory is missing. This is a bug in Terraform and should be reported.",
root.StateStore.Provider.Name,
root.StateStore.ProviderAddr,
root.StateStore.Type,
),
Subject: &root.Backend.TypeRange,
})
return nil, true, diags
}

// If overrides supplied by -backend-config CLI flag, process them
var configOverride hcl.Body
if len(*extraConfig.Items) > 0 {

// We need to launch an instance of the provider to get the config of the state store for processing any overrides.
provider, err := factory()
defer provider.Close() // Stop the child process once we're done with it here.
if err != nil {
diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err))
return nil, true, diags
}

resp := provider.GetProviderSchema()

if len(resp.StateStores) == 0 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Provider does not support pluggable state storage",
Detail: fmt.Sprintf("There are no state stores implemented by provider %s (%q)",
root.StateStore.Provider.Name,
root.StateStore.ProviderAddr),
Subject: &root.StateStore.DeclRange,
})
return nil, true, diags
}

stateStoreSchema, exists := resp.StateStores[root.StateStore.Type]
if !exists {
suggestions := slices.Sorted(maps.Keys(resp.StateStores))
suggestion := didyoumean.NameSuggestion(root.StateStore.Type, suggestions)
if suggestion != "" {
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "State store not implemented by the provider",
Detail: fmt.Sprintf("State store %q is not implemented by provider %s (%q)%s",
root.StateStore.Type, root.StateStore.Provider.Name,
root.StateStore.ProviderAddr, suggestion),
Subject: &root.StateStore.DeclRange,
})
return nil, true, diags
}

// Handle any overrides supplied via -backend-config CLI flags
var overrideDiags tfdiags.Diagnostics
configOverride, overrideDiags = c.backendConfigOverrideBody(extraConfig, stateStoreSchema.Body)
diags = diags.Append(overrideDiags)
if overrideDiags.HasErrors() {
return nil, true, diags
}
}

opts = &BackendOpts{
StateStoreConfig: root.StateStore,
Locks: locks,
ProviderFactory: factory,
ConfigOverride: configOverride,
Init: true,
ViewType: viewType,
}

case root.Backend != nil:
// backend config present
backendType := root.Backend.Type
if backendType == "cloud" {
diags = diags.Append(&hcl.Diagnostic{
Expand Down Expand Up @@ -456,15 +577,28 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ext

b := bf()
backendSchema := b.ConfigSchema()
backendConfig = root.Backend
backendConfig := root.Backend

var configOverride hcl.Body
if len(*extraConfig.Items) > 0 {
var overrideDiags tfdiags.Diagnostics
configOverride, overrideDiags = c.backendConfigOverrideBody(extraConfig, backendSchema)
diags = diags.Append(overrideDiags)
if overrideDiags.HasErrors() {
return nil, true, diags
}
}

var overrideDiags tfdiags.Diagnostics
backendConfigOverride, overrideDiags = c.backendConfigOverrideBody(extraConfig, backendSchema)
diags = diags.Append(overrideDiags)
if overrideDiags.HasErrors() {
return nil, true, diags
opts = &BackendOpts{
BackendConfig: backendConfig,
ConfigOverride: configOverride,
Init: true,
ViewType: viewType,
}
} else {

default:
// No config; defaults to local state storage

// If the user supplied a -backend-config on the CLI but no backend
// block was found in the configuration, it's likely - but not
// necessarily - a mistake. Return a warning.
Expand All @@ -486,14 +620,13 @@ However, if you intended to override a defined backend, please verify that
the backend configuration is present and valid.
`,
))

}
}

opts := &BackendOpts{
BackendConfig: backendConfig,
ConfigOverride: backendConfigOverride,
Init: true,
ViewType: viewType,
opts = &BackendOpts{
Init: true,
ViewType: viewType,
}
}

back, backDiags := c.Backend(opts)
Expand Down
Loading
Loading