package terraform import ( "fmt" "log" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" ) type phaseState int const ( workingState phaseState = iota refreshState ) // EvalReadState is an EvalNode implementation that reads the // current object for a specific instance in the state. type EvalReadState struct { // Addr is the address of the instance to read state for. Addr addrs.ResourceInstance // ProviderSchema is the schema for the provider given in Provider. ProviderSchema **ProviderSchema // Provider is the provider that will subsequently perform actions on // the the state object. This is used to perform any schema upgrades // that might be required to prepare the stored data for use. Provider *providers.Interface // Output will be written with a pointer to the retrieved object. Output **states.ResourceInstanceObject } func (n *EvalReadState) Eval(ctx EvalContext) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if n.Provider == nil || *n.Provider == nil { panic("EvalReadState used with no Provider object") } if n.ProviderSchema == nil || *n.ProviderSchema == nil { panic("EvalReadState used with no ProviderSchema object") } absAddr := n.Addr.Absolute(ctx.Path()) log.Printf("[TRACE] EvalReadState: reading state for %s", absAddr) src := ctx.State().ResourceInstanceObject(absAddr, states.CurrentGen) if src == nil { // Presumably we only have deposed objects, then. log.Printf("[TRACE] EvalReadState: no state present for %s", absAddr) return nil } schema, currentVersion := (*n.ProviderSchema).SchemaForResourceAddr(n.Addr.ContainingResource()) if schema == nil { // Shouldn't happen since we should've failed long ago if no schema is present diags = diags.Append(fmt.Errorf("no schema available for %s while reading state; this is a bug in Terraform and should be reported", absAddr)) return diags } src, diags = UpgradeResourceState(absAddr, *n.Provider, src, schema, currentVersion) if diags.HasErrors() { // Note that we don't have any channel to return warnings here. We'll // accept that for now since warnings during a schema upgrade would // be pretty weird anyway, since this operation is supposed to seem // invisible to the user. return diags } obj, err := src.Decode(schema.ImpliedType()) if err != nil { diags = diags.Append(err) return diags } if n.Output != nil { *n.Output = obj } return diags } // EvalReadStateDeposed is an EvalNode implementation that reads the // deposed InstanceState for a specific resource out of the state type EvalReadStateDeposed struct { // Addr is the address of the instance to read state for. Addr addrs.ResourceInstance // Key identifies which deposed object we will read. Key states.DeposedKey // ProviderSchema is the schema for the provider given in Provider. ProviderSchema **ProviderSchema // Provider is the provider that will subsequently perform actions on // the the state object. This is used to perform any schema upgrades // that might be required to prepare the stored data for use. Provider *providers.Interface // Output will be written with a pointer to the retrieved object. Output **states.ResourceInstanceObject } func (n *EvalReadStateDeposed) Eval(ctx EvalContext) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if n.Provider == nil || *n.Provider == nil { panic("EvalReadStateDeposed used with no Provider object") } if n.ProviderSchema == nil || *n.ProviderSchema == nil { panic("EvalReadStateDeposed used with no ProviderSchema object") } key := n.Key if key == states.NotDeposed { diags = diags.Append(fmt.Errorf("EvalReadStateDeposed used with no instance key; this is a bug in Terraform and should be reported")) return diags } absAddr := n.Addr.Absolute(ctx.Path()) log.Printf("[TRACE] EvalReadStateDeposed: reading state for %s deposed object %s", absAddr, n.Key) src := ctx.State().ResourceInstanceObject(absAddr, key) if src == nil { // Presumably we only have deposed objects, then. log.Printf("[TRACE] EvalReadStateDeposed: no state present for %s deposed object %s", absAddr, n.Key) return diags } schema, currentVersion := (*n.ProviderSchema).SchemaForResourceAddr(n.Addr.ContainingResource()) if schema == nil { // Shouldn't happen since we should've failed long ago if no schema is present diags = diags.Append(fmt.Errorf("no schema available for %s while reading state; this is a bug in Terraform and should be reported", absAddr)) return diags } src, diags = UpgradeResourceState(absAddr, *n.Provider, src, schema, currentVersion) if diags.HasErrors() { // Note that we don't have any channel to return warnings here. We'll // accept that for now since warnings during a schema upgrade would // be pretty weird anyway, since this operation is supposed to seem // invisible to the user. return diags } obj, err := src.Decode(schema.ImpliedType()) if err != nil { diags = diags.Append(err) return diags } if n.Output != nil { *n.Output = obj } return diags } // UpdateStateHook calls the PostStateUpdate hook with the current state. func UpdateStateHook(ctx EvalContext) error { // In principle we could grab the lock here just long enough to take a // deep copy and then pass that to our hooks below, but we'll instead // hold the hook for the duration to avoid the potential confusing // situation of us racing to call PostStateUpdate concurrently with // different state snapshots. stateSync := ctx.State() state := stateSync.Lock().DeepCopy() defer stateSync.Unlock() // Call the hook err := ctx.Hook(func(h Hook) (HookAction, error) { return h.PostStateUpdate(state) }) return err } // evalWriteEmptyState wraps EvalWriteState to specifically record an empty // state for a particular object. type evalWriteEmptyState struct { EvalWriteState } func (n *evalWriteEmptyState) Eval(ctx EvalContext) tfdiags.Diagnostics { var state *states.ResourceInstanceObject n.State = &state return n.EvalWriteState.Eval(ctx) } // EvalWriteState is an EvalNode implementation that saves the given object // as the current object for the selected resource instance. type EvalWriteState struct { // Addr is the address of the instance to read state for. Addr addrs.ResourceInstance // State is the object state to save. State **states.ResourceInstanceObject // ProviderSchema is the schema for the provider given in ProviderAddr. ProviderSchema **ProviderSchema // ProviderAddr is the address of the provider configuration that // produced the given object. ProviderAddr addrs.AbsProviderConfig // Dependencies are the inter-resource dependencies to be stored in the // state. Dependencies *[]addrs.ConfigResource // targetState determines which context state we're writing to during plan. // The default is the global working state. targetState phaseState } func (n *EvalWriteState) Eval(ctx EvalContext) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if n.State == nil { // Note that a pointer _to_ nil is valid here, indicating the total // absense of an object as we'd see during destroy. panic("EvalWriteState used with no ResourceInstanceObject") } absAddr := n.Addr.Absolute(ctx.Path()) var state *states.SyncState switch n.targetState { case refreshState: log.Printf("[TRACE] EvalWriteState: using RefreshState for %s", absAddr) state = ctx.RefreshState() default: state = ctx.State() } if n.ProviderAddr.Provider.Type == "" { diags = diags.Append(fmt.Errorf("failed to write state for %s: missing provider type", absAddr)) return diags } obj := *n.State if obj == nil || obj.Value.IsNull() { // No need to encode anything: we'll just write it directly. state.SetResourceInstanceCurrent(absAddr, nil, n.ProviderAddr) log.Printf("[TRACE] EvalWriteState: removing state object for %s", absAddr) return diags } // store the new deps in the state if n.Dependencies != nil { log.Printf("[TRACE] EvalWriteState: recording %d dependencies for %s", len(*n.Dependencies), absAddr) obj.Dependencies = *n.Dependencies } if n.ProviderSchema == nil || *n.ProviderSchema == nil { // Should never happen, unless our state object is nil panic("EvalWriteState used with pointer to nil ProviderSchema object") } if obj != nil { log.Printf("[TRACE] EvalWriteState: writing current state object for %s", absAddr) } else { log.Printf("[TRACE] EvalWriteState: removing current state object for %s", absAddr) } schema, currentVersion := (*n.ProviderSchema).SchemaForResourceAddr(n.Addr.ContainingResource()) if schema == nil { // It shouldn't be possible to get this far in any real scenario // without a schema, but we might end up here in contrived tests that // fail to set up their world properly. diags = diags.Append(fmt.Errorf("failed to encode %s in state: no resource type schema available", absAddr)) return diags } src, err := obj.Encode(schema.ImpliedType(), currentVersion) if err != nil { diags = diags.Append(fmt.Errorf("failed to encode %s in state: %s", absAddr, err)) return diags } state.SetResourceInstanceCurrent(absAddr, src, n.ProviderAddr) return diags } // EvalWriteStateDeposed is an EvalNode implementation that writes // an InstanceState out to the Deposed list of a resource in the state. type EvalWriteStateDeposed struct { // Addr is the address of the instance to read state for. Addr addrs.ResourceInstance // Key indicates which deposed object to write to. Key states.DeposedKey // State is the object state to save. State **states.ResourceInstanceObject // ProviderSchema is the schema for the provider given in ProviderAddr. ProviderSchema **ProviderSchema // ProviderAddr is the address of the provider configuration that // produced the given object. ProviderAddr addrs.AbsProviderConfig } func (n *EvalWriteStateDeposed) Eval(ctx EvalContext) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if n.State == nil { // Note that a pointer _to_ nil is valid here, indicating the total // absense of an object as we'd see during destroy. panic("EvalWriteStateDeposed used with no ResourceInstanceObject") } absAddr := n.Addr.Absolute(ctx.Path()) key := n.Key state := ctx.State() if key == states.NotDeposed { // should never happen diags = diags.Append(fmt.Errorf("can't save deposed object for %s without a deposed key; this is a bug in Terraform that should be reported", absAddr)) return diags } obj := *n.State if obj == nil { // No need to encode anything: we'll just write it directly. state.SetResourceInstanceDeposed(absAddr, key, nil, n.ProviderAddr) log.Printf("[TRACE] EvalWriteStateDeposed: removing state object for %s deposed %s", absAddr, key) return diags } if n.ProviderSchema == nil || *n.ProviderSchema == nil { // Should never happen, unless our state object is nil panic("EvalWriteStateDeposed used with no ProviderSchema object") } schema, currentVersion := (*n.ProviderSchema).SchemaForResourceAddr(n.Addr.ContainingResource()) if schema == nil { // It shouldn't be possible to get this far in any real scenario // without a schema, but we might end up here in contrived tests that // fail to set up their world properly. diags = diags.Append(fmt.Errorf("failed to encode %s in state: no resource type schema available", absAddr)) return diags } src, err := obj.Encode(schema.ImpliedType(), currentVersion) if err != nil { diags = diags.Append(fmt.Errorf("failed to encode %s in state: %s", absAddr, err)) return diags } log.Printf("[TRACE] EvalWriteStateDeposed: writing state object for %s deposed %s", absAddr, key) state.SetResourceInstanceDeposed(absAddr, key, src, n.ProviderAddr) return diags } // EvalDeposeState is an EvalNode implementation that moves the current object // for the given instance to instead be a deposed object, leaving the instance // with no current object. // This is used at the beginning of a create-before-destroy replace action so // that the create can create while preserving the old state of the // to-be-destroyed object. type EvalDeposeState struct { Addr addrs.ResourceInstance // ForceKey, if a value other than states.NotDeposed, will be used as the // key for the newly-created deposed object that results from this action. // If set to states.NotDeposed (the zero value), a new unique key will be // allocated. ForceKey states.DeposedKey // OutputKey, if non-nil, will be written with the deposed object key that // was generated for the object. This can then be passed to // EvalUndeposeState.Key so it knows which deposed instance to forget. OutputKey *states.DeposedKey } // TODO: test func (n *EvalDeposeState) Eval(ctx EvalContext) tfdiags.Diagnostics { absAddr := n.Addr.Absolute(ctx.Path()) state := ctx.State() var key states.DeposedKey if n.ForceKey == states.NotDeposed { key = state.DeposeResourceInstanceObject(absAddr) } else { key = n.ForceKey state.DeposeResourceInstanceObjectForceKey(absAddr, key) } log.Printf("[TRACE] EvalDeposeState: prior object for %s now deposed with key %s", absAddr, key) if n.OutputKey != nil { *n.OutputKey = key } return nil } // EvalMaybeRestoreDeposedObject is an EvalNode implementation that will // restore a particular deposed object of the specified resource instance // to be the "current" object if and only if the instance doesn't currently // have a current object. // // This is intended for use when the create leg of a create before destroy // fails with no partial new object: if we didn't take any action, the user // would be left in the unfortunate situation of having no current object // and the previously-workign object now deposed. This EvalNode causes a // better outcome by restoring things to how they were before the replace // operation began. // // The create operation may have produced a partial result even though it // failed and it's important that we don't "forget" that state, so in that // situation the prior object remains deposed and the partial new object // remains the current object, allowing the situation to hopefully be // improved in a subsequent run. type EvalMaybeRestoreDeposedObject struct { Addr addrs.ResourceInstance // PlannedChange might be the action we're performing that includes // the possiblity of restoring a deposed object. However, it might also // be nil. It's here only for use in error messages and must not be // used for business logic. PlannedChange **plans.ResourceInstanceChange // Key is a pointer to the deposed object key that should be forgotten // from the state, which must be non-nil. Key *states.DeposedKey } // TODO: test func (n *EvalMaybeRestoreDeposedObject) Eval(ctx EvalContext) tfdiags.Diagnostics { var diags tfdiags.Diagnostics absAddr := n.Addr.Absolute(ctx.Path()) dk := *n.Key state := ctx.State() if dk == states.NotDeposed { // This should never happen, and so it always indicates a bug. // We should evaluate this node only if we've previously deposed // an object as part of the same operation. if n.PlannedChange != nil && *n.PlannedChange != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Attempt to restore non-existent deposed object", fmt.Sprintf( "Terraform has encountered a bug where it would need to restore a deposed object for %s without knowing a deposed object key for that object. This occurred during a %s action. This is a bug in Terraform; please report it!", absAddr, (*n.PlannedChange).Action, ), )) } else { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Attempt to restore non-existent deposed object", fmt.Sprintf( "Terraform has encountered a bug where it would need to restore a deposed object for %s without knowing a deposed object key for that object. This is a bug in Terraform; please report it!", absAddr, ), )) } return diags } restored := state.MaybeRestoreResourceInstanceDeposed(absAddr, dk) if restored { log.Printf("[TRACE] EvalMaybeRestoreDeposedObject: %s deposed object %s was restored as the current object", absAddr, dk) } else { log.Printf("[TRACE] EvalMaybeRestoreDeposedObject: %s deposed object %s remains deposed", absAddr, dk) } return diags } // EvalRefreshLifecycle is an EvalNode implementation that updates // the status of the lifecycle options stored in the state. // This currently only applies to create_before_destroy. type EvalRefreshLifecycle struct { Addr addrs.AbsResourceInstance Config *configs.Resource // Prior State State **states.ResourceInstanceObject // ForceCreateBeforeDestroy indicates a create_before_destroy resource // depends on this resource. ForceCreateBeforeDestroy bool } func (n *EvalRefreshLifecycle) Eval(ctx EvalContext) tfdiags.Diagnostics { state := *n.State if state == nil { // no existing state return nil } // In 0.13 we could be refreshing a resource with no config. // We should be operating on managed resource, but check here to be certain if n.Config == nil || n.Config.Managed == nil { log.Printf("[WARN] EvalRefreshLifecycle: no Managed config value found in instance state for %q", n.Addr) return nil } state.CreateBeforeDestroy = n.Config.Managed.CreateBeforeDestroy || n.ForceCreateBeforeDestroy return nil }