github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/command/clistate/state.go (about)

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