1package schema
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"sync"
8
9	"github.com/hashicorp/go-multierror"
10	"github.com/hashicorp/terraform/config"
11	"github.com/hashicorp/terraform/configs/configschema"
12	"github.com/hashicorp/terraform/terraform"
13)
14
15// Provisioner represents a resource provisioner in Terraform and properly
16// implements all of the ResourceProvisioner API.
17//
18// This higher level structure makes it much easier to implement a new or
19// custom provisioner for Terraform.
20//
21// The function callbacks for this structure are all passed a context object.
22// This context object has a number of pre-defined values that can be accessed
23// via the global functions defined in context.go.
24type Provisioner struct {
25	// ConnSchema is the schema for the connection settings for this
26	// provisioner.
27	//
28	// The keys of this map are the configuration keys, and the value is
29	// the schema describing the value of the configuration.
30	//
31	// NOTE: The value of connection keys can only be strings for now.
32	ConnSchema map[string]*Schema
33
34	// Schema is the schema for the usage of this provisioner.
35	//
36	// The keys of this map are the configuration keys, and the value is
37	// the schema describing the value of the configuration.
38	Schema map[string]*Schema
39
40	// ApplyFunc is the function for executing the provisioner. This is required.
41	// It is given a context. See the Provisioner struct docs for more
42	// information.
43	ApplyFunc func(ctx context.Context) error
44
45	// ValidateFunc is a function for extended validation. This is optional
46	// and should be used when individual field validation is not enough.
47	ValidateFunc func(*terraform.ResourceConfig) ([]string, []error)
48
49	stopCtx       context.Context
50	stopCtxCancel context.CancelFunc
51	stopOnce      sync.Once
52}
53
54// Keys that can be used to access data in the context parameters for
55// Provisioners.
56var (
57	connDataInvalid = contextKey("data invalid")
58
59	// This returns a *ResourceData for the connection information.
60	// Guaranteed to never be nil.
61	ProvConnDataKey = contextKey("provider conn data")
62
63	// This returns a *ResourceData for the config information.
64	// Guaranteed to never be nil.
65	ProvConfigDataKey = contextKey("provider config data")
66
67	// This returns a terraform.UIOutput. Guaranteed to never be nil.
68	ProvOutputKey = contextKey("provider output")
69
70	// This returns the raw InstanceState passed to Apply. Guaranteed to
71	// be set, but may be nil.
72	ProvRawStateKey = contextKey("provider raw state")
73)
74
75// InternalValidate should be called to validate the structure
76// of the provisioner.
77//
78// This should be called in a unit test to verify before release that this
79// structure is properly configured for use.
80func (p *Provisioner) InternalValidate() error {
81	if p == nil {
82		return errors.New("provisioner is nil")
83	}
84
85	var validationErrors error
86	{
87		sm := schemaMap(p.ConnSchema)
88		if err := sm.InternalValidate(sm); err != nil {
89			validationErrors = multierror.Append(validationErrors, err)
90		}
91	}
92
93	{
94		sm := schemaMap(p.Schema)
95		if err := sm.InternalValidate(sm); err != nil {
96			validationErrors = multierror.Append(validationErrors, err)
97		}
98	}
99
100	if p.ApplyFunc == nil {
101		validationErrors = multierror.Append(validationErrors, fmt.Errorf(
102			"ApplyFunc must not be nil"))
103	}
104
105	return validationErrors
106}
107
108// StopContext returns a context that checks whether a provisioner is stopped.
109func (p *Provisioner) StopContext() context.Context {
110	p.stopOnce.Do(p.stopInit)
111	return p.stopCtx
112}
113
114func (p *Provisioner) stopInit() {
115	p.stopCtx, p.stopCtxCancel = context.WithCancel(context.Background())
116}
117
118// Stop implementation of terraform.ResourceProvisioner interface.
119func (p *Provisioner) Stop() error {
120	p.stopOnce.Do(p.stopInit)
121	p.stopCtxCancel()
122	return nil
123}
124
125// GetConfigSchema implementation of terraform.ResourceProvisioner interface.
126func (p *Provisioner) GetConfigSchema() (*configschema.Block, error) {
127	return schemaMap(p.Schema).CoreConfigSchema(), nil
128}
129
130// Apply implementation of terraform.ResourceProvisioner interface.
131func (p *Provisioner) Apply(
132	o terraform.UIOutput,
133	s *terraform.InstanceState,
134	c *terraform.ResourceConfig) error {
135	var connData, configData *ResourceData
136
137	{
138		// We first need to turn the connection information into a
139		// terraform.ResourceConfig so that we can use that type to more
140		// easily build a ResourceData structure. We do this by simply treating
141		// the conn info as configuration input.
142		raw := make(map[string]interface{})
143		if s != nil {
144			for k, v := range s.Ephemeral.ConnInfo {
145				raw[k] = v
146			}
147		}
148
149		c, err := config.NewRawConfig(raw)
150		if err != nil {
151			return err
152		}
153
154		sm := schemaMap(p.ConnSchema)
155		diff, err := sm.Diff(nil, terraform.NewResourceConfig(c), nil, nil, true)
156		if err != nil {
157			return err
158		}
159		connData, err = sm.Data(nil, diff)
160		if err != nil {
161			return err
162		}
163	}
164
165	{
166		// Build the configuration data. Doing this requires making a "diff"
167		// even though that's never used. We use that just to get the correct types.
168		configMap := schemaMap(p.Schema)
169		diff, err := configMap.Diff(nil, c, nil, nil, true)
170		if err != nil {
171			return err
172		}
173		configData, err = configMap.Data(nil, diff)
174		if err != nil {
175			return err
176		}
177	}
178
179	// Build the context and call the function
180	ctx := p.StopContext()
181	ctx = context.WithValue(ctx, ProvConnDataKey, connData)
182	ctx = context.WithValue(ctx, ProvConfigDataKey, configData)
183	ctx = context.WithValue(ctx, ProvOutputKey, o)
184	ctx = context.WithValue(ctx, ProvRawStateKey, s)
185	return p.ApplyFunc(ctx)
186}
187
188// Validate implements the terraform.ResourceProvisioner interface.
189func (p *Provisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
190	if err := p.InternalValidate(); err != nil {
191		return nil, []error{fmt.Errorf(
192			"Internal validation of the provisioner failed! This is always a bug\n"+
193				"with the provisioner itself, and not a user issue. Please report\n"+
194				"this bug:\n\n%s", err)}
195	}
196
197	if p.Schema != nil {
198		w, e := schemaMap(p.Schema).Validate(c)
199		ws = append(ws, w...)
200		es = append(es, e...)
201	}
202
203	if p.ValidateFunc != nil {
204		w, e := p.ValidateFunc(c)
205		ws = append(ws, w...)
206		es = append(es, e...)
207	}
208
209	return ws, es
210}
211