github.com/opentofu/opentofu@v1.7.1/internal/command/unlock.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 "context" 10 "fmt" 11 "strings" 12 13 "github.com/opentofu/opentofu/internal/states/statemgr" 14 15 "github.com/mitchellh/cli" 16 17 "github.com/opentofu/opentofu/internal/tfdiags" 18 "github.com/opentofu/opentofu/internal/tofu" 19 ) 20 21 // UnlockCommand is a cli.Command implementation that manually unlocks 22 // the state. 23 type UnlockCommand struct { 24 Meta 25 } 26 27 func (c *UnlockCommand) Run(args []string) int { 28 args = c.Meta.process(args) 29 var force bool 30 cmdFlags := c.Meta.defaultFlagSet("force-unlock") 31 cmdFlags.BoolVar(&force, "force", false, "force") 32 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 33 if err := cmdFlags.Parse(args); err != nil { 34 c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) 35 return 1 36 } 37 38 args = cmdFlags.Args() 39 if len(args) != 1 { 40 c.Ui.Error("Expected a single argument: LOCK_ID") 41 return cli.RunResultHelp 42 } 43 44 lockID := args[0] 45 args = args[1:] 46 47 // assume everything is initialized. The user can manually init if this is 48 // required. 49 configPath, err := modulePath(args) 50 if err != nil { 51 c.Ui.Error(err.Error()) 52 return 1 53 } 54 55 // Load the encryption configuration 56 enc, encDiags := c.EncryptionFromPath(configPath) 57 if encDiags.HasErrors() { 58 c.showDiagnostics(encDiags) 59 return 1 60 } 61 62 var diags tfdiags.Diagnostics 63 64 backendConfig, backendDiags := c.loadBackendConfig(configPath) 65 diags = diags.Append(backendDiags) 66 if diags.HasErrors() { 67 c.showDiagnostics(diags) 68 return 1 69 } 70 71 // Load the backend 72 b, backendDiags := c.Backend(&BackendOpts{ 73 Config: backendConfig, 74 }, enc.State()) 75 diags = diags.Append(backendDiags) 76 if backendDiags.HasErrors() { 77 c.showDiagnostics(diags) 78 return 1 79 } 80 81 // unlocking is read only when looking at state data 82 c.ignoreRemoteVersionConflict(b) 83 84 env, err := c.Workspace() 85 if err != nil { 86 c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) 87 return 1 88 } 89 stateMgr, err := b.StateMgr(env) 90 if err != nil { 91 c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) 92 return 1 93 } 94 95 _, isLocal := stateMgr.(*statemgr.Filesystem) 96 97 if !force { 98 // Forcing this doesn't do anything, but doesn't break anything either, 99 // and allows us to run the basic command test too. 100 if isLocal { 101 c.Ui.Error("Local state cannot be unlocked by another process") 102 return 1 103 } 104 105 desc := "OpenTofu will remove the lock on the remote state.\n" + 106 "This will allow local OpenTofu commands to modify this state, even though it\n" + 107 "may still be in use. Only 'yes' will be accepted to confirm." 108 109 v, err := c.UIInput().Input(context.Background(), &tofu.InputOpts{ 110 Id: "force-unlock", 111 Query: "Do you really want to force-unlock?", 112 Description: desc, 113 }) 114 if err != nil { 115 c.Ui.Error(fmt.Sprintf("Error asking for confirmation: %s", err)) 116 return 1 117 } 118 if v != "yes" { 119 c.Ui.Output("force-unlock cancelled.") 120 return 1 121 } 122 } 123 124 if err := stateMgr.Unlock(lockID); err != nil { 125 c.Ui.Error(fmt.Sprintf("Failed to unlock state: %s", err)) 126 return 1 127 } 128 129 c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputUnlockSuccess))) 130 return 0 131 } 132 133 func (c *UnlockCommand) Help() string { 134 helpText := ` 135 Usage: tofu [global options] force-unlock LOCK_ID 136 137 Manually unlock the state for the defined configuration. 138 139 This will not modify your infrastructure. This command removes the lock on the 140 state for the current workspace. The behavior of this lock is dependent 141 on the backend being used. Local state files cannot be unlocked by another 142 process. 143 144 Options: 145 146 -force Don't ask for input for unlock confirmation. 147 ` 148 return strings.TrimSpace(helpText) 149 } 150 151 func (c *UnlockCommand) Synopsis() string { 152 return "Release a stuck lock on the current workspace" 153 } 154 155 const outputUnlockSuccess = ` 156 [reset][bold][green]OpenTofu state has been successfully unlocked![reset][green] 157 158 The state has been unlocked, and OpenTofu commands should now be able to 159 obtain a new lock on the remote state. 160 `