Files
opentf/internal/command/meta_backend.go
2026-01-26 17:28:55 +02:00

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("{}"),
}
}
},
}
}