package terraform import ( "fmt" "log" "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/dag" "github.com/hashicorp/terraform/tfdiags" ) // NodeRefreshableManagedResource represents a resource that is expanabled into // NodeRefreshableManagedResourceInstance. Resource count orphans are also added. type NodeRefreshableManagedResource struct { *NodeAbstractResource } var ( _ GraphNodeSubPath = (*NodeRefreshableManagedResource)(nil) _ GraphNodeDynamicExpandable = (*NodeRefreshableManagedResource)(nil) _ GraphNodeReferenceable = (*NodeRefreshableManagedResource)(nil) _ GraphNodeReferencer = (*NodeRefreshableManagedResource)(nil) _ GraphNodeResource = (*NodeRefreshableManagedResource)(nil) _ GraphNodeAttachResourceConfig = (*NodeRefreshableManagedResource)(nil) ) // GraphNodeDynamicExpandable func (n *NodeRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph, error) { var diags tfdiags.Diagnostics count, countDiags := evaluateResourceCountExpression(n.Config.Count, ctx) diags = diags.Append(countDiags) if countDiags.HasErrors() { return nil, diags.Err() } // Next we need to potentially rename an instance address in the state // if we're transitioning whether "count" is set at all. fixResourceCountSetTransition(ctx, n.ResourceAddr(), count != -1) // Our graph transformers require access to the full state, so we'll // temporarily lock it while we work on this. state := ctx.State().Lock() defer ctx.State().Unlock() // The concrete resource factory we'll use concreteResource := func(a *NodeAbstractResourceInstance) dag.Vertex { // Add the config and state since we don't do that via transforms a.Config = n.Config a.ResolvedProvider = n.ResolvedProvider return &NodeRefreshableManagedResourceInstance{ NodeAbstractResourceInstance: a, } } // Start creating the steps steps := []GraphTransformer{ // Expand the count. &ResourceCountTransformer{ Concrete: concreteResource, Schema: n.Schema, Count: count, Addr: n.ResourceAddr(), }, // Add the count orphans to make sure these resources are accounted for // during a scale in. &OrphanResourceCountTransformer{ Concrete: concreteResource, Count: count, Addr: n.ResourceAddr(), State: state, }, // Attach the state &AttachStateTransformer{State: state}, // Targeting &TargetsTransformer{Targets: n.Targets}, // Connect references so ordering is correct &ReferenceTransformer{}, // Make sure there is a single root &RootTransformer{}, } // Build the graph b := &BasicGraphBuilder{ Steps: steps, Validate: true, Name: "NodeRefreshableManagedResource", } graph, diags := b.Build(ctx.Path()) return graph, diags.ErrWithWarnings() } // NodeRefreshableManagedResourceInstance represents a resource that is "applyable": // it is ready to be applied and is represented by a diff. type NodeRefreshableManagedResourceInstance struct { *NodeAbstractResourceInstance } var ( _ GraphNodeSubPath = (*NodeRefreshableManagedResourceInstance)(nil) _ GraphNodeReferenceable = (*NodeRefreshableManagedResourceInstance)(nil) _ GraphNodeReferencer = (*NodeRefreshableManagedResourceInstance)(nil) _ GraphNodeDestroyer = (*NodeRefreshableManagedResourceInstance)(nil) _ GraphNodeResource = (*NodeRefreshableManagedResourceInstance)(nil) _ GraphNodeResourceInstance = (*NodeRefreshableManagedResourceInstance)(nil) _ GraphNodeAttachResourceConfig = (*NodeRefreshableManagedResourceInstance)(nil) _ GraphNodeAttachResourceState = (*NodeRefreshableManagedResourceInstance)(nil) _ GraphNodeEvalable = (*NodeRefreshableManagedResourceInstance)(nil) ) // GraphNodeDestroyer func (n *NodeRefreshableManagedResourceInstance) DestroyAddr() *addrs.AbsResourceInstance { addr := n.ResourceInstanceAddr() return &addr } // GraphNodeEvalable func (n *NodeRefreshableManagedResourceInstance) EvalTree() EvalNode { addr := n.ResourceInstanceAddr() // Eval info is different depending on what kind of resource this is switch addr.Resource.Resource.Mode { case addrs.ManagedResourceMode: if n.ResourceState == nil { log.Printf("[TRACE] NodeRefreshableManagedResourceInstance: %s has no existing state to refresh", addr) return n.evalTreeManagedResourceNoState() } log.Printf("[TRACE] NodeRefreshableManagedResourceInstance: %s will be refreshed", addr) return n.evalTreeManagedResource() case addrs.DataResourceMode: // Get the data source node. If we don't have a configuration // then it is an orphan so we destroy it (remove it from the state). var dn GraphNodeEvalable if n.Config != nil { dn = &NodeRefreshableDataResourceInstance{ NodeAbstractResourceInstance: n.NodeAbstractResourceInstance, } } else { dn = &NodeDestroyableDataResourceInstance{ NodeAbstractResourceInstance: n.NodeAbstractResourceInstance, } } return dn.EvalTree() default: panic(fmt.Errorf("unsupported resource mode %s", addr.Resource.Resource.Mode)) } } func (n *NodeRefreshableManagedResourceInstance) evalTreeManagedResource() EvalNode { addr := n.ResourceInstanceAddr() // Declare a bunch of variables that are used for state during // evaluation. Most of this are written to by-address below. var provider providers.Interface var providerSchema *ProviderSchema var state *states.ResourceInstanceObject // This happened during initial development. All known cases were // fixed and tested but as a sanity check let's assert here. if n.ResourceState == nil { err := fmt.Errorf( "No resource state attached for addr: %s\n\n"+ "This is a bug. Please report this to Terraform with your configuration\n"+ "and state attached. Please be careful to scrub any sensitive information.", addr) return &EvalReturnError{Error: &err} } return &EvalSequence{ Nodes: []EvalNode{ &EvalGetProvider{ Addr: n.ResolvedProvider, Output: &provider, Schema: &providerSchema, }, &EvalReadState{ Addr: addr.Resource, Provider: &provider, ProviderSchema: &providerSchema, Output: &state, }, &EvalRefresh{ Addr: addr.Resource, ProviderAddr: n.ResolvedProvider, Provider: &provider, ProviderSchema: &providerSchema, State: &state, Output: &state, }, &EvalWriteState{ Addr: addr.Resource, ProviderAddr: n.ResolvedProvider, ProviderSchema: &providerSchema, State: &state, }, }, } } // evalTreeManagedResourceNoState produces an EvalSequence for refresh resource // nodes that don't have state attached. An example of where this functionality // is useful is when a resource that already exists in state is being scaled // out, ie: has its resource count increased. In this case, the scaled out node // needs to be available to other nodes (namely data sources) that may depend // on it for proper interpolation, or confusing "index out of range" errors can // occur. // // The steps in this sequence are very similar to the steps carried out in // plan, but nothing is done with the diff after it is created - it is dropped, // and its changes are not counted in the UI. func (n *NodeRefreshableManagedResourceInstance) evalTreeManagedResourceNoState() EvalNode { addr := n.ResourceInstanceAddr() // Declare a bunch of variables that are used for state during // evaluation. Most of this are written to by-address below. var provider providers.Interface var providerSchema *ProviderSchema var change *plans.ResourceInstanceChange var state *states.ResourceInstanceObject return &EvalSequence{ Nodes: []EvalNode{ &EvalGetProvider{ Addr: n.ResolvedProvider, Output: &provider, Schema: &providerSchema, }, &EvalReadState{ Addr: addr.Resource, Provider: &provider, ProviderSchema: &providerSchema, Output: &state, }, &EvalDiff{ Addr: addr.Resource, Config: n.Config, Provider: &provider, ProviderAddr: n.ResolvedProvider, ProviderSchema: &providerSchema, State: &state, OutputChange: &change, OutputState: &state, Stub: true, }, &EvalWriteState{ Addr: addr.Resource, ProviderAddr: n.ResolvedProvider, ProviderSchema: &providerSchema, State: &state, }, // We must also save the planned change, so that expressions in // other nodes, such as provider configurations and data resources, // can work with the planned new value. // // This depends on the fact that Context.Refresh creates a // temporary new empty changeset for the duration of its graph // walk, and so this recorded change will be discarded immediately // after the refresh walk completes. &EvalWriteDiff{ Addr: addr.Resource, Change: &change, ProviderSchema: &providerSchema, }, }, } }