| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package local |
| |
| import ( |
| "log" |
| "sync" |
| "time" |
| |
| "github.com/hashicorp/terraform/internal/states" |
| "github.com/hashicorp/terraform/internal/states/statemgr" |
| "github.com/hashicorp/terraform/internal/terraform" |
| ) |
| |
| // StateHook is a hook that continuously updates the state by calling |
| // WriteState on a statemgr.Full. |
| type StateHook struct { |
| terraform.NilHook |
| sync.Mutex |
| |
| StateMgr statemgr.Writer |
| |
| // If PersistInterval is nonzero then for any new state update after |
| // the duration has elapsed we'll try to persist a state snapshot |
| // to the persistent backend too. |
| // That's only possible if field Schemas is valid, because the |
| // StateMgr.PersistState function for some backends needs schemas. |
| PersistInterval time.Duration |
| |
| // Schemas are the schemas to use when persisting state due to |
| // PersistInterval. This is ignored if PersistInterval is zero, |
| // and PersistInterval is ignored if this is nil. |
| Schemas *terraform.Schemas |
| |
| intermediatePersist IntermediateStatePersistInfo |
| } |
| |
| type IntermediateStatePersistInfo struct { |
| // RequestedPersistInterval is the persist interval requested by whatever |
| // instantiated the StateHook. |
| // |
| // Implementations of [IntermediateStateConditionalPersister] should ideally |
| // respect this, but may ignore it if they use something other than the |
| // passage of time to make their decision. |
| RequestedPersistInterval time.Duration |
| |
| // LastPersist is the time when the last intermediate state snapshot was |
| // persisted, or the time of the first report for Terraform Core if there |
| // hasn't yet been a persisted snapshot. |
| LastPersist time.Time |
| |
| // ForcePersist is true when Terraform CLI has receieved an interrupt |
| // signal and is therefore trying to create snapshots more aggressively |
| // in anticipation of possibly being terminated ungracefully. |
| // [IntermediateStateConditionalPersister] implementations should ideally |
| // persist every snapshot they get when this flag is set, unless they have |
| // some external information that implies this shouldn't be necessary. |
| ForcePersist bool |
| } |
| |
| var _ terraform.Hook = (*StateHook)(nil) |
| |
| func (h *StateHook) PostStateUpdate(new *states.State) (terraform.HookAction, error) { |
| h.Lock() |
| defer h.Unlock() |
| |
| h.intermediatePersist.RequestedPersistInterval = h.PersistInterval |
| |
| if h.intermediatePersist.LastPersist.IsZero() { |
| // The first PostStateUpdate starts the clock for intermediate |
| // calls to PersistState. |
| h.intermediatePersist.LastPersist = time.Now() |
| } |
| |
| if h.StateMgr != nil { |
| if err := h.StateMgr.WriteState(new); err != nil { |
| return terraform.HookActionHalt, err |
| } |
| if mgrPersist, ok := h.StateMgr.(statemgr.Persister); ok && h.PersistInterval != 0 && h.Schemas != nil { |
| if h.shouldPersist() { |
| err := mgrPersist.PersistState(h.Schemas) |
| if err != nil { |
| return terraform.HookActionHalt, err |
| } |
| h.intermediatePersist.LastPersist = time.Now() |
| } else { |
| log.Printf("[DEBUG] State storage %T declined to persist a state snapshot", h.StateMgr) |
| } |
| } |
| } |
| |
| return terraform.HookActionContinue, nil |
| } |
| |
| func (h *StateHook) Stopping() { |
| h.Lock() |
| defer h.Unlock() |
| |
| // If Terraform has been asked to stop then that might mean that a hard |
| // kill signal will follow shortly in case Terraform doesn't stop |
| // quickly enough, and so we'll try to persist the latest state |
| // snapshot in the hope that it'll give the user less recovery work to |
| // do if they _do_ subsequently hard-kill Terraform during an apply. |
| |
| if mgrPersist, ok := h.StateMgr.(statemgr.Persister); ok && h.Schemas != nil { |
| // While we're in the stopping phase we'll try to persist every |
| // new state update to maximize every opportunity we get to avoid |
| // losing track of objects that have been created or updated. |
| // Terraform Core won't start any new operations after it's been |
| // stopped, so at most we should see one more PostStateUpdate |
| // call per already-active request. |
| h.intermediatePersist.ForcePersist = true |
| |
| if h.shouldPersist() { |
| err := mgrPersist.PersistState(h.Schemas) |
| if err != nil { |
| // This hook can't affect Terraform Core's ongoing behavior, |
| // but it's a best effort thing anyway so we'll just emit a |
| // log to aid with debugging. |
| log.Printf("[ERROR] Failed to persist state after interruption: %s", err) |
| } |
| } else { |
| log.Printf("[DEBUG] State storage %T declined to persist a state snapshot", h.StateMgr) |
| } |
| } |
| |
| } |
| |
| func (h *StateHook) shouldPersist() bool { |
| if m, ok := h.StateMgr.(IntermediateStateConditionalPersister); ok { |
| return m.ShouldPersistIntermediateState(&h.intermediatePersist) |
| } |
| return DefaultIntermediateStatePersistRule(&h.intermediatePersist) |
| } |
| |
| // DefaultIntermediateStatePersistRule is the default implementation of |
| // [IntermediateStateConditionalPersister.ShouldPersistIntermediateState] used |
| // when the selected state manager doesn't implement that interface. |
| // |
| // Implementers of that interface can optionally wrap a call to this function |
| // if they want to combine the default behavior with some logic of their own. |
| func DefaultIntermediateStatePersistRule(info *IntermediateStatePersistInfo) bool { |
| return info.ForcePersist || time.Since(info.LastPersist) >= info.RequestedPersistInterval |
| } |
| |
| // IntermediateStateConditionalPersister is an optional extension of |
| // [statemgr.Persister] that allows an implementation to tailor the rules for |
| // whether to create intermediate state snapshots when Terraform Core emits |
| // events reporting that the state might have changed. |
| // |
| // For state managers that don't implement this interface, [StateHook] uses |
| // a default set of rules that aim to be a good compromise between how long |
| // a state change can be active before it gets committed as a snapshot vs. |
| // how many intermediate snapshots will get created. That compromise is subject |
| // to change over time, but a state manager can implement this interface to |
| // exert full control over those rules. |
| type IntermediateStateConditionalPersister interface { |
| // ShouldPersistIntermediateState will be called each time Terraform Core |
| // emits an intermediate state event that is potentially eligible to be |
| // persisted. |
| // |
| // The implemention should return true to signal that the state snapshot |
| // most recently provided to the object's WriteState should be persisted, |
| // or false if it should not be persisted. If this function returns true |
| // then the receiver will see a subsequent call to |
| // [statemgr.Persister.PersistState] to request persistence. |
| // |
| // The implementation must not modify anything reachable through the |
| // arguments, and must not retain pointers to anything reachable through |
| // them after the function returns. However, implementers can assume that |
| // nothing will write to anything reachable through the arguments while |
| // this function is active. |
| ShouldPersistIntermediateState(info *IntermediateStatePersistInfo) bool |
| } |