Skip to content

Commit 6ee201a

Browse files
authored
Merge pull request #487 from twobiers/error-print-line
Add error context to YAML errors for better debugging
2 parents 3b1d886 + a42df65 commit 6ee201a

File tree

2 files changed

+116
-2
lines changed

2 files changed

+116
-2
lines changed

fn.go

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"bytes"
55
"context"
66
"encoding/base64"
7+
"fmt"
78
"io"
89
"io/fs"
910
"os"
11+
"strings"
1012
"text/template"
1113

1214
"dario.cat/mergo"
@@ -45,6 +47,13 @@ type Function struct {
4547
fsys fs.FS
4648
}
4749

50+
type YamlErrorContext struct {
51+
RelLine int
52+
AbsLine int
53+
Message string
54+
Context string
55+
}
56+
4857
const (
4958
annotationKeyCompositionResourceName = "gotemplating.fn.crossplane.io/composition-resource-name"
5059
annotationKeyReady = "gotemplating.fn.crossplane.io/ready"
@@ -106,14 +115,34 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest)
106115

107116
// Parse the rendered manifests.
108117
var objs []*unstructured.Unstructured
109-
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(buf.String()), 1024)
118+
data := buf.String()
119+
decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(data), 1024)
120+
121+
lines := strings.Split(data, "\n")
122+
startLine := moveToNextDoc(lines, 1)
123+
docIndex := 0
124+
110125
for {
111126
u := &unstructured.Unstructured{}
112127
if err := decoder.Decode(&u); err != nil {
113128
if err == io.EOF {
114129
break
115130
}
116-
response.Fatal(rsp, errors.Wrap(err, "cannot decode manifest"))
131+
132+
var newErr error
133+
yamlErr := getYamlErrorContextFromErr(err, startLine, lines)
134+
if yamlErr == (YamlErrorContext{}) {
135+
newErr = err
136+
} else {
137+
context := strings.TrimSpace(yamlErr.Context)
138+
if len(context) > 80 {
139+
context = context[:80] + "..."
140+
}
141+
142+
newErr = fmt.Errorf("error converting YAML to JSON: yaml: line %d (document %d, line %d) near: '%s': %s", yamlErr.AbsLine, docIndex+1, yamlErr.RelLine, context, yamlErr.Message)
143+
}
144+
145+
response.Fatal(rsp, errors.Wrap(newErr, "cannot decode manifest"))
117146
return rsp, nil
118147
}
119148

@@ -131,6 +160,9 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest)
131160
}
132161

133162
objs = append(objs, u)
163+
164+
startLine = moveToNextDoc(lines, startLine)
165+
docIndex++
134166
}
135167

136168
// Get the desired composite resource from the request.
@@ -328,3 +360,38 @@ func safeApplyTemplateOptions(templ *template.Template, options []string) (err e
328360
templ.Option(options...)
329361
return nil
330362
}
363+
364+
func moveToNextDoc(lines []string, startLine int) int {
365+
for i := startLine; i <= len(lines); i++ {
366+
if strings.TrimSpace(lines[i-1]) == "---" && i > startLine {
367+
return i
368+
}
369+
}
370+
return startLine
371+
}
372+
373+
func getYamlErrorContextFromErr(err error, startLine int, lines []string) YamlErrorContext {
374+
var relLine int
375+
n, scanErr := fmt.Sscanf(err.Error(), "error converting YAML to JSON: yaml: line %d:", &relLine)
376+
var errMsg string
377+
if scanErr == nil && n == 1 {
378+
// Extract the rest of the error message after the matched prefix.
379+
prefix := fmt.Sprintf("error converting YAML to JSON: yaml: line %d:", relLine)
380+
errStr := err.Error()
381+
if idx := strings.Index(errStr, prefix); idx != -1 {
382+
errMsg = strings.TrimSpace(errStr[idx+len(prefix):])
383+
}
384+
}
385+
if scanErr == nil && n == 1 {
386+
absLine := startLine + relLine
387+
if absLine-1 < len(lines) && absLine-1 >= 0 {
388+
return YamlErrorContext{
389+
RelLine: relLine,
390+
AbsLine: absLine,
391+
Message: errMsg,
392+
Context: lines[absLine-1],
393+
}
394+
}
395+
}
396+
return YamlErrorContext{}
397+
}

fn_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ import (
2222
)
2323

2424
var (
25+
invalidYaml = `
26+
---
27+
apiVersion: example.org/v1
28+
kind: CD
29+
metadata:
30+
name: %!@#$%^&*()_+
31+
`
32+
2533
cd = `{"apiVersion":"example.org/v1","kind":"CD","metadata":{"annotations":{"gotemplating.fn.crossplane.io/composition-resource-name":"cool-cd"},"name":"cool-cd"}}`
2634
cdTmpl = `{"apiVersion":"example.org/v1","kind":"CD","metadata":{"annotations":{"gotemplating.fn.crossplane.io/composition-resource-name":"cool-cd"},"name":"cool-cd","labels":{"belongsTo":{{.observed.composite.resource.metadata.name|quote}}}}}`
2735
cdMissingKeyTmpl = `{"apiVersion":"example.org/v1","kind":"CD","metadata":{"name":"cool-cd","labels":{"belongsTo":{{.missing | quote }}}}}`
@@ -1191,6 +1199,45 @@ func TestRunFunction(t *testing.T) {
11911199
},
11921200
},
11931201
},
1202+
"PrintYamlErrorLine": {
1203+
reason: "The Function should print the line content when invalid YAML is provided.",
1204+
args: args{
1205+
req: &fnv1.RunFunctionRequest{
1206+
Input: resource.MustStructObject(
1207+
&v1beta1.GoTemplate{
1208+
Source: v1beta1.InlineSource,
1209+
Inline: &v1beta1.TemplateSourceInline{Template: invalidYaml},
1210+
}),
1211+
Observed: &fnv1.State{
1212+
Composite: &fnv1.Resource{
1213+
Resource: resource.MustStructJSON(xr),
1214+
},
1215+
},
1216+
Desired: &fnv1.State{
1217+
Composite: &fnv1.Resource{
1218+
Resource: resource.MustStructJSON(xr),
1219+
},
1220+
},
1221+
},
1222+
},
1223+
want: want{
1224+
rsp: &fnv1.RunFunctionResponse{
1225+
Meta: &fnv1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)},
1226+
Results: []*fnv1.Result{
1227+
{
1228+
Severity: fnv1.Severity_SEVERITY_FATAL,
1229+
Message: "cannot decode manifest: error converting YAML to JSON: yaml: line 6 (document 1, line 4) near: 'name: %!@#$%^&*()_+': found character that cannot start any token",
1230+
Target: fnv1.Target_TARGET_COMPOSITE.Enum(),
1231+
},
1232+
},
1233+
Desired: &fnv1.State{
1234+
Composite: &fnv1.Resource{
1235+
Resource: resource.MustStructJSON(xr),
1236+
},
1237+
},
1238+
},
1239+
},
1240+
},
11941241
}
11951242

11961243
for name, tc := range cases {

0 commit comments

Comments
 (0)