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 }