Skip to content
Merged
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
158 changes: 140 additions & 18 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,7 @@ 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/didyoumean"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/providercache"
"github.com/hashicorp/terraform/internal/states"
Expand Down Expand Up @@ -390,7 +393,7 @@ func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, ear

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

view.Output(views.InitializingTerraformCloudMessage)
Expand Down Expand Up @@ -419,14 +422,125 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra

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) {
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
_ = ctx // prevent staticcheck from complaining to avoid a maintenance 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
}

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 !extraConfig.Empty() {
// 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()

var backendConfig *configs.Backend
var backendConfigOverride hcl.Body
if root.Backend != nil {
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,
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 +570,24 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ext

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

var overrideDiags tfdiags.Diagnostics
backendConfigOverride, overrideDiags = c.backendConfigOverrideBody(extraConfig, backendSchema)
backendConfigOverride, overrideDiags := c.backendConfigOverrideBody(extraConfig, backendSchema)
diags = diags.Append(overrideDiags)
if overrideDiags.HasErrors() {
return nil, true, diags
}
} else {

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

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 +609,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 Expand Up @@ -896,7 +1018,7 @@ func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config,
if !newLocks.Equal(previousLocks) {
// if readonly mode
if flagLockfile == "readonly" {
// check if required provider dependences change
// check if required provider dependencies change
if !newLocks.EqualProviderAddress(previousLocks) {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
Expand Down Expand Up @@ -1114,7 +1236,7 @@ Options:
itself.

-force-copy Suppress prompts about copying state data when
initializating a new state backend. This is
initializing a new state backend. This is
equivalent to providing a "yes" to all confirmation
prompts.

Expand Down
5 changes: 5 additions & 0 deletions internal/command/views/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMe
HumanValue: "\n[reset][bold]Initializing provider plugins...",
JSONValue: "Initializing provider plugins...",
},
"initializing_state_store_message": {
HumanValue: "\n[reset][bold]Initializing the state store...",
JSONValue: "Initializing the state store...",
},
"dependencies_lock_changes_info": {
HumanValue: dependenciesLockChangesInfo,
JSONValue: dependenciesLockChangesInfo,
Expand Down Expand Up @@ -258,6 +262,7 @@ const (
InitializingTerraformCloudMessage InitMessageCode = "initializing_terraform_cloud_message"
InitializingModulesMessage InitMessageCode = "initializing_modules_message"
InitializingBackendMessage InitMessageCode = "initializing_backend_message"
InitializingStateStoreMessage InitMessageCode = "initializing_state_store_message"
InitializingProviderPluginMessage InitMessageCode = "initializing_provider_plugin_message"
LockInfo InitMessageCode = "lock_info"
DependenciesLockChangesInfo InitMessageCode = "dependencies_lock_changes_info"
Expand Down