blob: 9e4fce915423e921d36a4d36fd8de1f410d2b2f6 [file]
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/clistate"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/command/workdir"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
"github.com/hashicorp/terraform/internal/providercache"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// StateMigrateCommand is a Command implementation that migrates
// the state file from one location to another
type StateMigrateCommand struct {
Meta
// incompleteProviders is necessary here to coordinate separate
// provider installation and lock file update processes.
incompleteProviders []string
}
func (c *StateMigrateCommand) Run(rawArgs []string) int {
// Parse and apply global view arguments
common, rawArgs := arguments.ParseView(rawArgs)
c.Meta.View.Configure(common)
args, diags := arguments.ParseStateMigrate(rawArgs)
view := views.NewStateMigrate(args.ViewType, c.View)
if diags.HasErrors() {
view.Diagnostics(diags)
return 1
}
// FIXME: the -input flag value is needed but there is no clear path to pass
// this value down, so we continue to mutate the Meta object state for now.
c.Meta.input = args.InputEnabled
// Command can be aborted by interruption signals
ctx, done := c.InterruptibleContext(c.CommandContext())
defer done()
// return validation errors early if there are any
if diags.HasErrors() {
view.Diagnostics(diags)
return 1
}
c.Meta.includeStateMigrateFiles = true
dir := c.Meta.WorkingDir.RootModuleDir()
cfg, mDiags := c.Meta.loadConfig(dir)
if mDiags.HasErrors() {
diags = diags.Append(mDiags)
view.Diagnostics(diags)
return 1
}
if cfg.Module.StateStore != nil {
cfg.Module.StateStore.ProviderSupplyMode = c.getProviderSupplyModeForStateStore(cfg.Module)
}
smi := cfg.Module.StateMigrationInstructions
if smi == nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"No state migration instructions found",
"No instructions were found in the configuration files. Please ensure that a file with a .tfmigrate.hcl extension is present and contains valid state migration instructions.",
))
view.Diagnostics(diags)
return 1
}
// When configuring the source and destination backends,
// we prepare options for the migration action.
//
// If input is disabled, we "force" the migration. This stops the migration logic shared
// with the init command failing when it discovers that input is disabled.
// In "terraform init" the force field is controlled by the -force-copy flag.
// In contrast, "terraform state migrate" assumes that if input is disabled the user wants to force the migration.
migrateOpts := &backendMigrateOpts{
ViewType: args.ViewType,
force: args.ForceCopy,
}
c.Meta.forceInitCopy = args.ForceCopy // backendMigrateOpts.force is overwritten with this value, so we also need to set it.
// Load the source backend
var source string
var sourceLock *depsfile.Locks // This should only contain a single lock, if non nil. Used to avoid re-download if destination provider is the same.
if smi.Backend != nil {
source = fmt.Sprintf("backend %q", smi.Backend.Type)
srcB, _, srcDiags := c.Meta.backendInitFromConfig(smi.Backend)
diags = diags.Append(srcDiags)
if !diags.HasErrors() {
migrateOpts.SourceType = smi.Backend.Type
migrateOpts.Source = srcB
}
} else if smi.StateStore != nil {
source = fmt.Sprintf("state store %q (%s)", smi.StateStore.Type,
smi.StateStore.ProviderAddr.ForDisplay())
// Load any pre-existing source provider lock file.
var lockfilePath string
if args.SourceLockFilePath != "" {
lockfilePath = args.SourceLockFilePath
} else {
lockfilePath = dependencyLockFilename // default
}
srcLocks, srcLockDiags := c.readLockedDependenciesFromPath(lockfilePath)
diags = diags.Append(srcLockDiags)
if srcLockDiags.HasErrors() {
view.Diagnostics(diags)
return 1
}
upgrade := false // The first provider download step will never be an upgrade. Either it's constrained by a preexisting lock or there is no lock.
var srcProviderDiags tfdiags.Diagnostics
var output bool
output, sourceLock, srcProviderDiags = c.getSingleProvider(ctx, smi.StateStore.Type, smi.StateStoreProvider.Requirement, srcLocks, upgrade, MigrationSource, view)
diags = diags.Append(srcProviderDiags)
if srcProviderDiags.HasErrors() {
view.Diagnostics(diags)
return 1
}
if output {
// Space out provider download output from the migration output below.
view.Spacer()
}
srcB, _, _, srcDiags := c.Meta.stateStoreInitFromConfig(smi.StateStore, sourceLock)
diags = diags.Append(srcDiags)
if !diags.HasErrors() {
migrateOpts.SourceType = smi.StateStore.Type
migrateOpts.Source = srcB
}
}
// Load the destination backend
rootMod := cfg.Module
var destination string
var destinationLock *depsfile.Locks // This should only contain a single lock, if non nil. Used to update the dependency lock file on disk.
var bsf *workdir.BackendStateFile = workdir.NewBackendStateFile() // Collect data below for updating the backend state file.
if rootMod.Backend != nil {
destination = fmt.Sprintf("backend %q", rootMod.Backend.Type)
dstB, dstConfig, dstDiags := c.Meta.backendInitFromConfig(rootMod.Backend)
diags = diags.Append(dstDiags)
if !diags.HasErrors() {
// Assign migration options for the migration action later in the command
migrateOpts.DestinationType = rootMod.Backend.Type
migrateOpts.Destination = dstB
// Capture details of the destination backend for updating the backend state file after a successful migration.
_, cHash, bcDiags := c.backendConfig(&BackendOpts{
BackendConfig: rootMod.Backend,
})
diags = diags.Append(bcDiags)
if bcDiags.HasErrors() {
view.Diagnostics(diags)
return 1
}
bsf.Backend = &workdir.BackendConfigState{
Type: rootMod.Backend.Type,
Hash: uint64(cHash),
}
err := bsf.Backend.SetConfig(dstConfig, dstB.ConfigSchema())
if err != nil {
diags = diags.Append(fmt.Errorf("Can't serialize backend configuration as JSON: %s", err))
view.Diagnostics(diags)
return 1
}
}
} else if rootMod.StateStore != nil {
destination = fmt.Sprintf("state store %q (%s)", rootMod.StateStore.Type,
rootMod.StateStore.ProviderAddr.ForDisplay())
// Get single required_providers entry for state store provider.
dstReq, dstReqDiags := c.getDestinationStateStoreProviderRequirements(rootMod.StateStore.ProviderAddr, rootMod.ProviderRequirements)
diags = diags.Append(dstReqDiags)
if dstReqDiags.HasErrors() {
view.Diagnostics(diags)
return 1
}
// Load any pre-existing destination provider lock file.
var lockfilePath string
if args.DestinationLockFilePath != "" {
lockfilePath = args.DestinationLockFilePath
} else {
lockfilePath = dependencyLockFilename // default
}
dstLocks, dstLockDiags := c.readLockedDependenciesFromPath(lockfilePath)
diags = diags.Append(dstLockDiags)
if dstLockDiags.HasErrors() {
view.Diagnostics(diags)
return 1
}
// The source provider download step may have introduced a new lock that can be re-used here.
// Else, this download step could re-download the same provider if the migration is between stores
// in the same provider.
//
// TODO: Make this conditional based on whether we're doing an upgrade or not?
// Or is use of upgrade flag in second download sufficient?
var mergedLocks *depsfile.Locks
if sourceLock != nil {
mergedLocks = c.mergeLockedDependencies(dstLocks, sourceLock)
} else {
mergedLocks = dstLocks
}
// Perform download of the destination provider.
// This may be controlled by a pre-existing lock from above or not, therefore the returned
// lock for the destination state store may not already be in the lock file.
//
// We only pass in a single required provider, so we expect a single lock to be
// returned. This will be added the dependency lock file after a successful migration.
upgrade := false // TODO - control this by -upgrade flag
var dstProviderDiags tfdiags.Diagnostics
var output bool
output, destinationLock, dstProviderDiags = c.getSingleProvider(ctx, rootMod.StateStore.Type, dstReq, mergedLocks, upgrade, MigrationDestination, view)
diags = diags.Append(dstProviderDiags)
if dstProviderDiags.HasErrors() {
view.Diagnostics(diags)
return 1
}
if output {
// Space out provider download output from the migration output below.
view.Spacer()
}
dstB, stateStoreConfigVal, providerConfigVal, dstDiags := c.Meta.stateStoreInitFromConfig(rootMod.StateStore, destinationLock)
diags = diags.Append(dstDiags)
if !diags.HasErrors() {
migrateOpts.DestinationType = rootMod.StateStore.Type
migrateOpts.Destination = dstB
}
// Capture details of the destination state store for updating the backend state file after a successful migration.
_, cHash, sscDiags := c.stateStoreConfig(&BackendOpts{
StateStoreConfig: rootMod.StateStore,
Locks: destinationLock,
})
diags = diags.Append(sscDiags)
if sscDiags.HasErrors() {
view.Diagnostics(diags)
return 1
}
v := destinationLock.Provider(rootMod.StateStore.ProviderAddr).Version() // We just downloaded this provider, so the lock wil be present.
version, err := providerreqs.GoVersionFromVersion(v)
if err != nil {
diags = diags.Append(fmt.Errorf("Failed to convert provider version to Go version: %s", err))
view.Diagnostics(diags)
return 1
}
bsf.StateStore = &workdir.StateStoreConfigState{
Type: rootMod.StateStore.Type,
Hash: uint64(cHash),
Provider: &workdir.ProviderConfigState{
Source: &rootMod.StateStore.ProviderAddr,
Version: version,
},
ProviderSupplyMode: rootMod.StateStore.ProviderSupplyMode,
}
err = bsf.StateStore.SetConfig(stateStoreConfigVal, dstB.ConfigSchema())
if err != nil {
diags = diags.Append(fmt.Errorf("Failed to set state store configuration: %w", err))
view.Diagnostics(diags)
return 1
}
err = bsf.StateStore.Provider.SetConfig(providerConfigVal, dstB.ProviderSchema())
if err != nil {
diags = diags.Append(fmt.Errorf("Failed to set state store provider configuration: %w", err))
view.Diagnostics(diags)
return 1
}
} else {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unknown migration destination",
"No configuration was provided for where to migrate the state to. Please ensure that a file with a .tf extension is present and contains valid state_store or backend configuration inside the terraform block.",
))
}
// present all errors from above together so user can fix them all at once
if diags.HasErrors() {
view.Diagnostics(diags)
return 1
}
view.Log(views.StateMigrationStartMessage, source, destination)
// Perform the migration from source to destination
err := c.Meta.backendMigrateState(migrateOpts)
if err != nil {
diags = diags.Append(fmt.Errorf("State migration failed: %w", err))
view.Diagnostics(diags)
view.Log(views.StateMigrationFailureMessage, source, destination)
return 1
}
// After a successful migration to a state store, we must make sure the dependency lock file contains the
// details of the destination state store provider.
if rootMod.StateStore != nil {
originalLocks, originalLockDiags := c.lockedDependencies()
diags = diags.Append(originalLockDiags)
if originalLockDiags.HasErrors() {
view.Diagnostics(diags)
return 1
}
// Get the combination of locks
//
// Take the lock from the destination provider download and add in the original locks from the dependency lock file.
// This means the lock created from the destination provider download is authoritative (e.g. any hashes from the
// installation process are preserved)
newLocks := c.mergeLockedDependencies(destinationLock, originalLocks)
output, depLockFileDiags := c.saveDependencyLockFile(originalLocks, newLocks, view)
diags = diags.Append(depLockFileDiags)
if depLockFileDiags.HasErrors() {
view.Diagnostics(diags)
view.Log(views.StateMigrationPostStepsInterruptedMessage, source, destination)
return 1
}
if output {
// Space out lock file creation/update output from the final outputs below.
view.Spacer()
}
}
// Success, so update backend state file to reflect the new location state is stored in.
bsfDiags := c.updateBackendStateFile(bsf)
diags = diags.Append(bsfDiags)
if bsfDiags.HasErrors() {
view.Diagnostics(diags)
view.Log(views.StateMigrationPostStepsInterruptedMessage, source, destination)
return 1
}
view.Diagnostics(diags) // Log any warnings
view.Log(views.StateMigrationCompletedMessage, source, destination)
return 0
}
func (c *StateMigrateCommand) Help() string {
helpText := `
Usage: terraform [global options] state migrate [options]
Migrate state from source declared in the migration configuration (*.tfmigrate.hcl)
to the destination declared in the root module (*.tf).
An error will be returned if the migration fails, e.g. if the state
is inaccessible or the migration configuration is invalid.
Options:
-source-provider-lock-file Path to a provider lock file for the source provider (requires -input=false).
Defaults to using the working directory's .terraform.lock.hcl file.
-destination-provider-lock-file Path to a provider lock file for the destination provider (requires -input=false).
Defaults to using the working directory's .terraform.lock.hcl file.
-upgrade Trigger upgrade of the provider used for state storage.
-input=true Enable input for interactive prompts (defaults to true, set to false in automation).
`
return strings.TrimSpace(helpText)
}
func (c *StateMigrateCommand) Synopsis() string {
return "Migrate the state from one location to another"
}
const (
MigrationSource = "source"
MigrationDestination = "destination"
)
func (c *StateMigrateCommand) updateBackendStateFile(s *workdir.BackendStateFile) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
statePath := filepath.Join(c.DataDir(), DefaultStateFilename)
sMgr := &clistate.LocalState{Path: statePath}
if err := sMgr.RefreshState(); err != nil {
diags = diags.Append(fmt.Errorf("Failed to load the backend state file when preparing to update it: %s", err))
return diags
}
if err := sMgr.WriteState(s); err != nil {
diags = diags.Append(errBackendWriteSavedDiag(err))
return diags
}
if err := sMgr.PersistState(); err != nil {
diags = diags.Append(errBackendWriteSavedDiag(err))
return diags
}
return diags
}
func (c *StateMigrateCommand) getDestinationStateStoreProviderRequirements(provider addrs.Provider, configReqs *configs.RequiredProviders) (providerreqs.Requirements, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
req := make(providerreqs.Requirements, 1)
if configReqs == nil {
panic(fmt.Sprintf("expected one provider requirement for the destination state store provider %q, but received empty data about required providers.", provider))
}
for _, providerReq := range configReqs.RequiredProviders {
if providerReq.Type.Equals(provider) {
con, err := providerreqs.ParseVersionConstraints(providerReq.Requirement.Required.String())
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid version constraint syntax for state store provider",
// The errors returned by ParseVersionConstraint already include
// the section of input that was incorrect, so we don't need to
// include that here.
Detail: fmt.Sprintf("Incorrect version constraint syntax: %s.", err.Error()),
Subject: providerReq.Requirement.DeclRange.Ptr(),
})
}
req[providerReq.Type] = con
}
}
if len(req) != 1 {
panic(fmt.Sprintf("expected exactly one provider requirement for the destination state store provider %q, got %d", provider, len(req)))
}
return req, diags
}
// saveDependencyLockFile overwrites the contents of the dependency lock file.
func (c *StateMigrateCommand) saveDependencyLockFile(previousLocks, newLocks *depsfile.Locks, view views.StateMigrate) (output bool, diags tfdiags.Diagnostics) {
// The state migrate command does not support the -lockfile=readonly flag
// This flag is specific to the init command, and can only take "" or "readonly" as values.
// As state migrate doesn't take this flag, we can safely set it to "" here.
flagLockfile := ""
return c.Meta.saveDependencyLockFile(previousLocks, newLocks, c.incompleteProviders, flagLockfile, view)
}
// getSingleProvider is used to download the source and/or destination state store providers during a state migration.
// Download of the up to 2 providers is kept separate due to:
// - Potential for downloading different versions of the same provider
// - Need to keep the locks separate for source and destination providers; destination providers are added to the dependency lock file.
func (c *StateMigrateCommand) getSingleProvider(ctx context.Context, storeName string, reqs providerreqs.Requirements, locks *depsfile.Locks, upgrade bool, location string, view views.StateMigrate) (output bool, resultingLock *depsfile.Locks, diags tfdiags.Diagnostics) {
ctx, span := tracer.Start(ctx, "install state migration "+location+" provider")
defer span.End()
// We expect to download only one provider
if len(reqs) != 1 {
panic(fmt.Sprintf("expected exactly one provider requirement for the destination state store provider, got %d", len(reqs)))
}
// Check for legacy provider addresses.
for providerAddr := range reqs {
if providerAddr.IsLegacy() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid legacy provider address",
fmt.Sprintf(
"This configuration or its associated state refers to the unqualified provider %q.\n\nYou must complete the Terraform 0.13 upgrade process before upgrading to later versions.",
providerAddr.Type,
),
))
}
}
if diags.HasErrors() {
return false, nil, diags
}
// Use a source that looks for providers in all of the standard locations,
// possibly customized by the user in CLI config.
inst := c.providerInstaller()
// Because we're currently just streaming a series of events sequentially
// into the terminal, we're showing only a subset of the events to keep
// things relatively concise. Later it'd be nice to have a progress UI
// where statuses update in-place, but we can't do that as long as we
// are shimming our vt100 output to the legacy console API on Windows.
evts := &providercache.InstallerEvents{
PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) {
view.LogInitializingStateStoreProviderPlugin(storeName)
},
ProviderAlreadyInstalled: providerAlreadyInstalledCallback(view),
BuiltInProviderAvailable: builtInProviderAvailableCallback(view),
BuiltInProviderFailure: builtInProviderFailureCallback(&diags),
QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) {
if locked {
view.LogReusingPreviousProviderVersion(provider)
} else {
if len(versionConstraints) > 0 {
view.LogFindingMatchingVersion(provider, versionConstraints)
} else {
view.LogFindingLatestVersion(provider)
}
}
},
LinkFromCacheBegin: linkFromCacheBeginCallback(view),
FetchPackageBegin: fetchPackageBeginCallback(view),
QueryPackagesFailure: queryPackagesFailureCallback(&diags, ctx, inst.ProviderSource(), reqs, nil),
QueryPackagesWarning: queryPackagesWarningCallback(&diags),
LinkFromCacheFailure: linkFromCacheFailureCallback(&diags),
FetchPackageFailure: fetchPackageFailureCallback(&diags, reqs),
FetchPackageSuccess: fetchPackageSuccessCallback(view),
ProvidersLockUpdated: providersLockUpdatedCallback(&c.incompleteProviders),
ProvidersFetched: providersFetchedCallback(view),
}
ctx = evts.OnContext(ctx)
mode := providercache.InstallNewProvidersOnly
if upgrade {
mode = providercache.InstallUpgrades
}
newLocks, err := inst.EnsureProviderVersions(ctx, locks, reqs, mode)
if ctx.Err() == context.Canceled {
diags = diags.Append(fmt.Errorf("Provider installation was canceled by an interrupt signal."))
return true, nil, diags
}
if err != nil {
// The errors captured in "err" should be redundant with what we
// received via the InstallerEvents callbacks above, so we'll
// just return those as long as we have some.
if !diags.HasErrors() {
diags = diags.Append(err)
}
return true, nil, diags
}
return true, newLocks, diags
}