1package configs
2
3import (
4	"fmt"
5
6	"github.com/hashicorp/hcl/v2"
7)
8
9// Provisioner represents a "provisioner" block when used within a
10// "resource" block in a module or file.
11type Provisioner struct {
12	Type       string
13	Config     hcl.Body
14	Connection *Connection
15	When       ProvisionerWhen
16	OnFailure  ProvisionerOnFailure
17
18	DeclRange hcl.Range
19	TypeRange hcl.Range
20}
21
22func decodeProvisionerBlock(block *hcl.Block) (*Provisioner, hcl.Diagnostics) {
23	pv := &Provisioner{
24		Type:      block.Labels[0],
25		TypeRange: block.LabelRanges[0],
26		DeclRange: block.DefRange,
27		When:      ProvisionerWhenCreate,
28		OnFailure: ProvisionerOnFailureFail,
29	}
30
31	content, config, diags := block.Body.PartialContent(provisionerBlockSchema)
32	pv.Config = config
33
34	switch pv.Type {
35	case "chef", "habitat", "puppet", "salt-masterless":
36		diags = append(diags, &hcl.Diagnostic{
37			Severity: hcl.DiagError,
38			Summary:  fmt.Sprintf("The \"%s\" provisioner has been removed", pv.Type),
39			Detail:   fmt.Sprintf("The \"%s\" provisioner was deprecated in Terraform 0.13.4 has been removed from Terraform. Visit https://learn.hashicorp.com/collections/terraform/provision for alternatives to using provisioners that are a better fit for the Terraform workflow.", pv.Type),
40			Subject:  &pv.TypeRange,
41		})
42		return nil, diags
43	}
44
45	if attr, exists := content.Attributes["when"]; exists {
46		expr, shimDiags := shimTraversalInString(attr.Expr, true)
47		diags = append(diags, shimDiags...)
48
49		switch hcl.ExprAsKeyword(expr) {
50		case "create":
51			pv.When = ProvisionerWhenCreate
52		case "destroy":
53			pv.When = ProvisionerWhenDestroy
54		default:
55			diags = append(diags, &hcl.Diagnostic{
56				Severity: hcl.DiagError,
57				Summary:  "Invalid \"when\" keyword",
58				Detail:   "The \"when\" argument requires one of the following keywords: create or destroy.",
59				Subject:  expr.Range().Ptr(),
60			})
61		}
62	}
63
64	// destroy provisioners can only refer to self
65	if pv.When == ProvisionerWhenDestroy {
66		diags = append(diags, onlySelfRefs(config)...)
67	}
68
69	if attr, exists := content.Attributes["on_failure"]; exists {
70		expr, shimDiags := shimTraversalInString(attr.Expr, true)
71		diags = append(diags, shimDiags...)
72
73		switch hcl.ExprAsKeyword(expr) {
74		case "continue":
75			pv.OnFailure = ProvisionerOnFailureContinue
76		case "fail":
77			pv.OnFailure = ProvisionerOnFailureFail
78		default:
79			diags = append(diags, &hcl.Diagnostic{
80				Severity: hcl.DiagError,
81				Summary:  "Invalid \"on_failure\" keyword",
82				Detail:   "The \"on_failure\" argument requires one of the following keywords: continue or fail.",
83				Subject:  attr.Expr.Range().Ptr(),
84			})
85		}
86	}
87
88	var seenConnection *hcl.Block
89	var seenEscapeBlock *hcl.Block
90	for _, block := range content.Blocks {
91		switch block.Type {
92		case "_":
93			if seenEscapeBlock != nil {
94				diags = append(diags, &hcl.Diagnostic{
95					Severity: hcl.DiagError,
96					Summary:  "Duplicate escaping block",
97					Detail: fmt.Sprintf(
98						"The special block type \"_\" can be used to force particular arguments to be interpreted as provisioner-typpe-specific rather than as meta-arguments, but each provisioner block can have only one such block. The first escaping block was at %s.",
99						seenEscapeBlock.DefRange,
100					),
101					Subject: &block.DefRange,
102				})
103				continue
104			}
105			seenEscapeBlock = block
106
107			// When there's an escaping block its content merges with the
108			// existing config we extracted earlier, so later decoding
109			// will see a blend of both.
110			pv.Config = hcl.MergeBodies([]hcl.Body{pv.Config, block.Body})
111
112		case "connection":
113			if seenConnection != nil {
114				diags = append(diags, &hcl.Diagnostic{
115					Severity: hcl.DiagError,
116					Summary:  "Duplicate connection block",
117					Detail:   fmt.Sprintf("This provisioner already has a connection block at %s.", seenConnection.DefRange),
118					Subject:  &block.DefRange,
119				})
120				continue
121			}
122			seenConnection = block
123
124			// destroy provisioners can only refer to self
125			if pv.When == ProvisionerWhenDestroy {
126				diags = append(diags, onlySelfRefs(block.Body)...)
127			}
128
129			pv.Connection = &Connection{
130				Config:    block.Body,
131				DeclRange: block.DefRange,
132			}
133
134		default:
135			// Any other block types are ones we've reserved for future use,
136			// so they get a generic message.
137			diags = append(diags, &hcl.Diagnostic{
138				Severity: hcl.DiagError,
139				Summary:  "Reserved block type name in provisioner block",
140				Detail:   fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type),
141				Subject:  &block.TypeRange,
142			})
143		}
144	}
145
146	return pv, diags
147}
148
149func onlySelfRefs(body hcl.Body) hcl.Diagnostics {
150	var diags hcl.Diagnostics
151
152	// Provisioners currently do not use any blocks in their configuration.
153	// Blocks are likely to remain solely for meta parameters, but in the case
154	// that blocks are supported for provisioners, we will want to extend this
155	// to find variables in nested blocks.
156	attrs, _ := body.JustAttributes()
157	for _, attr := range attrs {
158		for _, v := range attr.Expr.Variables() {
159			valid := false
160			switch v.RootName() {
161			case "self", "path", "terraform":
162				valid = true
163			case "count":
164				// count must use "index"
165				if len(v) == 2 {
166					if t, ok := v[1].(hcl.TraverseAttr); ok && t.Name == "index" {
167						valid = true
168					}
169				}
170
171			case "each":
172				if len(v) == 2 {
173					if t, ok := v[1].(hcl.TraverseAttr); ok && t.Name == "key" {
174						valid = true
175					}
176				}
177			}
178
179			if !valid {
180				diags = append(diags, &hcl.Diagnostic{
181					Severity: hcl.DiagError,
182					Summary:  "Invalid reference from destroy provisioner",
183					Detail: "Destroy-time provisioners and their connection configurations may only " +
184						"reference attributes of the related resource, via 'self', 'count.index', " +
185						"or 'each.key'.\n\nReferences to other resources during the destroy phase " +
186						"can cause dependency cycles and interact poorly with create_before_destroy.",
187					Subject: attr.Expr.Range().Ptr(),
188				})
189			}
190		}
191	}
192	return diags
193}
194
195// Connection represents a "connection" block when used within either a
196// "resource" or "provisioner" block in a module or file.
197type Connection struct {
198	Config hcl.Body
199
200	DeclRange hcl.Range
201}
202
203// ProvisionerWhen is an enum for valid values for when to run provisioners.
204type ProvisionerWhen int
205
206//go:generate go run golang.org/x/tools/cmd/stringer -type ProvisionerWhen
207
208const (
209	ProvisionerWhenInvalid ProvisionerWhen = iota
210	ProvisionerWhenCreate
211	ProvisionerWhenDestroy
212)
213
214// ProvisionerOnFailure is an enum for valid values for on_failure options
215// for provisioners.
216type ProvisionerOnFailure int
217
218//go:generate go run golang.org/x/tools/cmd/stringer -type ProvisionerOnFailure
219
220const (
221	ProvisionerOnFailureInvalid ProvisionerOnFailure = iota
222	ProvisionerOnFailureContinue
223	ProvisionerOnFailureFail
224)
225
226var provisionerBlockSchema = &hcl.BodySchema{
227	Attributes: []hcl.AttributeSchema{
228		{Name: "when"},
229		{Name: "on_failure"},
230	},
231	Blocks: []hcl.BlockHeaderSchema{
232		{Type: "_"}, // meta-argument escaping block
233
234		{Type: "connection"},
235		{Type: "lifecycle"}, // reserved for future use
236	},
237}
238