/
githubmirror
/
terraform
Обзор
Документация
Войти
/
githubmirror
/
terraform
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
internal/command/state_migrate.go
624 строки
24 KB
Sarah French
refactor: Rename const to DuringWorkDirStateUpdate
11 авг 2026, 19:12
11 авг 2026, 19:12
d32a084
Код
Авторство
О чём код?
// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package command import ( "context" "fmt" "log" "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 view.LogMigrationSourceInitializationStart() 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 { // 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 source 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 trust ProviderTrust var stateStoreProviderAuthResult *getproviders.PackageAuthenticationResult var output bool output, sourceLock, trust, stateStoreProviderAuthResult, srcProviderDiags = c.getSingleProvider(ctx, smi.StateStore, 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() } // The provider needs to be trusted to use it immediately after download. If the provider is not yet trusted we either prompt or raise an error. trustDiags := c.confirmProviderIsTrusted(trust, smi.StateStore.ProviderAddr, stateStoreProviderAuthResult, sourceLock, srcLocks, args.SourceLockFilePath, c, view) diags = diags.Append(trustDiags) if trustDiags.HasErrors() { view.Diagnostics(diags) return 1 } pLock := sourceLock.Provider(smi.StateStore.ProviderAddr) source = fmt.Sprintf("state store %q (%s %s)", smi.StateStore.Type, smi.StateStore.ProviderAddr.ForDisplay(), pLock.Version()) srcB, _, _, srcDiags := c.Meta.stateStoreInitFromConfig(smi.StateStore, sourceLock) diags = diags.Append(srcDiags) if !diags.HasErrors() { migrateOpts.SourceType = smi.StateStore.Type migrateOpts.Source = srcB } } view.LogMigrationSourceInitializationComplete() // Load the destination backend view.LogMigrationDestinationInitializationStart() 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 { // 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 := args.Upgrade var dstProviderDiags tfdiags.Diagnostics var trust ProviderTrust var stateStoreProviderAuthResult *getproviders.PackageAuthenticationResult var output bool output, destinationLock, trust, stateStoreProviderAuthResult, dstProviderDiags = c.getSingleProvider(ctx, rootMod.StateStore, 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() } // The provider needs to be trusted to use it immediately after download. If the provider is not yet trusted we either prompt or raise an error. trustDiags := c.confirmProviderIsTrusted(trust, rootMod.StateStore.ProviderAddr, stateStoreProviderAuthResult, destinationLock, mergedLocks, args.DestinationLockFilePath, c, view) diags = diags.Append(trustDiags) if trustDiags.HasErrors() { view.Diagnostics(diags) return 1 } pLock := destinationLock.Provider(rootMod.StateStore.ProviderAddr) destination = fmt.Sprintf("state store %q (%s %s)", rootMod.StateStore.Type, rootMod.StateStore.ProviderAddr.ForDisplay(), pLock.Version()) 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.LogMigrationDestinationInitializationComplete() view.LogStateMigrationStart(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.LogStateMigrationErrored(views.DuringMigration, source, destination) return 1 } view.LogStateMigrationComplete() // 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) view.LogStateMigrationErrored(views.DuringLockfile, source, destination) 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.LogStateMigrationErrored(views.DuringLockfile, 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.LogStateMigrationErrored(views.DuringWorkDirStateUpdate, source, destination) return 1 } view.Diagnostics(diags) // Log any warnings view.LogStateMigrationFinalized(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.ProviderLockingLogger) (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, stateStore *configs.StateStore, reqs providerreqs.Requirements, locks *depsfile.Locks, upgrade bool, location string, view views.StateMigrate) (output bool, resultingLock *depsfile.Locks, trust ProviderTrust, authResult *getproviders.PackageAuthenticationResult, 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, Invalid, 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() // Prepare callback functions for the installer. // These allow us to send output to the terminal as events happen, catch // diagnostics, etc. // // We use some callbacks to capture data that's surfaced during the // installation process: // - provider authentication info. // - info about what type of location a provider is sourced from. // These pieces of data are used to determine if additional security features // need to be enabled. providerLocations := make(map[addrs.Provider]getproviders.PackageLocation) var stateStoreProviderAuthResult *getproviders.PackageAuthenticationResult evts := &providercache.InstallerEvents{ PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) { pAddr := stateStore.ProviderAddr // empty address would indicate wrong configuration // such as missing or mismatching provider requirement // which will be surfaced as diagnostic during installation if !pAddr.IsZero() { cons := reqs[pAddr] view.LogInstallStateStoreProviderStart(pAddr, cons, stateStore.Type) } }, ProviderAlreadyInstalled: providerAlreadyInstalledCallback(view), BuiltInProviderAvailable: builtInProviderAvailableCallback(view), BuiltInProviderFailure: builtInProviderFailureCallback(&diags), QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) { if locked { pLock := locks.Provider(provider) view.LogReusingPreviousProviderVersion(provider, pLock.Version()) } else { if len(versionConstraints) > 0 { view.LogFindingMatchingVersion(provider, versionConstraints) } else { view.LogFindingLatestVersion(provider) } } }, LinkFromCacheBegin: linkFromCacheBeginCallback(view), FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) { // 1) Record the location of this provider. // // FetchPackageBegin is the callback hook at the start of the process of obtaining a provider that isn't yet // in the dependency lock file. Providers that are processed here will not be processed here on the next init, // as then they will be in the lock file. The same provider type would only be processed here again if the // provider version changed via an `init -upgrade` command. providerLocations[provider] = location // 2) Call the shared callback for FetchPackageBegin. cb := fetchPackageBeginCallback(view) cb(provider, version, location) }, QueryPackagesFailure: queryPackagesFailureCallback(&diags, ctx, inst.ProviderSource(), reqs, nil), QueryPackagesWarning: queryPackagesWarningCallback(&diags), LinkFromCacheFailure: linkFromCacheFailureCallback(&diags), FetchPackageFailure: fetchPackageFailureCallback(&diags, reqs), FetchPackageSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) { // 1. Capture auth result if this provider is used for state storage. if stateStore != nil && provider.Equals(stateStore.ProviderAddr) { log.Printf("[TRACE] getProvidersFromConfig: state storage provider %s (%q) auth result: %q", stateStore.ProviderAddr.Type, stateStore.ProviderAddr.ForDisplay(), stateStoreProviderAuthResult.String()) stateStoreProviderAuthResult = authResult } // 2. Call the shared callback for FetchPackageSuccess cb := fetchPackageSuccessCallback(view) cb(provider, version, localDir, authResult) }, 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, Invalid, 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, Invalid, nil, diags } // Return advice to the calling code about what to do regarding safe state store provider installation trust = c.determineIfProviderTrusted(stateStore.ProviderAddr, providerLocations, locks) return true, newLocks, trust, stateStoreProviderAuthResult, diags }