| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package local |
| |
| import ( |
| "log" |
| "sync" |
| "time" |
| |
| "github.com/hashicorp/terraform/internal/schemarepo" |
| "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 *schemarepo.Schemas |
| |
| intermediatePersist statemgr.IntermediateStatePersistInfo |
| } |
| |
| 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.(statemgr.IntermediateStateConditionalPersister); ok { |
| return m.ShouldPersistIntermediateState(&h.intermediatePersist) |
| } |
| return statemgr.DefaultIntermediateStatePersistRule(&h.intermediatePersist) |
| } |