1package terraform
2
3import (
4	"github.com/hashicorp/terraform-plugin-sdk/internal/dag"
5	"github.com/hashicorp/terraform-plugin-sdk/internal/plans"
6	"github.com/hashicorp/terraform-plugin-sdk/internal/providers"
7	"github.com/hashicorp/terraform-plugin-sdk/internal/states"
8	"github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags"
9	"github.com/zclconf/go-cty/cty"
10)
11
12// NodeRefreshableDataResource represents a resource that is "refreshable".
13type NodeRefreshableDataResource struct {
14	*NodeAbstractResource
15}
16
17var (
18	_ GraphNodeSubPath              = (*NodeRefreshableDataResource)(nil)
19	_ GraphNodeDynamicExpandable    = (*NodeRefreshableDataResource)(nil)
20	_ GraphNodeReferenceable        = (*NodeRefreshableDataResource)(nil)
21	_ GraphNodeReferencer           = (*NodeRefreshableDataResource)(nil)
22	_ GraphNodeResource             = (*NodeRefreshableDataResource)(nil)
23	_ GraphNodeAttachResourceConfig = (*NodeRefreshableDataResource)(nil)
24)
25
26// GraphNodeDynamicExpandable
27func (n *NodeRefreshableDataResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
28	var diags tfdiags.Diagnostics
29
30	count, countKnown, countDiags := evaluateResourceCountExpressionKnown(n.Config.Count, ctx)
31	diags = diags.Append(countDiags)
32	if countDiags.HasErrors() {
33		return nil, diags.Err()
34	}
35	if !countKnown {
36		// If the count isn't known yet, we'll skip refreshing and try expansion
37		// again during the plan walk.
38		return nil, nil
39	}
40
41	forEachMap, forEachKnown, forEachDiags := evaluateResourceForEachExpressionKnown(n.Config.ForEach, ctx)
42	if forEachDiags.HasErrors() {
43		return nil, diags.Err()
44	}
45	if !forEachKnown {
46		// If the for_each isn't known yet, we'll skip refreshing and try expansion
47		// again during the plan walk.
48		return nil, nil
49	}
50
51	// Next we need to potentially rename an instance address in the state
52	// if we're transitioning whether "count" is set at all.
53	fixResourceCountSetTransition(ctx, n.ResourceAddr(), count != -1)
54
55	// Our graph transformers require access to the full state, so we'll
56	// temporarily lock it while we work on this.
57	state := ctx.State().Lock()
58	defer ctx.State().Unlock()
59
60	// The concrete resource factory we'll use
61	concreteResource := func(a *NodeAbstractResourceInstance) dag.Vertex {
62		// Add the config and state since we don't do that via transforms
63		a.Config = n.Config
64		a.ResolvedProvider = n.ResolvedProvider
65
66		return &NodeRefreshableDataResourceInstance{
67			NodeAbstractResourceInstance: a,
68		}
69	}
70
71	// We also need a destroyable resource for orphans that are a result of a
72	// scaled-in count.
73	concreteResourceDestroyable := func(a *NodeAbstractResourceInstance) dag.Vertex {
74		// Add the config and provider since we don't do that via transforms
75		a.Config = n.Config
76		a.ResolvedProvider = n.ResolvedProvider
77
78		return &NodeDestroyableDataResourceInstance{
79			NodeAbstractResourceInstance: a,
80		}
81	}
82
83	// Start creating the steps
84	steps := []GraphTransformer{
85		// Expand the count.
86		&ResourceCountTransformer{
87			Concrete: concreteResource,
88			Schema:   n.Schema,
89			Count:    count,
90			ForEach:  forEachMap,
91			Addr:     n.ResourceAddr(),
92		},
93
94		// Add the count orphans. As these are orphaned refresh nodes, we add them
95		// directly as NodeDestroyableDataResource.
96		&OrphanResourceCountTransformer{
97			Concrete: concreteResourceDestroyable,
98			Count:    count,
99			ForEach:  forEachMap,
100			Addr:     n.ResourceAddr(),
101			State:    state,
102		},
103
104		// Attach the state
105		&AttachStateTransformer{State: state},
106
107		// Targeting
108		&TargetsTransformer{Targets: n.Targets},
109
110		// Connect references so ordering is correct
111		&ReferenceTransformer{},
112
113		// Make sure there is a single root
114		&RootTransformer{},
115	}
116
117	// Build the graph
118	b := &BasicGraphBuilder{
119		Steps:    steps,
120		Validate: true,
121		Name:     "NodeRefreshableDataResource",
122	}
123
124	graph, diags := b.Build(ctx.Path())
125	return graph, diags.ErrWithWarnings()
126}
127
128// NodeRefreshableDataResourceInstance represents a single resource instance
129// that is refreshable.
130type NodeRefreshableDataResourceInstance struct {
131	*NodeAbstractResourceInstance
132}
133
134// GraphNodeEvalable
135func (n *NodeRefreshableDataResourceInstance) EvalTree() EvalNode {
136	addr := n.ResourceInstanceAddr()
137
138	// These variables are the state for the eval sequence below, and are
139	// updated through pointers.
140	var provider providers.Interface
141	var providerSchema *ProviderSchema
142	var change *plans.ResourceInstanceChange
143	var state *states.ResourceInstanceObject
144	var configVal cty.Value
145
146	return &EvalSequence{
147		Nodes: []EvalNode{
148			&EvalGetProvider{
149				Addr:   n.ResolvedProvider,
150				Output: &provider,
151				Schema: &providerSchema,
152			},
153
154			// Always destroy the existing state first, since we must
155			// make sure that values from a previous read will not
156			// get interpolated if we end up needing to defer our
157			// loading until apply time.
158			&EvalWriteState{
159				Addr:           addr.Resource,
160				ProviderAddr:   n.ResolvedProvider,
161				State:          &state, // a pointer to nil, here
162				ProviderSchema: &providerSchema,
163			},
164
165			&EvalIf{
166				If: func(ctx EvalContext) (bool, error) {
167					// If the config explicitly has a depends_on for this
168					// data source, assume the intention is to prevent
169					// refreshing ahead of that dependency, and therefore
170					// we need to deal with this resource during the apply
171					// phase..
172					if len(n.Config.DependsOn) > 0 {
173						return true, EvalEarlyExitError{}
174					}
175
176					return true, nil
177				},
178				Then: EvalNoop{},
179			},
180
181			// EvalReadData will _attempt_ to read the data source, but may
182			// generate an incomplete planned object if the configuration
183			// includes values that won't be known until apply.
184			&EvalReadData{
185				Addr:              addr.Resource,
186				Config:            n.Config,
187				Dependencies:      n.StateReferences(),
188				Provider:          &provider,
189				ProviderAddr:      n.ResolvedProvider,
190				ProviderSchema:    &providerSchema,
191				OutputChange:      &change,
192				OutputConfigValue: &configVal,
193				OutputState:       &state,
194			},
195
196			&EvalIf{
197				If: func(ctx EvalContext) (bool, error) {
198					return (*state).Status != states.ObjectPlanned, nil
199				},
200				Then: &EvalSequence{
201					Nodes: []EvalNode{
202						&EvalWriteState{
203							Addr:           addr.Resource,
204							ProviderAddr:   n.ResolvedProvider,
205							State:          &state,
206							ProviderSchema: &providerSchema,
207						},
208						&EvalUpdateStateHook{},
209					},
210				},
211				Else: &EvalSequence{
212					// We can't deal with this yet, so we'll repeat this step
213					// during the plan walk to produce a planned change to read
214					// this during the apply walk. However, we do still need to
215					// save the generated change and partial state so that
216					// results from it can be included in other data resources
217					// or provider configurations during the refresh walk.
218					// (The planned object we save in the state here will be
219					// pruned out at the end of the refresh walk, returning
220					// it back to being unset again for subsequent walks.)
221					Nodes: []EvalNode{
222						&EvalWriteDiff{
223							Addr:           addr.Resource,
224							Change:         &change,
225							ProviderSchema: &providerSchema,
226						},
227						&EvalWriteState{
228							Addr:           addr.Resource,
229							ProviderAddr:   n.ResolvedProvider,
230							State:          &state,
231							ProviderSchema: &providerSchema,
232						},
233					},
234				},
235			},
236		},
237	}
238}
239