Skip to content

Commit de4a04e

Browse files
authored
Merge pull request #368 from hashicorp/diagnostics
Diagnostics Proposal
2 parents 53afb31 + ebe4720 commit de4a04e

File tree

14 files changed

+1046
-425
lines changed

14 files changed

+1046
-425
lines changed

diag/diagnostic.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package diag
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/hashicorp/go-cty/cty"
8+
)
9+
10+
// Diagnostics is a collection of Diagnostic.
11+
//
12+
// Developers should append and build the list of diagnostics up until a fatal
13+
// error is reached, at which point they should return the Diagnostics to the
14+
// SDK.
15+
type Diagnostics []Diagnostic
16+
17+
// HasError returns true is Diagnostics contains an instance of
18+
// Severity == Error.
19+
//
20+
// This helper aims to mimic the go error practices of if err != nil. After any
21+
// operation that returns Diagnostics, check that it HasError and bubble up the
22+
// stack.
23+
func (diags Diagnostics) HasError() bool {
24+
for i := range diags {
25+
if diags[i].Severity == Error {
26+
return true
27+
}
28+
}
29+
return false
30+
}
31+
32+
// Diagnostic is a contextual message intended at outlining problems in user
33+
// configuration.
34+
//
35+
// It supports multiple levels of severity (Error or Warning), a short Summary
36+
// of the problem, an optional longer Detail message that can assist the user in
37+
// fixing the problem, as well as an AttributePath representation which
38+
// Terraform uses to indicate where the issue took place in the user's
39+
// configuration.
40+
//
41+
// A Diagnostic will typically be used to pinpoint a problem with user
42+
// configuration, however it can still be used to present warnings or errors
43+
// to the user without any AttributePath set.
44+
type Diagnostic struct {
45+
// Severity indicates the level of the Diagnostic. Currently can be set to
46+
// either Error or Warning
47+
Severity Severity
48+
49+
// Summary is a short description of the problem, rendered above location
50+
// information
51+
Summary string
52+
53+
// Detail is an optional second message rendered below location information
54+
// typically used to communicate a potential fix to the user.
55+
Detail string
56+
57+
// AttributePath is a representation of the path starting from the root of
58+
// block (resource, datasource, provider) under evaluation by the SDK, to
59+
// the attribute that the Diagnostic should be associated to. Terraform will
60+
// use this information to render information on where the problem took
61+
// place in the user's configuration.
62+
//
63+
// It is represented with cty.Path, which is a list of steps of either
64+
// cty.GetAttrStep (an actual attribute) or cty.IndexStep (a step with Key
65+
// of cty.StringVal for map indexes, and cty.NumberVal for list indexes).
66+
//
67+
// PLEASE NOTE: While cty can support indexing into sets, the SDK and
68+
// protocol currently do not. For any Diagnostic related to a schema.TypeSet
69+
// or a child of that type, please terminate the path at the schema.TypeSet
70+
// and opt for more verbose Summary and Detail to help guide the user.
71+
//
72+
// Validity of the AttributePath is currently the responsibility of the
73+
// developer, Terraform should render the root block (provider, resource,
74+
// datasource) in cases where the attribute path is invalid.
75+
AttributePath cty.Path
76+
}
77+
78+
// Validate ensures a valid Severity and a non-empty Summary are set.
79+
func (d Diagnostic) Validate() error {
80+
var validSev bool
81+
for _, sev := range severities {
82+
if d.Severity == sev {
83+
validSev = true
84+
break
85+
}
86+
}
87+
if !validSev {
88+
return fmt.Errorf("invalid severity: %v", d.Severity)
89+
}
90+
if d.Summary == "" {
91+
return errors.New("empty summary")
92+
}
93+
return nil
94+
}
95+
96+
// FromErr will convert an error into a Diagnostic
97+
func FromErr(err error) Diagnostic {
98+
return Diagnostic{
99+
Severity: Error,
100+
Summary: err.Error(),
101+
}
102+
}
103+
104+
// Severity is an enum type marking the severity level of a Diagnostic
105+
type Severity int
106+
107+
const (
108+
Error Severity = iota
109+
Warning
110+
)
111+
112+
var severities = []Severity{Error, Warning}

helper/schema/diagnostic_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package schema
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/hashicorp/go-multierror"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
9+
)
10+
11+
type errorDiags diag.Diagnostics
12+
13+
func (diags errorDiags) Errors() []error {
14+
var es []error
15+
for i := range diags {
16+
if diags[i].Severity == diag.Error {
17+
s := fmt.Sprintf("Error: %s", diags[i].Summary)
18+
if diags[i].Detail != "" {
19+
s = fmt.Sprintf("%s: %s", s, diags[i].Detail)
20+
}
21+
es = append(es, errors.New(s))
22+
}
23+
}
24+
return es
25+
}
26+
27+
func (diags errorDiags) Error() string {
28+
return multierror.ListFormatFunc(diags.Errors())
29+
}
30+
31+
type warningDiags diag.Diagnostics
32+
33+
func (diags warningDiags) Warnings() []string {
34+
var ws []string
35+
for i := range diags {
36+
if diags[i].Severity == diag.Warning {
37+
s := fmt.Sprintf("Warning: %s", diags[i].Summary)
38+
if diags[i].Detail != "" {
39+
s = fmt.Sprintf("%s: %s", s, diags[i].Detail)
40+
}
41+
ws = append(ws, s)
42+
}
43+
}
44+
return ws
45+
}

helper/schema/provider.go

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/hashicorp/go-multierror"
1111

12+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1213
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/configschema"
1314
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
1415
)
@@ -58,7 +59,9 @@ type Provider struct {
5859
ConfigureFunc ConfigureFunc
5960

6061
// ConfigureContextFunc is a function for configuring the provider. If the
61-
// provider doesn't need to be configured, this can be omitted.
62+
// provider doesn't need to be configured, this can be omitted. This function
63+
// receives a context.Context that will cancel when Terraform sends a
64+
// cancellation signal. This function can yield Diagnostics
6265
ConfigureContextFunc ConfigureContextFunc
6366

6467
meta interface{}
@@ -77,7 +80,7 @@ type ConfigureFunc func(*ResourceData) (interface{}, error)
7780
// the subsequent resources as the meta parameter. This return value is
7881
// usually used to pass along a configured API client, a configuration
7982
// structure, etc.
80-
type ConfigureContextFunc func(context.Context, *ResourceData) (interface{}, error)
83+
type ConfigureContextFunc func(context.Context, *ResourceData) (interface{}, diag.Diagnostics)
8184

8285
// InternalValidate should be called to validate the structure
8386
// of the provider.
@@ -179,29 +182,32 @@ func (p *Provider) GetSchema(req *terraform.ProviderSchemaRequest) (*terraform.P
179182
}
180183

181184
// Validate is called once at the beginning with the raw configuration
182-
// (no interpolation done) and can return a list of warnings and/or
183-
// errors.
185+
// (no interpolation done) and can return diagnostics
184186
//
185187
// This is called once with the provider configuration only. It may not
186188
// be called at all if no provider configuration is given.
187189
//
188190
// This should not assume that any values of the configurations are valid.
189191
// The primary use case of this call is to check that required keys are
190192
// set.
191-
func (p *Provider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
193+
func (p *Provider) Validate(c *terraform.ResourceConfig) diag.Diagnostics {
192194
if err := p.InternalValidate(); err != nil {
193-
return nil, []error{fmt.Errorf(
194-
"Internal validation of the provider failed! This is always a bug\n"+
195-
"with the provider itself, and not a user issue. Please report\n"+
196-
"this bug:\n\n%s", err)}
195+
return []diag.Diagnostic{
196+
{
197+
Severity: diag.Error,
198+
Summary: "InternalValidate",
199+
Detail: fmt.Sprintf("Internal validation of the provider failed! This is always a bug\n"+
200+
"with the provider itself, and not a user issue. Please report\n"+
201+
"this bug:\n\n%s", err),
202+
},
203+
}
197204
}
198205

199206
return schemaMap(p.Schema).Validate(c)
200207
}
201208

202209
// ValidateResource is called once at the beginning with the raw
203-
// configuration (no interpolation done) and can return a list of warnings
204-
// and/or errors.
210+
// configuration (no interpolation done) and can return diagnostics.
205211
//
206212
// This is called once per resource.
207213
//
@@ -210,11 +216,15 @@ func (p *Provider) Validate(c *terraform.ResourceConfig) ([]string, []error) {
210216
// The primary use case of this call is to check that the required keys
211217
// are set and that the general structure is correct.
212218
func (p *Provider) ValidateResource(
213-
t string, c *terraform.ResourceConfig) ([]string, []error) {
219+
t string, c *terraform.ResourceConfig) diag.Diagnostics {
214220
r, ok := p.ResourcesMap[t]
215221
if !ok {
216-
return nil, []error{fmt.Errorf(
217-
"Provider doesn't support resource: %s", t)}
222+
return []diag.Diagnostic{
223+
{
224+
Severity: diag.Error,
225+
Summary: fmt.Sprintf("Provider doesn't support resource: %s", t),
226+
},
227+
}
218228
}
219229

220230
return r.Validate(c)
@@ -224,12 +234,13 @@ func (p *Provider) ValidateResource(
224234
// given. This is useful for setting things like access keys.
225235
//
226236
// This won't be called at all if no provider configuration is given.
227-
//
228-
// Configure returns an error if it occurred.
229-
func (p *Provider) Configure(ctx context.Context, c *terraform.ResourceConfig) error {
237+
func (p *Provider) Configure(ctx context.Context, c *terraform.ResourceConfig) diag.Diagnostics {
238+
239+
var diags diag.Diagnostics
240+
230241
// No configuration
231242
if p.ConfigureFunc == nil && p.ConfigureContextFunc == nil {
232-
return nil
243+
return diags
233244
}
234245

235246
sm := schemaMap(p.Schema)
@@ -238,26 +249,31 @@ func (p *Provider) Configure(ctx context.Context, c *terraform.ResourceConfig) e
238249
// generate an intermediary "diff" although that is never exposed.
239250
diff, err := sm.Diff(ctx, nil, c, nil, p.meta, true)
240251
if err != nil {
241-
return err
252+
return append(diags, diag.FromErr(err))
242253
}
243254

244255
data, err := sm.Data(nil, diff)
245256
if err != nil {
246-
return err
257+
return append(diags, diag.FromErr(err))
247258
}
248259

249-
var meta interface{}
250-
if p.ConfigureContextFunc != nil {
251-
meta, err = p.ConfigureContextFunc(ctx, data)
252-
} else {
253-
meta, err = p.ConfigureFunc(data)
260+
if p.ConfigureFunc != nil {
261+
meta, err := p.ConfigureFunc(data)
262+
if err != nil {
263+
return append(diags, diag.FromErr(err))
264+
}
265+
p.meta = meta
254266
}
255-
if err != nil {
256-
return err
267+
if p.ConfigureContextFunc != nil {
268+
meta, ds := p.ConfigureContextFunc(ctx, data)
269+
diags = append(diags, ds...)
270+
if diags.HasError() {
271+
return diags
272+
}
273+
p.meta = meta
257274
}
258275

259-
p.meta = meta
260-
return nil
276+
return diags
261277
}
262278

263279
// Resources returns all the available resource types that this provider
@@ -361,8 +377,7 @@ func (p *Provider) ImportState(
361377
}
362378

363379
// ValidateDataSource is called once at the beginning with the raw
364-
// configuration (no interpolation done) and can return a list of warnings
365-
// and/or errors.
380+
// configuration (no interpolation done) and can return diagnostics.
366381
//
367382
// This is called once per data source instance.
368383
//
@@ -371,11 +386,15 @@ func (p *Provider) ImportState(
371386
// The primary use case of this call is to check that the required keys
372387
// are set and that the general structure is correct.
373388
func (p *Provider) ValidateDataSource(
374-
t string, c *terraform.ResourceConfig) ([]string, []error) {
389+
t string, c *terraform.ResourceConfig) diag.Diagnostics {
375390
r, ok := p.DataSourcesMap[t]
376391
if !ok {
377-
return nil, []error{fmt.Errorf(
378-
"Provider doesn't support data source: %s", t)}
392+
return []diag.Diagnostic{
393+
{
394+
Severity: diag.Error,
395+
Summary: fmt.Sprintf("Provider doesn't support data source: %s", t),
396+
},
397+
}
379398
}
380399

381400
return r.Validate(c)

0 commit comments

Comments
 (0)