1package plans
2
3import (
4	"sort"
5
6	"github.com/hashicorp/terraform/internal/addrs"
7	"github.com/hashicorp/terraform/internal/configs/configschema"
8	"github.com/hashicorp/terraform/internal/states"
9	"github.com/zclconf/go-cty/cty"
10)
11
12// Plan is the top-level type representing a planned set of changes.
13//
14// A plan is a summary of the set of changes required to move from a current
15// state to a goal state derived from configuration. The described changes
16// are not applied directly, but contain an approximation of the final
17// result that will be completed during apply by resolving any values that
18// cannot be predicted.
19//
20// A plan must always be accompanied by the configuration it was built from,
21// since the plan does not itself include all of the information required to
22// make the changes indicated.
23type Plan struct {
24	// Mode is the mode under which this plan was created.
25	//
26	// This is only recorded to allow for UI differences when presenting plans
27	// to the end-user, and so it must not be used to influence apply-time
28	// behavior. The actions during apply must be described entirely by
29	// the Changes field, regardless of how the plan was created.
30	UIMode Mode
31
32	VariableValues    map[string]DynamicValue
33	Changes           *Changes
34	TargetAddrs       []addrs.Targetable
35	ForceReplaceAddrs []addrs.AbsResourceInstance
36	ProviderSHA256s   map[string][]byte
37	Backend           Backend
38
39	// PrevRunState and PriorState both describe the situation that the plan
40	// was derived from:
41	//
42	// PrevRunState is a representation of the outcome of the previous
43	// Terraform operation, without any updates from the remote system but
44	// potentially including some changes that resulted from state upgrade
45	// actions.
46	//
47	// PriorState is a representation of the current state of remote objects,
48	// which will differ from PrevRunState if the "refresh" step returned
49	// different data, which might reflect drift.
50	//
51	// PriorState is the main snapshot we use for actions during apply.
52	// PrevRunState is only here so that we can diff PriorState against it in
53	// order to report to the user any out-of-band changes we've detected.
54	PrevRunState *states.State
55	PriorState   *states.State
56}
57
58// CanApply returns true if and only if the recieving plan includes content
59// that would make sense to apply. If it returns false, the plan operation
60// should indicate that there's nothing to do and Terraform should exit
61// without prompting the user to confirm the changes.
62//
63// This function represents our main business logic for making the decision
64// about whether a given plan represents meaningful "changes", and so its
65// exact definition may change over time; the intent is just to centralize the
66// rules for that rather than duplicating different versions of it at various
67// locations in the UI code.
68func (p *Plan) CanApply() bool {
69	switch {
70	case !p.Changes.Empty():
71		// "Empty" means that everything in the changes is a "NoOp", so if
72		// not empty then there's at least one non-NoOp change.
73		return true
74
75	case !p.PriorState.ManagedResourcesEqual(p.PrevRunState):
76		// If there are no changes planned but we detected some
77		// outside-Terraform changes while refreshing then we consider
78		// that applyable in isolation only if this was a refresh-only
79		// plan where we expect updating the state to include these
80		// changes was the intended goal.
81		//
82		// (We don't treat a "refresh only" plan as applyable in normal
83		// planning mode because historically the refresh result wasn't
84		// considered part of a plan at all, and so it would be
85		// a disruptive breaking change if refreshing alone suddenly
86		// became applyable in the normal case and an existing configuration
87		// was relying on ignore_changes in order to be convergent in spite
88		// of intentional out-of-band operations.)
89		return p.UIMode == RefreshOnlyMode
90
91	default:
92		// Otherwise, there are either no changes to apply or they are changes
93		// our cases above don't consider as worthy of applying in isolation.
94		return false
95	}
96}
97
98// ProviderAddrs returns a list of all of the provider configuration addresses
99// referenced throughout the receiving plan.
100//
101// The result is de-duplicated so that each distinct address appears only once.
102func (p *Plan) ProviderAddrs() []addrs.AbsProviderConfig {
103	if p == nil || p.Changes == nil {
104		return nil
105	}
106
107	m := map[string]addrs.AbsProviderConfig{}
108	for _, rc := range p.Changes.Resources {
109		m[rc.ProviderAddr.String()] = rc.ProviderAddr
110	}
111	if len(m) == 0 {
112		return nil
113	}
114
115	// This is mainly just so we'll get stable results for testing purposes.
116	keys := make([]string, 0, len(m))
117	for k := range m {
118		keys = append(keys, k)
119	}
120	sort.Strings(keys)
121
122	ret := make([]addrs.AbsProviderConfig, len(keys))
123	for i, key := range keys {
124		ret[i] = m[key]
125	}
126
127	return ret
128}
129
130// Backend represents the backend-related configuration and other data as it
131// existed when a plan was created.
132type Backend struct {
133	// Type is the type of backend that the plan will apply against.
134	Type string
135
136	// Config is the configuration of the backend, whose schema is decided by
137	// the backend Type.
138	Config DynamicValue
139
140	// Workspace is the name of the workspace that was active when the plan
141	// was created. It is illegal to apply a plan created for one workspace
142	// to the state of another workspace.
143	// (This constraint is already enforced by the statefile lineage mechanism,
144	// but storing this explicitly allows us to return a better error message
145	// in the situation where the user has the wrong workspace selected.)
146	Workspace string
147}
148
149func NewBackend(typeName string, config cty.Value, configSchema *configschema.Block, workspaceName string) (*Backend, error) {
150	dv, err := NewDynamicValue(config, configSchema.ImpliedType())
151	if err != nil {
152		return nil, err
153	}
154
155	return &Backend{
156		Type:      typeName,
157		Config:    dv,
158		Workspace: workspaceName,
159	}, nil
160}
161