Skip to content

Commit e6784cf

Browse files
authored
Merge pull request #551 from ulucinar/diag-err
Add terraform/errors.FrameworkDiagnosticsError
2 parents 955d943 + 4036419 commit e6784cf

3 files changed

Lines changed: 211 additions & 17 deletions

File tree

pkg/controller/external_tfpluginfw.go

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"github.com/crossplane/crossplane-runtime/v2/pkg/meta"
2020
"github.com/crossplane/crossplane-runtime/v2/pkg/reconciler/managed"
2121
xpresource "github.com/crossplane/crossplane-runtime/v2/pkg/resource"
22-
fwdiag "github.com/hashicorp/terraform-plugin-framework/diag"
2322
fwprovider "github.com/hashicorp/terraform-plugin-framework/provider"
2423
"github.com/hashicorp/terraform-plugin-framework/providerserver"
2524
fwresource "github.com/hashicorp/terraform-plugin-framework/resource"
@@ -36,6 +35,7 @@ import (
3635
"github.com/crossplane/upjet/v2/pkg/resource"
3736
upjson "github.com/crossplane/upjet/v2/pkg/resource/json"
3837
"github.com/crossplane/upjet/v2/pkg/terraform"
38+
tferrors "github.com/crossplane/upjet/v2/pkg/terraform/errors"
3939
)
4040

4141
// TerraformPluginFrameworkConnector is an external client, with credentials and
@@ -249,8 +249,7 @@ func (c *TerraformPluginFrameworkConnector) getResourceSchema(ctx context.Contex
249249
schemaResp := &fwresource.SchemaResponse{}
250250
res.Schema(ctx, fwresource.SchemaRequest{}, schemaResp)
251251
if schemaResp.Diagnostics.HasError() {
252-
fwErrors := frameworkDiagnosticsToString(schemaResp.Diagnostics)
253-
return rschema.Schema{}, errors.Errorf("could not retrieve resource schema: %s", fwErrors)
252+
return rschema.Schema{}, tferrors.FrameworkDiagnosticsError("could not retrieve resource schema", schemaResp.Diagnostics)
254253
}
255254

256255
return schemaResp.Schema, nil
@@ -269,8 +268,7 @@ func (c *TerraformPluginFrameworkConnector) configureProvider(ctx context.Contex
269268
var schemaResp fwprovider.SchemaResponse
270269
ts.FrameworkProvider.Schema(ctx, fwprovider.SchemaRequest{}, &schemaResp)
271270
if schemaResp.Diagnostics.HasError() {
272-
fwDiags := frameworkDiagnosticsToString(schemaResp.Diagnostics)
273-
return nil, fmt.Errorf("cannot retrieve provider schema: %s", fwDiags)
271+
return nil, tferrors.FrameworkDiagnosticsError("cannot retrieve provider schema", schemaResp.Diagnostics)
274272
}
275273
providerServer := providerserver.NewProtocol6(ts.FrameworkProvider)()
276274

@@ -905,18 +903,6 @@ func getFatalDiagnostics(diags []*tfprotov6.Diagnostic) error {
905903
return errs
906904
}
907905

908-
// frameworkDiagnosticsToString constructs an error string from the provided
909-
// Plugin Framework diagnostics instance. Only Error severity diagnostics are
910-
// included.
911-
func frameworkDiagnosticsToString(fwdiags fwdiag.Diagnostics) string {
912-
frameworkErrorDiags := fwdiags.Errors()
913-
diagErrors := make([]string, 0, len(frameworkErrorDiags))
914-
for _, tfdiag := range frameworkErrorDiags {
915-
diagErrors = append(diagErrors, fmt.Sprintf("%s: %s", tfdiag.Summary(), tfdiag.Detail()))
916-
}
917-
return strings.Join(diagErrors, "\n")
918-
}
919-
920906
// protov6DynamicValueFromMap constructs a protov6 DynamicValue given the
921907
// map[string]any using the terraform type as reference.
922908
func protov6DynamicValueFromMap(data map[string]any, terraformType tftypes.Type) (*tfprotov6.DynamicValue, error) {

pkg/terraform/errors/fw_diag.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-FileCopyrightText: 2025 The Crossplane Authors <https://crossplane.io>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package errors
6+
7+
import (
8+
"strings"
9+
10+
"github.com/crossplane/crossplane-runtime/v2/pkg/errors"
11+
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
)
13+
14+
// FrameworkDiagnosticsError returns an error representing
15+
// the collected diagnostics at SeverityError level.
16+
func FrameworkDiagnosticsError(parentMessage string, diags diag.Diagnostics) error {
17+
eds := diags.Errors()
18+
if len(eds) == 0 {
19+
return nil
20+
}
21+
22+
errs := make([]error, 0, len(eds)+1)
23+
errs = append(errs, errors.New(parentMessage))
24+
for _, d := range eds {
25+
errs = append(errs, errors.New(frameworkDiagnosticString(d)))
26+
}
27+
return errors.Join(errs...)
28+
}
29+
30+
// frameworkDiagnosticString formats the given framework Diagnostic
31+
// as a string <summary>[: <detail>][: <path>].
32+
func frameworkDiagnosticString(d diag.Diagnostic) string {
33+
var msg strings.Builder
34+
msg.WriteString(strings.TrimSpace(d.Summary()))
35+
36+
detail := strings.TrimSpace(d.Detail())
37+
if detail != "" {
38+
msg.WriteString(": ")
39+
msg.WriteString(detail)
40+
}
41+
42+
if p, ok := d.(diag.DiagnosticWithPath); ok {
43+
wp := strings.TrimSpace(p.Path().String())
44+
if wp != "" {
45+
msg.WriteString(": ")
46+
msg.WriteString(wp)
47+
}
48+
}
49+
50+
return msg.String()
51+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// SPDX-FileCopyrightText: 2025 The Crossplane Authors <https://crossplane.io>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package errors
6+
7+
import (
8+
"testing"
9+
10+
xperrors "github.com/crossplane/crossplane-runtime/v2/pkg/errors"
11+
"github.com/crossplane/crossplane-runtime/v2/pkg/test"
12+
"github.com/google/go-cmp/cmp"
13+
fwdiag "github.com/hashicorp/terraform-plugin-framework/diag"
14+
fwpath "github.com/hashicorp/terraform-plugin-framework/path"
15+
)
16+
17+
// fakeDiag is a minimal implementation of the framework diag.Diagnostic
18+
// interface sufficient for testing frameworkDiagnosticString.
19+
type fakeDiag struct {
20+
summary string
21+
detail string
22+
}
23+
24+
func (f fakeDiag) Severity() fwdiag.Severity { return fwdiag.SeverityError }
25+
func (f fakeDiag) Summary() string { return f.summary }
26+
func (f fakeDiag) Detail() string { return f.detail }
27+
func (f fakeDiag) Equal(other fwdiag.Diagnostic) bool {
28+
o, ok := other.(fakeDiag)
29+
if !ok {
30+
return false
31+
}
32+
return f.summary == o.summary && f.detail == o.detail
33+
}
34+
35+
// fakeWarnDiag implements a warning diagnostic.
36+
type fakeWarnDiag struct {
37+
summary string
38+
detail string
39+
}
40+
41+
func (f fakeWarnDiag) Severity() fwdiag.Severity { return fwdiag.SeverityWarning }
42+
func (f fakeWarnDiag) Summary() string { return f.summary }
43+
func (f fakeWarnDiag) Detail() string { return f.detail }
44+
func (f fakeWarnDiag) Equal(other fwdiag.Diagnostic) bool {
45+
o, ok := other.(fakeWarnDiag)
46+
if !ok {
47+
return false
48+
}
49+
return f.summary == o.summary && f.detail == o.detail
50+
}
51+
52+
// fakeDiagWithPath implements DiagnosticWithPath.
53+
type fakeDiagWithPath struct {
54+
summary string
55+
detail string
56+
p fwpath.Path
57+
}
58+
59+
func (f fakeDiagWithPath) Severity() fwdiag.Severity { return fwdiag.SeverityError }
60+
func (f fakeDiagWithPath) Summary() string { return f.summary }
61+
func (f fakeDiagWithPath) Detail() string { return f.detail }
62+
func (f fakeDiagWithPath) Path() fwpath.Path { return f.p }
63+
func (f fakeDiagWithPath) Equal(other fwdiag.Diagnostic) bool {
64+
o, ok := other.(fakeDiagWithPath)
65+
if !ok {
66+
return false
67+
}
68+
return f.summary == o.summary && f.detail == o.detail && f.p.String() == o.p.String()
69+
}
70+
71+
func TestFrameworkDiagnosticString(t *testing.T) {
72+
type args struct {
73+
d fwdiag.Diagnostic
74+
}
75+
type want struct {
76+
out string
77+
}
78+
cases := map[string]struct {
79+
args
80+
want
81+
}{
82+
"SummaryOnly": {
83+
args: args{d: fakeDiag{summary: "read resource failed"}},
84+
want: want{out: "read resource failed"},
85+
},
86+
"SummaryAndDetail": {
87+
args: args{d: fakeDiag{summary: "apply failed", detail: "invalid input provided"}},
88+
want: want{out: "apply failed: invalid input provided"},
89+
},
90+
"TrimsSpaces": {
91+
args: args{d: fakeDiag{summary: " parse ", detail: " bad format "}},
92+
want: want{out: "parse: bad format"},
93+
},
94+
"MultilineDetailPreserved": {
95+
args: args{d: fakeDiag{summary: "plan failed", detail: "line1\nline2"}},
96+
want: want{out: "plan failed: line1\nline2"},
97+
},
98+
}
99+
100+
for name, tc := range cases {
101+
t.Run(name, func(t *testing.T) {
102+
got := frameworkDiagnosticString(tc.args.d)
103+
if diff := cmp.Diff(tc.want.out, got); diff != "" {
104+
t.Errorf("\n%s\nframeworkDiagnosticString(...): -want out, +got out:\n%s", name, diff)
105+
}
106+
})
107+
}
108+
}
109+
110+
func TestFrameworkDiagnosticsError(t *testing.T) {
111+
type args struct {
112+
ds fwdiag.Diagnostics
113+
}
114+
type want struct {
115+
err error
116+
}
117+
// Build a path for the path-bearing diag
118+
p := fwpath.Root("root").AtName("field")
119+
120+
dErr1 := fakeDiag{summary: "op failed", detail: "reason one"}
121+
dErr2 := fakeDiagWithPath{summary: "apply failed", detail: "invalid value", p: p}
122+
dWarn := fakeWarnDiag{summary: "just a warning", detail: "ignore me"}
123+
124+
cases := map[string]struct {
125+
args
126+
want
127+
}{
128+
"NoDiagnostics": {},
129+
"OnlyWarnings": {
130+
args: args{ds: fwdiag.Diagnostics{dWarn}},
131+
want: want{err: nil},
132+
},
133+
"SingleError": {
134+
args: args{ds: fwdiag.Diagnostics{dErr1}},
135+
want: want{err: xperrors.Join(
136+
xperrors.New("terraform diagnostic errors"),
137+
xperrors.New(frameworkDiagnosticString(dErr1)),
138+
)},
139+
},
140+
"MultipleErrorsWithPath": {
141+
args: args{ds: fwdiag.Diagnostics{dErr1, dErr2}},
142+
want: want{err: xperrors.Join(
143+
xperrors.New("terraform diagnostic errors"),
144+
xperrors.New(frameworkDiagnosticString(dErr1)),
145+
xperrors.New(frameworkDiagnosticString(dErr2)),
146+
)},
147+
},
148+
}
149+
for name, tc := range cases {
150+
t.Run(name, func(t *testing.T) {
151+
got := FrameworkDiagnosticsError("terraform diagnostic errors", tc.args.ds)
152+
if diff := cmp.Diff(tc.want.err, got, test.EquateErrors()); diff != "" {
153+
t.Errorf("\n%s\nFrameworkDiagnosticsError(...): -want err, +got err:\n%s", name, diff)
154+
}
155+
})
156+
}
157+
}

0 commit comments

Comments
 (0)