mirror of
https://github.com/hashicorp/terraform.git
synced 2026-05-28 04:03:27 -04:00
Some checks are pending
build / Determine intended Terraform version (push) Waiting to run
build / Determine Go toolchain version (push) Waiting to run
build / Generate release metadata (push) Blocked by required conditions
build / Build for freebsd_386 (push) Blocked by required conditions
build / Build for linux_386 (push) Blocked by required conditions
build / Build for openbsd_386 (push) Blocked by required conditions
build / Build for windows_386 (push) Blocked by required conditions
build / Build for darwin_amd64 (push) Blocked by required conditions
build / Build for freebsd_amd64 (push) Blocked by required conditions
build / Build for linux_amd64 (push) Blocked by required conditions
build / Build for openbsd_amd64 (push) Blocked by required conditions
build / Build for solaris_amd64 (push) Blocked by required conditions
build / Build for windows_amd64 (push) Blocked by required conditions
build / Build for freebsd_arm (push) Blocked by required conditions
build / Build for linux_arm (push) Blocked by required conditions
build / Build for darwin_arm64 (push) Blocked by required conditions
build / Build for linux_arm64 (push) Blocked by required conditions
build / Build for windows_arm64 (push) Blocked by required conditions
build / Build Docker image for linux_386 (push) Blocked by required conditions
build / Build Docker image for linux_amd64 (push) Blocked by required conditions
build / Build Docker image for linux_arm (push) Blocked by required conditions
build / Build Docker image for linux_arm64 (push) Blocked by required conditions
build / Build e2etest for linux_386 (push) Blocked by required conditions
build / Build e2etest for windows_386 (push) Blocked by required conditions
build / Build e2etest for darwin_amd64 (push) Blocked by required conditions
build / Build e2etest for linux_amd64 (push) Blocked by required conditions
build / Build e2etest for windows_amd64 (push) Blocked by required conditions
build / Build e2etest for linux_arm (push) Blocked by required conditions
build / Build e2etest for darwin_arm64 (push) Blocked by required conditions
build / Build e2etest for linux_arm64 (push) Blocked by required conditions
build / Run e2e test for linux_386 (push) Blocked by required conditions
build / Run e2e test for windows_386 (push) Blocked by required conditions
build / Run e2e test for darwin_amd64 (push) Blocked by required conditions
build / Run e2e test for linux_amd64 (push) Blocked by required conditions
build / Run e2e test for windows_amd64 (push) Blocked by required conditions
build / Run e2e test for linux_arm (push) Blocked by required conditions
build / Run e2e test for linux_arm64 (push) Blocked by required conditions
build / Run terraform-exec test for linux amd64 (push) Blocked by required conditions
Quick Checks / Unit Tests (push) Waiting to run
Quick Checks / Race Tests (push) Waiting to run
Quick Checks / End-to-end Tests (push) Waiting to run
Quick Checks / Code Consistency Checks (push) Waiting to run
* refactor: Replace use of prepareInstallerEvents method. This will allow finer control of callbacks when implementing security related features * feat: Users are prompted to approve a provider used for PSS on first use, and only if downloaded via HTTP. Prompts include signer details and key ID data. * test: Users see "Authentication: unauthenticated" in prompt if network mirror doesn't include hashes They'll see authentication data in all other prompt scenarios. There's no auth when using an fs mirror, but when those are in use we trust the providers already and no prompts are raised. * refactor: Simplify how we prepare installation event callbacks by defining reused callbacks * refactor: Remove unused parameters from `getProvidersFromState`
475 lines
16 KiB
Go
475 lines
16 KiB
Go
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package command
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/backend"
|
|
"github.com/hashicorp/terraform/internal/cloud"
|
|
"github.com/hashicorp/terraform/internal/command/arguments"
|
|
"github.com/hashicorp/terraform/internal/command/views"
|
|
"github.com/hashicorp/terraform/internal/configs"
|
|
"github.com/hashicorp/terraform/internal/depsfile"
|
|
"github.com/hashicorp/terraform/internal/getproviders"
|
|
"github.com/hashicorp/terraform/internal/states"
|
|
"github.com/hashicorp/terraform/internal/terraform"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
c.forceInitCopy = initArgs.ForceInitCopy
|
|
c.Meta.stateLock = initArgs.StateLock
|
|
c.Meta.stateLockTimeout = initArgs.StateLockTimeout
|
|
c.reconfigure = initArgs.Reconfigure
|
|
c.migrateState = initArgs.MigrateState
|
|
c.Meta.ignoreRemoteVersion = initArgs.IgnoreRemoteVersion
|
|
c.Meta.input = initArgs.InputEnabled
|
|
c.Meta.targetFlags = initArgs.TargetFlags
|
|
c.Meta.compactWarnings = initArgs.CompactWarnings
|
|
|
|
// Copying the state only happens during backend migration, so setting
|
|
// -force-copy implies -migrate-state
|
|
if c.forceInitCopy {
|
|
c.migrateState = true
|
|
}
|
|
|
|
if len(initArgs.PluginPath) > 0 {
|
|
c.pluginPath = initArgs.PluginPath
|
|
}
|
|
|
|
// Validate the arg count and get the working directory
|
|
path, err := ModulePath(initArgs.Args)
|
|
if err != nil {
|
|
diags = diags.Append(err)
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
if err := c.storePluginPath(c.pluginPath); err != nil {
|
|
diags = diags.Append(fmt.Errorf("Error saving -plugin-dir to workspace directory: %s", err))
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
// Initialization can be aborted by interruption signals
|
|
ctx, done := c.InterruptibleContext(c.CommandContext())
|
|
defer done()
|
|
|
|
// This will track whether we outputted anything so that we know whether
|
|
// to output a newline before the success message
|
|
var header bool
|
|
|
|
if initArgs.FromModule != "" {
|
|
src := initArgs.FromModule
|
|
|
|
empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory)
|
|
if err != nil {
|
|
diags = diags.Append(fmt.Errorf("Error validating destination directory: %s", err))
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
if !empty {
|
|
diags = diags.Append(errors.New(strings.TrimSpace(errInitCopyNotEmpty)))
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
view.Output(views.CopyingConfigurationMessage, src)
|
|
header = true
|
|
|
|
hooks := uiModuleInstallHooks{
|
|
Ui: c.Ui,
|
|
ShowLocalPaths: false, // since they are in a weird location for init
|
|
View: view,
|
|
}
|
|
|
|
ctx, span := tracer.Start(ctx, "-from-module=...", trace.WithAttributes(
|
|
attribute.String("module_source", src),
|
|
))
|
|
|
|
initDirFromModuleAbort, initDirFromModuleDiags := c.initDirFromModule(ctx, path, src, hooks)
|
|
diags = diags.Append(initDirFromModuleDiags)
|
|
if initDirFromModuleAbort || initDirFromModuleDiags.HasErrors() {
|
|
view.Diagnostics(diags)
|
|
span.SetStatus(codes.Error, "module installation failed")
|
|
span.End()
|
|
return 1
|
|
}
|
|
span.End()
|
|
|
|
view.Output(views.EmptyMessage)
|
|
}
|
|
|
|
// If our directory is empty, then we're done. We can't get or set up
|
|
// the backend with an empty directory.
|
|
empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory)
|
|
if err != nil {
|
|
diags = diags.Append(fmt.Errorf("Error checking configuration: %s", err))
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
if empty {
|
|
view.Output(views.OutputInitEmptyMessage)
|
|
return 0
|
|
}
|
|
|
|
// Load just the root module to begin backend and module initialization
|
|
rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, initArgs.TestsDirectory)
|
|
|
|
// There may be parsing errors in config loading but these will be shown later _after_
|
|
// checking for core version requirement errors. Not meeting the version requirement should
|
|
// be the first error displayed if that is an issue, but other operations are required
|
|
// before being able to check core version requirements.
|
|
if rootModEarly == nil {
|
|
diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)), earlyConfDiags)
|
|
view.Diagnostics(diags)
|
|
|
|
return 1
|
|
}
|
|
if !(c.Meta.AllowExperimentalFeatures && initArgs.EnablePssExperiment) && rootModEarly.StateStore != nil {
|
|
// TODO(SarahFrench/radeksimko) - remove when this feature isn't experimental.
|
|
// This approach for making the feature experimental is required
|
|
// to let us assert the feature is gated behind an experiment in tests.
|
|
// See https://github.com/hashicorp/terraform/pull/37350#issuecomment-3168555619
|
|
|
|
detail := "Pluggable state store is an experiment which requires"
|
|
if !c.Meta.AllowExperimentalFeatures {
|
|
detail += " an experimental build of terraform"
|
|
}
|
|
if !initArgs.EnablePssExperiment {
|
|
if !c.Meta.AllowExperimentalFeatures {
|
|
detail += " and"
|
|
}
|
|
detail += " -enable-pluggable-state-storage-experiment flag"
|
|
}
|
|
|
|
diags = diags.Append(earlyConfDiags)
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Pluggable state store experiment not supported",
|
|
Detail: detail,
|
|
Subject: &rootModEarly.StateStore.TypeRange,
|
|
})
|
|
view.Diagnostics(diags)
|
|
|
|
return 1
|
|
}
|
|
|
|
if initArgs.Get {
|
|
modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, initArgs.TestsDirectory, rootModEarly, initArgs.Upgrade, view)
|
|
diags = diags.Append(modsDiags)
|
|
if modsAbort || modsDiags.HasErrors() {
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
if modsOutput {
|
|
header = true
|
|
}
|
|
}
|
|
|
|
// With all of the modules (hopefully) installed, we can now try to load the
|
|
// whole configuration tree.
|
|
config, confDiags := c.loadConfigWithTests(path, initArgs.TestsDirectory)
|
|
// configDiags will be handled after:
|
|
// - the version constraint check has happened
|
|
// - and, the backend/state_store is initialised
|
|
|
|
// Before we go further, we'll check to make sure none of the modules in
|
|
// the configuration declare that they don't support this Terraform
|
|
// version, so we can produce a version-related error message rather than
|
|
// potentially-confusing downstream errors.
|
|
versionDiags := terraform.CheckCoreVersionRequirements(config)
|
|
if versionDiags.HasErrors() {
|
|
view.Diagnostics(versionDiags)
|
|
return 1
|
|
}
|
|
|
|
earlyBdiags := c.earlyValidateBackend(rootModEarly, initArgs)
|
|
diags = diags.Append(earlyBdiags)
|
|
|
|
// We've passed the core version check, now we can show errors from the early configuration.
|
|
// This prevents trying to initialise the backend with faulty configuration.
|
|
if earlyConfDiags.HasErrors() || earlyBdiags.HasErrors() {
|
|
diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)), earlyConfDiags)
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
// Now the full configuration is loaded, we can download the providers specified in the configuration.
|
|
// This is step one of a two-step provider download process
|
|
// Providers may be downloaded by this code, but the dependency lock file is only updated later in `init`
|
|
// after step two of provider download is complete.
|
|
previousLocks, moreDiags := c.lockedDependencies()
|
|
diags = diags.Append(moreDiags)
|
|
|
|
configProvidersOutput, configLocks, safeInitAction, stateStoreProviderAuthResult, configProviderDiags := c.getProvidersFromConfig(ctx, config, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view)
|
|
diags = diags.Append(configProviderDiags)
|
|
if configProviderDiags.HasErrors() {
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
if configProvidersOutput {
|
|
header = true
|
|
}
|
|
|
|
// Prompt the user about trusting the provider used for state storage.
|
|
// Course of action depends on the safeInitAction returned from getProvidersFromConfig
|
|
switch safeInitAction {
|
|
case SafeInitActionNotRelevant:
|
|
// do nothing; security features aren't relevant.
|
|
case SafeInitActionProceed:
|
|
// do nothing; provider is already trusted and there's no need to notify the user.
|
|
case SafeInitActionPromptForInput:
|
|
diags = diags.Append(c.promptStateStorageProviderApproval(config.Module.StateStore.ProviderAddr, configLocks, stateStoreProviderAuthResult))
|
|
if diags.HasErrors() {
|
|
view.Output(views.StateStoreProviderRejectedMessage)
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
view.Output(views.StateStoreProviderApprovedMessage)
|
|
default:
|
|
// Handle SafeInitActionInvalid or unexpected action types
|
|
panic(fmt.Sprintf("When installing providers described in the config Terraform couldn't determine what 'safe init' action should be taken and returned action type %T. This is a bug in Terraform and should be reported.", safeInitAction))
|
|
}
|
|
|
|
// The init command is not allowed to upgrade the provider used for state storage (unless we're reconfiguring the state store).
|
|
// Unless users choose to reconfigure, they must upgrade the state store provider separately using `terraform state migrate -upgrade`.
|
|
if initArgs.Upgrade && !initArgs.Reconfigure && config.Module.StateStore != nil {
|
|
pAddr := config.Module.StateStore.ProviderAddr
|
|
old := previousLocks.Provider(pAddr)
|
|
new := configLocks.Provider(pAddr)
|
|
if old == nil || new == nil {
|
|
panic(fmt.Sprintf(`Unexpected missing provider lock for %s during init -upgrade:
|
|
prior lock: %#v
|
|
new lock: %#v`, pAddr.ForDisplay(), old, new))
|
|
}
|
|
if !new.Version().Same((old.Version())) {
|
|
// The upgrade has impacted the provider
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Cannot upgrade the provider used for state storage during \"terraform init -upgrade\"",
|
|
fmt.Sprintf(`While upgrading providers Terraform attempted to upgrade the %s (%q) provider, which is used by the state_store block in your configuration.
|
|
Please use \"terraform state migrate -upgrade\" to upgrade the state store provider and navigate migrating your state between the two versions. You can then re-attempt \"terraform init -upgrade\" to upgrade the rest of your providers.
|
|
|
|
If you do not intend to upgrade the state store provider, please update your configuration to pin to the current version (%s), and re-run \"terraform init -upgrade\" to upgrade the rest of your providers.
|
|
`,
|
|
pAddr.Type, pAddr.ForDisplay(), old.Version()),
|
|
),
|
|
)
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
}
|
|
|
|
// If we outputted information, then we need to output a newline
|
|
// so that our success message is nicely spaced out from prior text.
|
|
if header {
|
|
view.Output(views.EmptyMessage)
|
|
}
|
|
|
|
var back backend.Backend
|
|
|
|
var backDiags tfdiags.Diagnostics
|
|
var backendOutput bool
|
|
switch {
|
|
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, configLocks, view)
|
|
default:
|
|
// load the previously-stored backend config
|
|
back, backDiags = c.Meta.backendFromState(ctx)
|
|
}
|
|
if backendOutput {
|
|
header = true
|
|
}
|
|
if header {
|
|
// If we outputted information, then we need to output a newline
|
|
// so that our success message is nicely spaced out from prior text.
|
|
view.Output(views.EmptyMessage)
|
|
}
|
|
|
|
// Show any errors from initializing the backend.
|
|
// No preamble using `InitConfigError` is present, as we expect
|
|
// any errors to from configuring the backend itself.
|
|
diags = diags.Append(backDiags)
|
|
if backDiags.HasErrors() {
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
// If everything is ok with the core version check and backend/state_store initialization,
|
|
// show other errors from loading the full configuration tree.
|
|
diags = diags.Append(confDiags)
|
|
if confDiags.HasErrors() {
|
|
diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)))
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
var state *states.State
|
|
|
|
// If we have a functional backend (either just initialized or initialized
|
|
// on a previous run) we'll use the current state as a potential source
|
|
// of provider dependencies.
|
|
if back != nil {
|
|
c.ignoreRemoteVersionConflict(back)
|
|
workspace, err := c.Workspace()
|
|
if err != nil {
|
|
diags = diags.Append(fmt.Errorf("Error selecting workspace: %s", err))
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
sMgr, sDiags := back.StateMgr(workspace)
|
|
if sDiags.HasErrors() {
|
|
diags = diags.Append(fmt.Errorf("Error loading state: %s", sDiags.Err()))
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
if err := sMgr.RefreshState(); err != nil {
|
|
diags = diags.Append(fmt.Errorf("Error refreshing state: %s", err))
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
state = sMgr.State()
|
|
}
|
|
|
|
// Now the resource state is loaded, we can download the providers specified in the state but not the configuration.
|
|
// This is step two of a two-step provider download process
|
|
configReqs, cReqDiags := config.ProviderRequirements()
|
|
diags = diags.Append(cReqDiags)
|
|
if cReqDiags.HasErrors() {
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
stateProvidersOutput, stateLocks, stateProvidersDiags := c.getProvidersFromState(ctx, state, configReqs, configLocks, initArgs.PluginPath, view)
|
|
diags = diags.Append(stateProvidersDiags)
|
|
if stateProvidersDiags.HasErrors() {
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
if stateProvidersOutput {
|
|
header = true
|
|
}
|
|
if header {
|
|
// If we outputted information, then we need to output a newline
|
|
// so that our success message is nicely spaced out from prior text.
|
|
view.Output(views.EmptyMessage)
|
|
}
|
|
|
|
// Now the two steps of provider download have happened, update the dependency lock file if it has changed.
|
|
lockFileOutput, lockFileDiags := c.saveDependencyLockFile(previousLocks, configLocks, stateLocks, initArgs.Lockfile, view)
|
|
diags = diags.Append(lockFileDiags)
|
|
if lockFileDiags.HasErrors() {
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
if lockFileOutput {
|
|
header = true
|
|
}
|
|
if header {
|
|
// If we outputted information, then we need to output a newline
|
|
// so that our success message is nicely spaced out from prior text.
|
|
view.Output(views.EmptyMessage)
|
|
}
|
|
|
|
if cb, ok := back.(*cloud.Cloud); ok {
|
|
if c.RunningInAutomation {
|
|
if err := cb.AssertImportCompatible(config); err != nil {
|
|
diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Compatibility error", err.Error()))
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we accumulated any warnings along the way that weren't accompanied
|
|
// by errors then we'll output them here so that the success message is
|
|
// still the final thing shown.
|
|
view.Diagnostics(diags)
|
|
_, cloud := back.(*cloud.Cloud)
|
|
output := views.OutputInitSuccessMessage
|
|
if cloud {
|
|
output = views.OutputInitSuccessCloudMessage
|
|
}
|
|
|
|
view.Output(output)
|
|
|
|
if !c.RunningInAutomation {
|
|
// If we're not running in an automation wrapper, give the user
|
|
// some more detailed next steps that are appropriate for interactive
|
|
// shell usage.
|
|
output = views.OutputInitSuccessCLIMessage
|
|
if cloud {
|
|
output = views.OutputInitSuccessCLICloudMessage
|
|
}
|
|
view.Output(output)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// promptStateStorageProviderApproval is used when Terraform is unsure about the safety of the provider downloaded for state storage
|
|
// purposes, and we need to prompt the user to approve or reject using it.
|
|
func (c *InitCommand) promptStateStorageProviderApproval(stateStorageProvider addrs.Provider, configLocks *depsfile.Locks, authResult *getproviders.PackageAuthenticationResult) tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// If we can receive input then we prompt for ok from the user
|
|
lock := configLocks.Provider(stateStorageProvider)
|
|
|
|
var hashList strings.Builder
|
|
for _, hash := range lock.PreferredHashes() {
|
|
hashList.WriteString(fmt.Sprintf("- %s\n", hash))
|
|
}
|
|
|
|
var authentication string
|
|
if authResult != nil && authResult.KeyID != "" {
|
|
authentication = fmt.Sprintf("%s, key ID %s", authResult.String(), authResult.KeyID)
|
|
} else {
|
|
authentication = authResult.String()
|
|
}
|
|
|
|
v, err := c.UIInput().Input(context.Background(), &terraform.InputOpts{
|
|
Id: "approve",
|
|
Query: fmt.Sprintf(`Do you want to use provider %q (%s), version %s, for managing state?
|
|
Platform: %s
|
|
Authentication: %s
|
|
Hashes:
|
|
%s
|
|
`,
|
|
lock.Provider().Type,
|
|
lock.Provider(),
|
|
lock.Version(),
|
|
getproviders.CurrentPlatform.String(),
|
|
authentication,
|
|
hashList.String(),
|
|
),
|
|
Description: fmt.Sprintf(`Check the details above for provider %q and confirm that you trust the provider.
|
|
Only 'yes' will be accepted to confirm.`, lock.Provider().Type),
|
|
})
|
|
if err != nil {
|
|
return diags.Append(fmt.Errorf("Failed to approve use of state storage provider: %s", err))
|
|
}
|
|
if v != "yes" {
|
|
return diags.Append(
|
|
fmt.Errorf("State store provider %q (%s) was not approved, so init cannot continue.",
|
|
lock.Provider().Type,
|
|
lock.Provider(),
|
|
),
|
|
)
|
|
}
|
|
return diags
|
|
}
|