| // 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 |
| } |