opentofu/command/clistate/state.go
Alisdair McDiarmid 2d1976bbda clistate: Update clistate.Locker for command views
The clistate package includes a Locker interface which provides a simple
way for the local backend to lock and unlock state, while providing
feedback to the user if there is a delay while waiting for the lock.
Prior to this commit, the backend was responsible for initializing the
Locker, passing through direct access to the cli.Ui instance.

This structure prevented commands from implementing different
implementations of the state locker UI. In this commit, we:

- Move the responsibility of creating the appropriate Locker to the
  source of the Operation;
- Add the ability to set the context for a Locker via a WithContext
  method;
- Replace the Locker's cli.Ui and Colorize members with a StateLocker
  view;
- Implement views.StateLocker for human-readable UI;
- Update the Locker interface to return detailed diagnostics instead of
  errors, reducing its direct interactions with UI;
- Add a Timeout() method on Locker to allow the remote backend to
  continue to misuse the -lock-timeout flag to cancel pending runs.

When an Operation is created, the StateLocker field must now be
populated with an implementation of Locker. For situations where locking
is disabled, this can be a no-op locker.

This change has no significant effect on the operation of Terraform,
with the exception of slightly different formatting of errors when state
locking or unlocking fails.
2021-02-16 07:19:22 -05:00

191 lines
5.1 KiB
Go

// Package state exposes common helpers for working with state from the CLI.
//
// This is a separate package so that backends can use this for consistent
// messaging without creating a circular reference to the command package.
package clistate
import (
"context"
"fmt"
"sync"
"time"
"github.com/hashicorp/terraform/command/views"
"github.com/hashicorp/terraform/internal/helper/slowmessage"
"github.com/hashicorp/terraform/states/statemgr"
"github.com/hashicorp/terraform/tfdiags"
)
const (
LockThreshold = 400 * time.Millisecond
LockErrorMessage = `Error message: %s
Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and try
again. For most commands, you can disable locking with the "-lock=false"
flag, but this is not recommended.`
UnlockErrorMessage = `Error message: %s
Terraform acquires a lock when accessing your state to prevent others
running Terraform to potentially modify the state at the same time. An
error occurred while releasing this lock. This could mean that the lock
did or did not release properly. If the lock didn't release properly,
Terraform may not be able to run future commands since it'll appear as if
the lock is held.
In this scenario, please call the "force-unlock" command to unlock the
state manually. This is a very dangerous operation since if it is done
erroneously it could result in two people modifying state at the same time.
Only call this command if you're certain that the unlock above failed and
that no one else is holding a lock.`
)
// Locker allows for more convenient usage of the lower-level statemgr.Locker
// implementations.
// The statemgr.Locker API requires passing in a statemgr.LockInfo struct. Locker
// implementations are expected to create the required LockInfo struct when
// Lock is called, populate the Operation field with the "reason" string
// provided, and pass that on to the underlying statemgr.Locker.
// Locker implementations are also expected to store any state required to call
// Unlock, which is at a minimum the LockID string returned by the
// statemgr.Locker.
type Locker interface {
// Returns a shallow copy of the locker with its context changed to ctx.
WithContext(ctx context.Context) Locker
// Lock the provided state manager, storing the reason string in the LockInfo.
Lock(s statemgr.Locker, reason string) tfdiags.Diagnostics
// Unlock the previously locked state.
Unlock() tfdiags.Diagnostics
// Timeout returns the configured timeout duration
Timeout() time.Duration
}
type locker struct {
mu sync.Mutex
ctx context.Context
timeout time.Duration
state statemgr.Locker
view views.StateLocker
lockID string
}
var _ Locker = (*locker)(nil)
// Create a new Locker.
// This Locker uses state.LockWithContext to retry the lock until the provided
// timeout is reached, or the context is canceled. Lock progress will be be
// reported to the user through the provided UI.
func NewLocker(timeout time.Duration, view views.StateLocker) Locker {
return &locker{
ctx: context.Background(),
timeout: timeout,
view: view,
}
}
// WithContext returns a new Locker with the specified context, copying the
// timeout and view parameters from the original Locker.
func (l *locker) WithContext(ctx context.Context) Locker {
if ctx == nil {
panic("nil context")
}
return &locker{
ctx: ctx,
timeout: l.timeout,
view: l.view,
}
}
// Locker locks the given state and outputs to the user if locking is taking
// longer than the threshold. The lock is retried until the context is
// cancelled.
func (l *locker) Lock(s statemgr.Locker, reason string) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
l.mu.Lock()
defer l.mu.Unlock()
l.state = s
ctx, cancel := context.WithTimeout(l.ctx, l.timeout)
defer cancel()
lockInfo := statemgr.NewLockInfo()
lockInfo.Operation = reason
err := slowmessage.Do(LockThreshold, func() error {
id, err := statemgr.LockWithContext(ctx, s, lockInfo)
l.lockID = id
return err
}, l.view.Locking)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error acquiring the state lock",
fmt.Sprintf(LockErrorMessage, err),
))
}
return diags
}
func (l *locker) Unlock() tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
l.mu.Lock()
defer l.mu.Unlock()
if l.lockID == "" {
return diags
}
err := slowmessage.Do(LockThreshold, func() error {
return l.state.Unlock(l.lockID)
}, l.view.Unlocking)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Error releasing the state lock",
fmt.Sprintf(UnlockErrorMessage, err),
))
}
return diags
}
func (l *locker) Timeout() time.Duration {
return l.timeout
}
type noopLocker struct{}
// NewNoopLocker returns a valid Locker that does nothing.
func NewNoopLocker() Locker {
return noopLocker{}
}
var _ Locker = noopLocker{}
func (l noopLocker) WithContext(ctx context.Context) Locker {
return l
}
func (l noopLocker) Lock(statemgr.Locker, string) tfdiags.Diagnostics {
return nil
}
func (l noopLocker) Unlock() tfdiags.Diagnostics {
return nil
}
func (l noopLocker) Timeout() time.Duration {
return 0
}