191 lines
5.2 KiB
Go
191 lines
5.2 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/internal/command/views"
|
|
"github.com/hashicorp/terraform/internal/helper/slowmessage"
|
|
"github.com/hashicorp/terraform/internal/states/statemgr"
|
|
"github.com/hashicorp/terraform/internal/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
|
|
}
|