| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package stackmigrate | |
| import ( | |
| "fmt" | |
| "os" | |
| "path/filepath" | |
| "github.com/zclconf/go-cty/cty" | |
| "github.com/hashicorp/terraform-svchost/disco" | |
| "github.com/hashicorp/terraform/internal/backend" | |
| backendInit "github.com/hashicorp/terraform/internal/backend/init" | |
| "github.com/hashicorp/terraform/internal/backend/local" | |
| "github.com/hashicorp/terraform/internal/backend/remote" | |
| "github.com/hashicorp/terraform/internal/command/clistate" | |
| "github.com/hashicorp/terraform/internal/command/workdir" | |
| "github.com/hashicorp/terraform/internal/states" | |
| "github.com/hashicorp/terraform/internal/states/statemgr" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| type Loader struct { | |
| Discovery *disco.Disco | |
| } | |
| var ( | |
| WorkspaceNameEnvVar = "TF_WORKSPACE" | |
| ) | |
| // LoadState loads a state from the given configPath. The configuration at configPath | |
| // must have been initialized via `terraform init` before calling this function. | |
| // The function returns an empty state even if there are errors. | |
| func (l *Loader) LoadState(configPath string) (*states.State, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| state := states.NewState() | |
| workingDirectory, workspace, err := l.loadWorkingDir(configPath) | |
| if err != nil { | |
| return state, diags.Append(fmt.Errorf("error loading working directory: %s", err)) | |
| } | |
| backendInit.Init(l.Discovery) | |
| // First, we'll load the "backend state". This should have been initialised | |
| // by the `terraform init` command, and contains the configuration for the | |
| // backend that we're using. | |
| var backendState *workdir.BackendStateFile | |
| backendStatePath := filepath.Join(workingDirectory.DataDir(), ".terraform.tfstate") | |
| st := &clistate.LocalState{Path: backendStatePath} | |
| // If the backend state file is not provided, RefreshState will | |
| // return nil error and State will be empty. | |
| // In this case, we assume that we're using a local backend. | |
| if err := st.RefreshState(); err != nil { | |
| diags = diags.Append(fmt.Errorf("error loading backend state: %s", err)) | |
| return state, diags | |
| } | |
| backendState = st.State() | |
| // Now that we have the backend state, we can initialise the backend itself | |
| // based on what we had from the `terraform init` command. | |
| var backend backend.Backend | |
| var backendConfig cty.Value | |
| // the absence of backend state file indicates a local backend | |
| if backendState == nil { | |
| backend = local.New() | |
| backendConfig = cty.ObjectVal(map[string]cty.Value{ | |
| "path": cty.StringVal(fmt.Sprintf("%s/%s", configPath, "terraform.tfstate")), | |
| "workspace_dir": cty.StringVal(configPath), | |
| }) | |
| } else { | |
| initFn := backendInit.Backend(backendState.Backend.Type) | |
| if initFn == nil { | |
| diags = diags.Append(fmt.Errorf("unknown backend type %q", backendState.Backend.Type)) | |
| return state, diags | |
| } | |
| backend = initFn() | |
| schema := backend.ConfigSchema() | |
| config, err := backendState.Backend.Config(schema) | |
| if err != nil { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Failed to decode current backend config", | |
| fmt.Sprintf("The backend configuration created by the most recent run of \"terraform init\" could not be decoded: %s. The configuration may have been initialized by an earlier version that used an incompatible configuration structure. Run \"terraform init -reconfigure\" to force re-initialization of the backend.", err), | |
| )) | |
| return state, diags | |
| } | |
| var moreDiags tfdiags.Diagnostics | |
| backendConfig, moreDiags = backend.PrepareConfig(config) | |
| diags = diags.Append(moreDiags) | |
| if moreDiags.HasErrors() { | |
| return state, diags | |
| } | |
| // it's safe to ignore terraform version conflict between the local and remote environments, | |
| // as we are only reading the state | |
| if backendR, ok := backend.(*remote.Remote); ok { | |
| backendR.IgnoreVersionConflict() | |
| } | |
| } | |
| // Now that we have the backend and its configuration, we can configure it. | |
| moreDiags := backend.Configure(backendConfig) | |
| diags = diags.Append(moreDiags) | |
| if moreDiags.HasErrors() { | |
| return state, diags | |
| } | |
| // The backend is initialised and configured, so now we can load the state | |
| // from the backend. | |
| stateManager, sDiags := backend.StateMgr(workspace) | |
| if sDiags.HasErrors() { | |
| diags = diags.Append(fmt.Errorf("error loading state: %s", sDiags.Err())) | |
| return state, diags | |
| } | |
| // We'll lock the backend here to ensure that we don't have any concurrent | |
| // operations on the state. If this fails, we'll return an error and the | |
| // user should retry the migration later when nothing is currently updating | |
| // the state. | |
| id, err := stateManager.Lock(statemgr.NewLockInfo()) | |
| if err != nil { | |
| diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Failed to lock state", fmt.Sprintf("The state is currently locked by another operation: %s. Please retry the migration later.", err))) | |
| return state, diags | |
| } | |
| // Remember to unlock the state when we're done. | |
| defer func() { | |
| // Remember to unlock the state when we're done. | |
| if err := stateManager.Unlock(id); err != nil { | |
| // If we couldn't unlock the state, we'll warn about that but the | |
| // migration can actually continue. | |
| diags = diags.Append(tfdiags.Sourceless(tfdiags.Warning, "Failed to unlock state", fmt.Sprintf("The state was successfully loaded but could not be unlocked: %s. The migration can continue but the state many need to be unlocked manually.", err))) | |
| } | |
| }() | |
| if err := stateManager.RefreshState(); err != nil { | |
| diags = diags.Append(fmt.Errorf("error loading state: %s", err)) | |
| return state, diags | |
| } | |
| state = stateManager.State() | |
| return state, diags | |
| } | |
| func (l *Loader) loadWorkingDir(configPath string) (*workdir.Dir, string, error) { | |
| // load the state specified by this configuration | |
| workingDirectory := workdir.NewDir(configPath) | |
| if data := os.Getenv("TF_DATA_DIR"); len(data) > 0 { | |
| workingDirectory.OverrideDataDir(data) | |
| } | |
| meta := Meta{WorkingDir: workingDirectory} | |
| // Load the currently active workspace from the environment, defaulting | |
| // to the default workspace if not set. | |
| workspace, err := meta.Workspace() | |
| if err != nil { | |
| return nil, "", fmt.Errorf("failed to load workspace: %s", err) | |
| } | |
| return workingDirectory, workspace, nil | |
| } |