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