github.com/opentofu/opentofu@v1.7.1/internal/command/state_replace_provider.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package command 7 8 import ( 9 "fmt" 10 "strings" 11 12 "github.com/mitchellh/cli" 13 14 "github.com/opentofu/opentofu/internal/addrs" 15 "github.com/opentofu/opentofu/internal/command/arguments" 16 "github.com/opentofu/opentofu/internal/command/clistate" 17 "github.com/opentofu/opentofu/internal/command/views" 18 "github.com/opentofu/opentofu/internal/states" 19 "github.com/opentofu/opentofu/internal/tfdiags" 20 "github.com/opentofu/opentofu/internal/tofu" 21 ) 22 23 // StateReplaceProviderCommand is a Command implementation that allows users 24 // to change the provider associated with existing resources. This is only 25 // likely to be useful if a provider is forked or changes its fully-qualified 26 // name. 27 28 type StateReplaceProviderCommand struct { 29 StateMeta 30 } 31 32 func (c *StateReplaceProviderCommand) Run(args []string) int { 33 args = c.Meta.process(args) 34 35 var autoApprove bool 36 cmdFlags := c.Meta.ignoreRemoteVersionFlagSet("state replace-provider") 37 cmdFlags.BoolVar(&autoApprove, "auto-approve", false, "skip interactive approval of replacements") 38 cmdFlags.StringVar(&c.backupPath, "backup", "-", "backup") 39 cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock states") 40 cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") 41 cmdFlags.StringVar(&c.statePath, "state", "", "path") 42 if err := cmdFlags.Parse(args); err != nil { 43 c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) 44 return cli.RunResultHelp 45 } 46 args = cmdFlags.Args() 47 if len(args) != 2 { 48 c.Ui.Error("Exactly two arguments expected.\n") 49 return cli.RunResultHelp 50 } 51 52 if diags := c.Meta.checkRequiredVersion(); diags != nil { 53 c.showDiagnostics(diags) 54 return 1 55 } 56 57 var diags tfdiags.Diagnostics 58 59 // Parse from/to arguments into providers 60 from, fromDiags := addrs.ParseProviderSourceString(args[0]) 61 if fromDiags.HasErrors() { 62 diags = diags.Append(tfdiags.Sourceless( 63 tfdiags.Error, 64 fmt.Sprintf(`Invalid "from" provider %q`, args[0]), 65 fromDiags.Err().Error(), 66 )) 67 } 68 to, toDiags := addrs.ParseProviderSourceString(args[1]) 69 if toDiags.HasErrors() { 70 diags = diags.Append(tfdiags.Sourceless( 71 tfdiags.Error, 72 fmt.Sprintf(`Invalid "to" provider %q`, args[1]), 73 toDiags.Err().Error(), 74 )) 75 } 76 if diags.HasErrors() { 77 c.showDiagnostics(diags) 78 return 1 79 } 80 81 // Load the encryption configuration 82 enc, encDiags := c.Encryption() 83 diags = diags.Append(encDiags) 84 if encDiags.HasErrors() { 85 c.showDiagnostics(diags) 86 return 1 87 } 88 89 // Initialize the state manager as configured 90 stateMgr, err := c.State(enc) 91 if err != nil { 92 c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) 93 return 1 94 } 95 96 // Acquire lock if requested 97 if c.stateLock { 98 stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View)) 99 if diags := stateLocker.Lock(stateMgr, "state-replace-provider"); diags.HasErrors() { 100 c.showDiagnostics(diags) 101 return 1 102 } 103 defer func() { 104 if diags := stateLocker.Unlock(); diags.HasErrors() { 105 c.showDiagnostics(diags) 106 } 107 }() 108 } 109 110 // Refresh and load state 111 if err := stateMgr.RefreshState(); err != nil { 112 c.Ui.Error(fmt.Sprintf("Failed to refresh source state: %s", err)) 113 return 1 114 } 115 116 state := stateMgr.State() 117 if state == nil { 118 c.Ui.Error(errStateNotFound) 119 return 1 120 } 121 122 // Fetch all resources from the state 123 resources, diags := c.lookupAllResources(state) 124 if diags.HasErrors() { 125 c.showDiagnostics(diags) 126 return 1 127 } 128 129 var willReplace []*states.Resource 130 131 // Update all matching resources with new provider 132 for _, resource := range resources { 133 if resource.ProviderConfig.Provider.Equals(from) { 134 willReplace = append(willReplace, resource) 135 } 136 } 137 c.showDiagnostics(diags) 138 139 if len(willReplace) == 0 { 140 c.Ui.Output("No matching resources found.") 141 return 0 142 } 143 144 // Explain the changes 145 colorize := c.Colorize() 146 c.Ui.Output("OpenTofu will perform the following actions:\n") 147 c.Ui.Output(colorize.Color(" [yellow]~[reset] Updating provider:")) 148 c.Ui.Output(colorize.Color(fmt.Sprintf(" [red]-[reset] %s", from))) 149 c.Ui.Output(colorize.Color(fmt.Sprintf(" [green]+[reset] %s\n", to))) 150 151 c.Ui.Output(colorize.Color(fmt.Sprintf("[bold]Changing[reset] %d resources:\n", len(willReplace)))) 152 for _, resource := range willReplace { 153 c.Ui.Output(colorize.Color(fmt.Sprintf(" %s", resource.Addr))) 154 } 155 156 // Confirm 157 if !autoApprove { 158 c.Ui.Output(colorize.Color( 159 "\n[bold]Do you want to make these changes?[reset]\n" + 160 "Only 'yes' will be accepted to continue.\n", 161 )) 162 v, err := c.Ui.Ask("Enter a value:") 163 if err != nil { 164 c.Ui.Error(fmt.Sprintf("Error asking for approval: %s", err)) 165 return 1 166 } 167 if v != "yes" { 168 c.Ui.Output("Cancelled replacing providers.") 169 return 0 170 } 171 } 172 173 // Update the provider for each resource 174 for _, resource := range willReplace { 175 resource.ProviderConfig.Provider = to 176 } 177 178 b, backendDiags := c.Backend(nil, enc.State()) 179 diags = diags.Append(backendDiags) 180 if backendDiags.HasErrors() { 181 c.showDiagnostics(diags) 182 return 1 183 } 184 185 // Get schemas, if possible, before writing state 186 var schemas *tofu.Schemas 187 if isCloudMode(b) { 188 var schemaDiags tfdiags.Diagnostics 189 schemas, schemaDiags = c.MaybeGetSchemas(state, nil) 190 diags = diags.Append(schemaDiags) 191 } 192 193 // Write the updated state 194 if err := stateMgr.WriteState(state); err != nil { 195 c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) 196 return 1 197 } 198 if err := stateMgr.PersistState(schemas); err != nil { 199 c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) 200 return 1 201 } 202 203 c.showDiagnostics(diags) 204 c.Ui.Output(fmt.Sprintf("\nSuccessfully replaced provider for %d resources.", len(willReplace))) 205 return 0 206 } 207 208 func (c *StateReplaceProviderCommand) Help() string { 209 helpText := ` 210 Usage: tofu [global options] state replace-provider [options] FROM_PROVIDER_FQN TO_PROVIDER_FQN 211 212 Replace provider for resources in the OpenTofu state. 213 214 Options: 215 216 -auto-approve Skip interactive approval. 217 218 -lock=false Don't hold a state lock during the operation. This is 219 dangerous if others might concurrently run commands 220 against the same workspace. 221 222 -lock-timeout=0s Duration to retry a state lock. 223 224 -ignore-remote-version A rare option used for the remote backend only. See 225 the remote backend documentation for more information. 226 227 -state, state-out, and -backup are legacy options supported for the local 228 backend only. For more information, see the local backend's documentation. 229 230 ` 231 return strings.TrimSpace(helpText) 232 } 233 234 func (c *StateReplaceProviderCommand) Synopsis() string { 235 return "Replace provider in the state" 236 }