| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package command | |
| import ( | |
| "fmt" | |
| "sort" | |
| "time" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/command/arguments" | |
| "github.com/hashicorp/terraform/internal/states" | |
| "github.com/hashicorp/terraform/internal/states/statemgr" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| backendLocal "github.com/hashicorp/terraform/internal/backend/local" | |
| ) | |
| // StateMeta is the meta struct that should be embedded in state subcommands. | |
| type StateMeta struct { | |
| Meta | |
| } | |
| // State returns the state for this meta. This gets the appropriate state from | |
| // the backend, but changes the way that backups are done. This configures | |
| // backups to be timestamped rather than just the original state path plus a | |
| // backup path. | |
| func (c *StateMeta) State(view arguments.ViewType) (statemgr.Full, error) { | |
| var realState statemgr.Full | |
| backupPath := c.backupPath | |
| stateOutPath := c.statePath | |
| // use the specified state | |
| if c.statePath != "" { | |
| realState = statemgr.NewFilesystem(c.statePath) | |
| } else { | |
| // Load the backend | |
| b, diags := c.backend(".", view) | |
| if diags.HasErrors() { | |
| return nil, diags.Err() | |
| } | |
| workspace, err := c.Workspace() | |
| if err != nil { | |
| return nil, err | |
| } | |
| // Check remote Terraform version is compatible | |
| remoteVersionDiags := c.remoteVersionCheck(b, workspace) | |
| c.showDiagnostics(remoteVersionDiags) | |
| if remoteVersionDiags.HasErrors() { | |
| return nil, fmt.Errorf("Error checking remote Terraform version") | |
| } | |
| // Get the state | |
| s, sDiags := b.StateMgr(workspace) | |
| if sDiags.HasErrors() { | |
| return nil, sDiags.Err() | |
| } | |
| // Get a local backend | |
| localRaw, backendDiags := c.Backend(&BackendOpts{ForceLocal: true}) | |
| if backendDiags.HasErrors() { | |
| // This should never fail | |
| panic(backendDiags.Err()) | |
| } | |
| localB := localRaw.(*backendLocal.Local) | |
| _, stateOutPath, _ = localB.StatePaths(workspace) | |
| if err != nil { | |
| return nil, err | |
| } | |
| realState = s | |
| } | |
| // We always backup state commands, so set the back if none was specified | |
| // (the default is "-", but some tests bypass the flag parsing). | |
| if backupPath == "-" || backupPath == "" { | |
| // Determine the backup path. stateOutPath is set to the resulting | |
| // file where state is written (cached in the case of remote state) | |
| backupPath = fmt.Sprintf( | |
| "%s.%d%s", | |
| stateOutPath, | |
| time.Now().UTC().Unix(), | |
| DefaultBackupExtension) | |
| } | |
| // If the backend is local (which it should always be, given our asserting | |
| // of it above) we can now enable backups for it. | |
| if lb, ok := realState.(*statemgr.Filesystem); ok { | |
| lb.SetBackupPath(backupPath) | |
| } | |
| return realState, nil | |
| } | |
| func (c *StateMeta) lookupResourceInstanceAddr(state *states.State, allowMissing bool, addrStr string) ([]addrs.AbsResourceInstance, tfdiags.Diagnostics) { | |
| target, diags := addrs.ParseTargetStr(addrStr) | |
| if diags.HasErrors() { | |
| return nil, diags | |
| } | |
| targetAddr := target.Subject | |
| var ret []addrs.AbsResourceInstance | |
| switch addr := targetAddr.(type) { | |
| case addrs.ModuleInstance: | |
| // Matches all instances within the indicated module and all of its | |
| // descendant modules. | |
| // found is used to identify cases where the selected module has no | |
| // resources, but one or more of its submodules does. | |
| found := false | |
| ms := state.Module(addr) | |
| if ms != nil { | |
| found = true | |
| ret = append(ret, c.collectModuleResourceInstances(ms)...) | |
| } | |
| for _, cms := range state.Modules { | |
| if !addr.Equal(cms.Addr) { | |
| if addr.IsAncestor(cms.Addr) || addr.TargetContains(cms.Addr) { | |
| found = true | |
| ret = append(ret, c.collectModuleResourceInstances(cms)...) | |
| } | |
| } | |
| } | |
| if !found && !allowMissing { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Unknown module", | |
| fmt.Sprintf(`The current state contains no module at %s. If you've just added this module to the configuration, you must run "terraform apply" first to create the module's entry in the state.`, addr), | |
| )) | |
| } | |
| case addrs.AbsResource: | |
| // Matches all instances of the specific selected resource. | |
| rs := state.Resource(addr) | |
| if rs == nil { | |
| if !allowMissing { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Unknown resource", | |
| fmt.Sprintf(`The current state contains no resource %s. If you've just added this resource to the configuration, you must run "terraform apply" first to create the resource's entry in the state.`, addr), | |
| )) | |
| } | |
| break | |
| } | |
| ret = append(ret, c.collectResourceInstances(addr.Module, rs)...) | |
| case addrs.AbsResourceInstance: | |
| is := state.ResourceInstance(addr) | |
| if is == nil { | |
| if !allowMissing { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Unknown resource instance", | |
| fmt.Sprintf(`The current state contains no resource instance %s. If you've just added its resource to the configuration or have changed the count or for_each arguments, you must run "terraform apply" first to update the resource's entry in the state.`, addr), | |
| )) | |
| } | |
| break | |
| } | |
| ret = append(ret, addr) | |
| } | |
| sort.Slice(ret, func(i, j int) bool { | |
| return ret[i].Less(ret[j]) | |
| }) | |
| return ret, diags | |
| } | |
| func (c *StateMeta) lookupSingleStateObjectAddr(state *states.State, addrStr string) (addrs.Targetable, tfdiags.Diagnostics) { | |
| target, diags := addrs.ParseTargetStr(addrStr) | |
| if diags.HasErrors() { | |
| return nil, diags | |
| } | |
| return target.Subject, diags | |
| } | |
| func (c *StateMeta) lookupResourceInstanceAddrs(state *states.State, addrStrs ...string) ([]addrs.AbsResourceInstance, tfdiags.Diagnostics) { | |
| var ret []addrs.AbsResourceInstance | |
| var diags tfdiags.Diagnostics | |
| for _, addrStr := range addrStrs { | |
| moreAddrs, moreDiags := c.lookupResourceInstanceAddr(state, false, addrStr) | |
| ret = append(ret, moreAddrs...) | |
| diags = diags.Append(moreDiags) | |
| } | |
| return ret, diags | |
| } | |
| func (c *StateMeta) lookupAllResourceInstanceAddrs(state *states.State) ([]addrs.AbsResourceInstance, tfdiags.Diagnostics) { | |
| var ret []addrs.AbsResourceInstance | |
| var diags tfdiags.Diagnostics | |
| for _, ms := range state.Modules { | |
| ret = append(ret, c.collectModuleResourceInstances(ms)...) | |
| } | |
| sort.Slice(ret, func(i, j int) bool { | |
| return ret[i].Less(ret[j]) | |
| }) | |
| return ret, diags | |
| } | |
| func (c *StateMeta) collectModuleResourceInstances(ms *states.Module) []addrs.AbsResourceInstance { | |
| var ret []addrs.AbsResourceInstance | |
| for _, rs := range ms.Resources { | |
| ret = append(ret, c.collectResourceInstances(ms.Addr, rs)...) | |
| } | |
| return ret | |
| } | |
| func (c *StateMeta) collectResourceInstances(moduleAddr addrs.ModuleInstance, rs *states.Resource) []addrs.AbsResourceInstance { | |
| var ret []addrs.AbsResourceInstance | |
| for key := range rs.Instances { | |
| ret = append(ret, rs.Addr.Instance(key)) | |
| } | |
| return ret | |
| } | |
| func (c *StateMeta) lookupAllResources(state *states.State) ([]*states.Resource, tfdiags.Diagnostics) { | |
| var ret []*states.Resource | |
| var diags tfdiags.Diagnostics | |
| for _, ms := range state.Modules { | |
| for _, resource := range ms.Resources { | |
| ret = append(ret, resource) | |
| } | |
| } | |
| return ret, diags | |
| } |