mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-03-24 11:02:03 -04:00
214 lines
8.5 KiB
Go
214 lines
8.5 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package command
|
|
|
|
// This file contains all the Backend-related function calls on Meta,
|
|
// exported and private.
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
|
|
"github.com/opentofu/opentofu/internal/backend"
|
|
"github.com/opentofu/opentofu/internal/command/arguments"
|
|
backend2 "github.com/opentofu/opentofu/internal/command/backend"
|
|
"github.com/opentofu/opentofu/internal/command/clistate"
|
|
"github.com/opentofu/opentofu/internal/command/views"
|
|
"github.com/opentofu/opentofu/internal/command/workdir"
|
|
"github.com/opentofu/opentofu/internal/command/workspace"
|
|
"github.com/opentofu/opentofu/internal/configs"
|
|
"github.com/opentofu/opentofu/internal/encryption"
|
|
legacy "github.com/opentofu/opentofu/internal/legacy/tofu"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
// Operation initializes a new backend.Operation struct.
|
|
//
|
|
// This prepares the operation. After calling this, the caller is expected
|
|
// to modify fields of the operation such as Sequence to specify what will
|
|
// be called.
|
|
func (m *Meta) Operation(ctx context.Context, b backend.Backend, vt arguments.ViewOptions, enc encryption.Encryption) *backend.Operation {
|
|
schema := b.ConfigSchema()
|
|
workspace, err := m.Workspace.Workspace(ctx)
|
|
if err != nil {
|
|
// An invalid workspace error would have been raised when creating the
|
|
// backend, and the caller should have already exited. Seeing the error
|
|
// here first is a bug, so panic.
|
|
panic(fmt.Sprintf("invalid workspace: %s", err))
|
|
}
|
|
planOutBackend, err := m.backendState.ForPlan(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))
|
|
}
|
|
|
|
stateLocker := clistate.NewNoopLocker()
|
|
if m.stateLock {
|
|
view := views.NewStateLocker(vt, m.View)
|
|
stateLocker = clistate.NewLocker(m.stateLockTimeout, view)
|
|
}
|
|
|
|
depLocks, diags := m.lockedDependencies()
|
|
if diags.HasErrors() {
|
|
// We can't actually report errors from here, but m.lockedDependencies
|
|
// should always have been called earlier to prepare the "ContextOpts"
|
|
// for the backend anyway, so we should never actually get here in
|
|
// a real situation. If we do get here then the backend will inevitably
|
|
// fail downstream somewhere if it tries to use the empty depLocks.
|
|
log.Printf("[WARN] Failed to load dependency locks while preparing backend operation (ignored): %s", diags.Err().Error())
|
|
}
|
|
|
|
return &backend.Operation{
|
|
Encryption: enc,
|
|
PlanOutBackend: planOutBackend,
|
|
Targets: m.targets,
|
|
Excludes: m.excludes,
|
|
UIIn: m.UIInput(),
|
|
UIOut: m.Ui,
|
|
Workspace: workspace,
|
|
StateLocker: stateLocker,
|
|
DependencyLocks: depLocks,
|
|
}
|
|
}
|
|
|
|
// backendCLIOpts returns a backend.CLIOpts object that should be passed to
|
|
// a backend that supports local CLI operations.
|
|
func (m *Meta) backendCLIOpts(ctx context.Context) (*backend.CLIOpts, error) {
|
|
contextOpts, err := m.contextOpts(ctx)
|
|
if contextOpts == nil && err != nil {
|
|
return nil, err
|
|
}
|
|
return &backend.CLIOpts{
|
|
CLI: m.Ui,
|
|
CLIColor: m.Colorize(),
|
|
Streams: m.Streams,
|
|
StatePath: m.statePath,
|
|
StateOutPath: m.stateOutPath,
|
|
StateBackupPath: m.backupPath,
|
|
ContextOpts: contextOpts,
|
|
Input: m.Input.Input(test),
|
|
RunningInAutomation: m.RunningInAutomation,
|
|
}, err
|
|
}
|
|
|
|
func buildCliOpts(m *Meta) backend2.BackendCLIOptsBuilder {
|
|
return func(ctx context.Context, opts *backend2.BackendOpts) (cliOpts *backend.CLIOpts, diags tfdiags.Diagnostics) {
|
|
cliOpts, err := m.backendCLIOpts(ctx)
|
|
if err != nil {
|
|
if errs := providerPluginErrors(nil); errors.As(err, &errs) {
|
|
// This is a special type returned by m.providerFactories, which
|
|
// indicates one or more inconsistencies between the dependency
|
|
// lock file and the provider plugins actually available in the
|
|
// local cache directory.
|
|
//
|
|
// If initialization is allowed, we ignore this error, as it may
|
|
// be resolved by the later step where providers are fetched.
|
|
if !opts.Init {
|
|
var buf bytes.Buffer
|
|
for addr, err := range errs {
|
|
fmt.Fprintf(&buf, "\n - %s: %s", addr, err)
|
|
}
|
|
suggestion := "To download the plugins required for this configuration, run:\n tofu init"
|
|
if m.RunningInAutomation {
|
|
// Don't mention "tofu init" specifically if we're running in an automation wrapper
|
|
suggestion = "You must install the required plugins before running OpenTofu operations."
|
|
}
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Required plugins are not installed",
|
|
fmt.Sprintf(
|
|
"The installed provider plugins are not consistent with the packages selected in the dependency lock file:%s\n\nOpenTofu uses external plugins to integrate with a variety of different infrastructure services. %s",
|
|
buf.String(), suggestion,
|
|
),
|
|
))
|
|
return nil, diags
|
|
}
|
|
} else {
|
|
// All other errors just get generic handling.
|
|
diags = diags.Append(err)
|
|
return nil, diags
|
|
}
|
|
}
|
|
return cliOpts, diags
|
|
}
|
|
}
|
|
|
|
func buildBackendFlags(m *Meta) *backend2.BackendFlags {
|
|
return &backend2.BackendFlags{
|
|
// Regular dependencies which are ok as they are
|
|
Workspace: workspace.ConfiguredWorkspace(m.Workspace, m.Input, m.UIInput()),
|
|
Services: m.Services,
|
|
// NOTE: This is ok because comes from ldflags
|
|
AllowExperimentalFeatures: m.AllowExperimentalFeatures,
|
|
// TODO: this needs more investigation because I am not sure that all the functionality inside backend
|
|
// relies strictly on the rootDir = '.' config. If it does, for the moment, this works.
|
|
// The idea would be to have all the config loading capabilities in a separate component
|
|
// that most probably will depend on the -var/-var-file.
|
|
// !!!! Be careful: the guts of the loadBackendConfig contains also a call on the view where it stores
|
|
// the cb to allow the view to use the loaded source
|
|
ConfigLoader: func(ctx context.Context) (*configs.Backend, tfdiags.Diagnostics) {
|
|
return m.loadBackendConfig(ctx, ".")
|
|
},
|
|
// NOTE: this reconfigure is doing quite well here. It's only used in the backend :thinking.
|
|
// We might be able to think of this as a flag of the backend, registered and managed by the backend.
|
|
Reconfigure: m.reconfigure,
|
|
// NOTE: this the same, backend flag
|
|
ForceInitCopy: m.forceInitCopy,
|
|
// NOTE: this the same, backend flag
|
|
MigrateState: m.migrateState,
|
|
SetBackendStateCb: func(b *legacy.BackendState) {
|
|
m.backendState = b
|
|
},
|
|
InputForcefullyDisabled: test,
|
|
Input: m.Input,
|
|
Ui: m.Ui,
|
|
View: m.View,
|
|
Colorize: m.Colorize,
|
|
ShowDiagnostics: m.showDiagnostics,
|
|
UIInput: m.UIInput,
|
|
IgnoreRemoteVersion: m.ignoreRemoteVersion,
|
|
StateLock: m.stateLock,
|
|
StateLockTimeout: m.stateLockTimeout,
|
|
|
|
WorkdirFetcher: func() *workdir.Dir {
|
|
return m.WorkingDir
|
|
},
|
|
CLIOptsBuilder: buildCliOpts(m),
|
|
// TODO andrei this is ugly and should be handled separately
|
|
LegacyStateCb: func() {
|
|
// If we got here from backendFromConfig returning nil then m.backendState
|
|
// won't be set, since that codepath considers that to be no backend at all,
|
|
// but our caller considers that to be the local backend with no config
|
|
// and so we'll synthesize a backend state so other code doesn't need to
|
|
// care about this special case.
|
|
//
|
|
// FIXME: We should refactor this so that we more directly and explicitly
|
|
// treat the local backend as the default, including in the UI shown to
|
|
// 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 {
|
|
// 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 = &legacy.BackendState{
|
|
Type: "local",
|
|
ConfigRaw: json.RawMessage("{}"),
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|