mirror of
https://github.com/hashicorp/terraform.git
synced 2026-02-19 02:39:17 -05:00
* PSS: Ensure experimental backend codepath is isolated + gated correctly * Update init_run.go Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com> --------- Co-authored-by: Sarah French <15078782+SarahFrench@users.noreply.github.com>
332 lines
10 KiB
Go
332 lines
10 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package command
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"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/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
|
|
|
|
varArgs := initArgs.Vars.All()
|
|
items := make([]arguments.FlagNameValue, len(varArgs))
|
|
for i := range varArgs {
|
|
items[i].Name = varArgs[i].Name
|
|
items[i].Value = varArgs[i].Value
|
|
}
|
|
c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items}
|
|
|
|
// 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
|
|
}
|
|
|
|
var back backend.Backend
|
|
|
|
// There may be config errors or backend init errors but these will be shown later _after_
|
|
// checking for core version requirement errors.
|
|
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.BackendConfig, initArgs.ViewType, view)
|
|
default:
|
|
// load the previously-stored backend config
|
|
back, backDiags = c.Meta.backendFromState(ctx)
|
|
}
|
|
if backendOutput {
|
|
header = true
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
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, since an
|
|
// incorrect version of terraform may be producing errors for configuration
|
|
// constructs added in later versions.
|
|
|
|
// 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
|
|
}
|
|
|
|
// We've passed the core version check, now we can show errors from the
|
|
// configuration and backend initialisation.
|
|
|
|
// Now, we can check the diagnostics from the early configuration and the
|
|
// backend.
|
|
diags = diags.Append(earlyConfDiags)
|
|
diags = diags.Append(backDiags)
|
|
if earlyConfDiags.HasErrors() {
|
|
diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)))
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
// Now, we can show any errors from initializing the backend, but we won't
|
|
// show the InitConfigError preamble as we didn't detect problems with
|
|
// the early configuration.
|
|
if backDiags.HasErrors() {
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
// If everything is ok with the core version check and backend 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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now that we have loaded all modules, check the module tree for missing providers.
|
|
providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view)
|
|
diags = diags.Append(providerDiags)
|
|
if providersAbort || providerDiags.HasErrors() {
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
if providersOutput {
|
|
header = true
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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
|
|
}
|