Skip to content

Commit

Permalink
feat: add state lock command
Browse files Browse the repository at this point in the history
  • Loading branch information
ventsislav-georgiev committed Nov 17, 2024
1 parent e044e56 commit 899fbab
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 0 deletions.
6 changes: 6 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,12 @@ func initCommands(
}, nil
},

"state lock": func() (cli.Command, error) {
return &command.StateLockCommand{
Meta: meta,
}, nil
},

"state rm": func() (cli.Command, error) {
return &command.StateRmCommand{
StateMeta: command.StateMeta{
Expand Down
10 changes: 10 additions & 0 deletions internal/command/clistate/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ type Locker interface {

// Timeout returns the configured timeout duration
Timeout() time.Duration

LockID() string
}

type locker struct {
Expand Down Expand Up @@ -167,6 +169,10 @@ func (l *locker) Timeout() time.Duration {
return l.timeout
}

func (l *locker) LockID() string {
return l.lockID
}

type noopLocker struct{}

// NewNoopLocker returns a valid Locker that does nothing.
Expand All @@ -191,3 +197,7 @@ func (l noopLocker) Unlock() tfdiags.Diagnostics {
func (l noopLocker) Timeout() time.Duration {
return 0
}

func (l noopLocker) LockID() string {
return ""
}
120 changes: 120 additions & 0 deletions internal/command/state_lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package command

import (
"fmt"
"strings"

"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/states/statemgr"

"github.com/hashicorp/cli"
"github.com/hashicorp/terraform/internal/tfdiags"
)

// StateLockCommand is a cli.Command implementation that manually locks
// the state.
type StateLockCommand struct {
Meta
StateMeta
}

func (c *StateLockCommand) Run(args []string) int {
args = c.Meta.process(args)
var statePath string
cmdFlags := c.Meta.defaultFlagSet("state lock")
cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout")
cmdFlags.StringVar(&statePath, "state", "", "path")
if err := cmdFlags.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
return cli.RunResultHelp
}
args = cmdFlags.Args()

if statePath != "" {
c.Meta.statePath = statePath
}

// assume everything is initialized. The user can manually init if this is
// required.
configPath, err := ModulePath(args)
if err != nil {
c.Ui.Error(err.Error())
return 1
}

var diags tfdiags.Diagnostics

backendConfig, backendDiags := c.loadBackendConfig(configPath)
diags = diags.Append(backendDiags)
if diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}

// Load the backend
b, backendDiags := c.Backend(&BackendOpts{
Config: backendConfig,
})
diags = diags.Append(backendDiags)
if backendDiags.HasErrors() {
c.showDiagnostics(diags)
return 1
}

env, err := c.Workspace()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
return 1
}
stateMgr, err := b.StateMgr(env)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1
}

_, isLocal := stateMgr.(*statemgr.Filesystem)
if isLocal {
c.Ui.Error("Local state locking is redundant.")
return 0
}

stateLocker := clistate.NewLocker(c.stateLockTimeout, views.NewStateLocker(arguments.ViewHuman, c.View))
if diags := stateLocker.Lock(stateMgr, "state-lock"); diags.HasErrors() {
c.showDiagnostics(diags)
return 1
}

c.Ui.Output(c.Colorize().Color(strings.TrimSpace(fmt.Sprintf(outputStateLockSuccess, stateLocker.LockID()))))
return 0
}

func (c *StateLockCommand) Help() string {
helpText := `
Usage: terraform [global options] state lock
Manually lock the state for the defined configuration.
This will not modify your infrastructure. This command adds a lock on the
state for the current workspace. The behavior of this lock is dependent
on the backend being used. Local state files cannot be locked.
`
return strings.TrimSpace(helpText)
}

func (c *StateLockCommand) Synopsis() string {
return "Acquire a lock on the current workspace"
}

const outputStateLockSuccess = `
LOCKID: %s
[reset][bold][green]Terraform state has been successfully been locked![reset][green]
The state has been locked, and Terraform commands should now be blocked from
obtaining a new lock on the remote state.
`

0 comments on commit 899fbab

Please sign in to comment.