github.com/opentofu/opentofu@v1.7.1/internal/command/clistate/state.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 state exposes common helpers for working with state from the CLI.
     7  //
     8  // This is a separate package so that backends can use this for consistent
     9  // messaging without creating a circular reference to the command package.
    10  package clistate
    11  
    12  import (
    13  	"context"
    14  	"fmt"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/opentofu/opentofu/internal/command/views"
    19  	"github.com/opentofu/opentofu/internal/helper/slowmessage"
    20  	"github.com/opentofu/opentofu/internal/states/statemgr"
    21  	"github.com/opentofu/opentofu/internal/tfdiags"
    22  )
    23  
    24  const (
    25  	LockThreshold    = 400 * time.Millisecond
    26  	LockErrorMessage = `Error message: %s
    27  
    28  OpenTofu acquires a state lock to protect the state from being written
    29  by multiple users at the same time. Please resolve the issue above and try
    30  again. For most commands, you can disable locking with the "-lock=false"
    31  flag, but this is not recommended.`
    32  
    33  	UnlockErrorMessage = `Error message: %s
    34  
    35  OpenTofu acquires a lock when accessing your state to prevent others
    36  running OpenTofu to potentially modify the state at the same time. An
    37  error occurred while releasing this lock. This could mean that the lock
    38  did or did not release properly. If the lock didn't release properly,
    39  OpenTofu may not be able to run future commands since it'll appear as if
    40  the lock is held.
    41  
    42  In this scenario, please call the "force-unlock" command to unlock the
    43  state manually. This is a very dangerous operation since if it is done
    44  erroneously it could result in two people modifying state at the same time.
    45  Only call this command if you're certain that the unlock above failed and
    46  that no one else is holding a lock.`
    47  )
    48  
    49  // Locker allows for more convenient usage of the lower-level statemgr.Locker
    50  // implementations.
    51  // The statemgr.Locker API requires passing in a statemgr.LockInfo struct. Locker
    52  // implementations are expected to create the required LockInfo struct when
    53  // Lock is called, populate the Operation field with the "reason" string
    54  // provided, and pass that on to the underlying statemgr.Locker.
    55  // Locker implementations are also expected to store any state required to call
    56  // Unlock, which is at a minimum the LockID string returned by the
    57  // statemgr.Locker.
    58  type Locker interface {
    59  	// Returns a shallow copy of the locker with its context changed to ctx.
    60  	WithContext(ctx context.Context) Locker
    61  
    62  	// Lock the provided state manager, storing the reason string in the LockInfo.
    63  	Lock(s statemgr.Locker, reason string) tfdiags.Diagnostics
    64  
    65  	// Unlock the previously locked state.
    66  	Unlock() tfdiags.Diagnostics
    67  
    68  	// Timeout returns the configured timeout duration
    69  	Timeout() time.Duration
    70  }
    71  
    72  type locker struct {
    73  	mu      sync.Mutex
    74  	ctx     context.Context
    75  	timeout time.Duration
    76  	state   statemgr.Locker
    77  	view    views.StateLocker
    78  	lockID  string
    79  }
    80  
    81  var _ Locker = (*locker)(nil)
    82  
    83  // Create a new Locker.
    84  // This Locker uses state.LockWithContext to retry the lock until the provided
    85  // timeout is reached, or the context is canceled. Lock progress will be be
    86  // reported to the user through the provided UI.
    87  func NewLocker(timeout time.Duration, view views.StateLocker) Locker {
    88  	return &locker{
    89  		ctx:     context.Background(),
    90  		timeout: timeout,
    91  		view:    view,
    92  	}
    93  }
    94  
    95  // WithContext returns a new Locker with the specified context, copying the
    96  // timeout and view parameters from the original Locker.
    97  func (l *locker) WithContext(ctx context.Context) Locker {
    98  	if ctx == nil {
    99  		panic("nil context")
   100  	}
   101  	return &locker{
   102  		ctx:     ctx,
   103  		timeout: l.timeout,
   104  		view:    l.view,
   105  	}
   106  }
   107  
   108  // Locker locks the given state and outputs to the user if locking is taking
   109  // longer than the threshold. The lock is retried until the context is
   110  // cancelled.
   111  func (l *locker) Lock(s statemgr.Locker, reason string) tfdiags.Diagnostics {
   112  	var diags tfdiags.Diagnostics
   113  
   114  	l.mu.Lock()
   115  	defer l.mu.Unlock()
   116  
   117  	l.state = s
   118  
   119  	ctx, cancel := context.WithTimeout(l.ctx, l.timeout)
   120  	defer cancel()
   121  
   122  	lockInfo := statemgr.NewLockInfo()
   123  	lockInfo.Operation = reason
   124  
   125  	err := slowmessage.Do(LockThreshold, func() error {
   126  		id, err := statemgr.LockWithContext(ctx, s, lockInfo)
   127  		l.lockID = id
   128  		return err
   129  	}, l.view.Locking)
   130  
   131  	if err != nil {
   132  		diags = diags.Append(tfdiags.Sourceless(
   133  			tfdiags.Error,
   134  			"Error acquiring the state lock",
   135  			fmt.Sprintf(LockErrorMessage, err),
   136  		))
   137  	}
   138  
   139  	return diags
   140  }
   141  
   142  func (l *locker) Unlock() tfdiags.Diagnostics {
   143  	var diags tfdiags.Diagnostics
   144  
   145  	l.mu.Lock()
   146  	defer l.mu.Unlock()
   147  
   148  	if l.lockID == "" {
   149  		return diags
   150  	}
   151  
   152  	err := slowmessage.Do(LockThreshold, func() error {
   153  		return l.state.Unlock(l.lockID)
   154  	}, l.view.Unlocking)
   155  
   156  	if err != nil {
   157  		diags = diags.Append(tfdiags.Sourceless(
   158  			tfdiags.Error,
   159  			"Error releasing the state lock",
   160  			fmt.Sprintf(UnlockErrorMessage, err),
   161  		))
   162  	}
   163  
   164  	return diags
   165  
   166  }
   167  
   168  func (l *locker) Timeout() time.Duration {
   169  	return l.timeout
   170  }
   171  
   172  type noopLocker struct{}
   173  
   174  // NewNoopLocker returns a valid Locker that does nothing.
   175  func NewNoopLocker() Locker {
   176  	return noopLocker{}
   177  }
   178  
   179  var _ Locker = noopLocker{}
   180  
   181  func (l noopLocker) WithContext(ctx context.Context) Locker {
   182  	return l
   183  }
   184  
   185  func (l noopLocker) Lock(statemgr.Locker, string) tfdiags.Diagnostics {
   186  	return nil
   187  }
   188  
   189  func (l noopLocker) Unlock() tfdiags.Diagnostics {
   190  	return nil
   191  }
   192  
   193  func (l noopLocker) Timeout() time.Duration {
   194  	return 0
   195  }