8000 feat(api): return more info about workflow template parsing and execu… · ovh/cds@62db688 · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Commit 62db688

Browse files
authored
feat(api): return more info about workflow template parsing and execution errors (#3850)
* feat(api): return more info about workflow template parsing and execution errors * feat(api,ui): return error for template parsing with data * feat(ui): display template error in code with message * fix(api): use multierror instead of error slice * fix(ui,api): returns template parsing error for push and add removable args on template ui editor * fix(api): code review * fix(api): code review
1 parent 7a538e2 commit 62db688

20 files changed

+380
-114
lines changed

engine/api/templates.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ func (api *API) postTemplateHandler() service.Handler {
155155
return err
156156
}
157157

158+
// execute template with no instance only to check if parsing is ok
159+
if _, err := workflowtemplate.Execute(&t, nil); err != nil {
160+
return err
161+
}
162+
158163
// duplicate couple of group id and slug will failed with sql constraint
159164
if err := workflowtemplate.Insert(api.mustDB(), &t); err != nil {
160165
return err
@@ -232,6 +237,11 @@ func (api *API) putTemplateHandler() service.Handler {
232237
new := sdk.WorkflowTemplate(*old)
233238
new.Update(data)
234239

240+
// execute template with no instance only to check if parsing is ok
241+
if _, err := workflowtemplate.Execute(&new, nil); err != nil {
242+
return err
243+
}
244+
235245
if err := workflowtemplate.Update(api.mustDB(), &new); err != nil {
236246
return err
237247
}

engine/api/workflowtemplate/execute.go

Lines changed: 113 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"encoding/base64"
77
"fmt"
88
"io"
9+
"regexp"
10+
"strconv"
911
"strings"
1012
"text/template"
1113

@@ -38,85 +40,156 @@ func prepareParams(wt *sdk.WorkflowTemplate, r sdk.WorkflowTemplateRequest) inte
3840
return m
3941
}
4042

41-
func executeTemplate(t string, data map[string]interface{}) (string, error) {
42-
tmpl, err := template.New(fmt.Sprintf("template")).Delims("[[", "]]").Parse(t)
43+
func parseTemplate(templateType string, number int, t string) (*template.Template, error) {
44+
var id string
45+
switch templateType {
46+
case "workflow":
47+
id = templateType
48+
default:
49+
id = fmt.Sprintf("%s.%d", templateType, number)
50+
}
51+
52+
tmpl, err := template.New(id).Delims("[[", "]]").Parse(t)
4353
if err != nil {
44-
return "", sdk.WrapError(err, "cannot parse workflow template")
54+
reg := regexp.MustCompile(`template: ([0-9a-zA-Z.]+):([0-9]+): (.*)$`)
55+
submatch := reg.FindStringSubmatch(err.Error())
56+
if len(submatch) != 4 {
57+
return nil, sdk.WithStack(err)
58+
}
59+
line, err := strconv.Atoi(submatch[2])
60+
if err != nil {
61+
return nil, sdk.WithStack(err)
62+
}
63+
return nil, sdk.WithStack(sdk.WorkflowTemplateError{
64+
Type: templateType,
65+
Number: number,
66+
Line: line,
67+
Message: submatch[3],
68+
})
4569
}
70+
return tmpl, nil
71+
}
4672

73+
func executeTemplate(tmpl *template.Template, data map[string]interface{}) (string, error) {
4774
var buffer bytes.Buffer
4875
if err := tmpl.Execute(&buffer, data); err != nil {
49-
return "", sdk.WrapError(err, "cannot execute workflow template")
76+
return "", sdk.WithStack(err)
5077
}
51-
5278
return buffer.String(), nil
5379
}
5480

81+
func decodeTemplateValue(value string) (string, error) {
82+
v, err := base64.StdEncoding.DecodeString(value)
83+
if err != nil {
84+
return "", sdk.NewError(sdk.ErrWrongRequest, err)
85+
}
86+
return string(v), nil
87+
}
88+
5589
// Execute returns yaml file from template.
56-
func Execute(wt *sdk.WorkflowTemplate, i *sdk.WorkflowTemplateInstance) (sdk.WorkflowTemplateResult, error) {
57-
data := map[string]interface{}{
58-
"id": i.ID,
59-
"name": i.Request.WorkflowName,
60-
"params": prepareParams(wt, i.Request),
90+
func Execute(wt *sdk.WorkflowTemplate, instance *sdk.WorkflowTemplateInstance) (sdk.WorkflowTemplateResult, error) {
91+
result := sdk.WorkflowTemplateResult{
92+
Pipelines: make([]string, len(wt.Pipelines)),
93+
Applications: make([]string, len(wt.Applications)),
94+
Environments: make([]string, len(wt.Environments)),
6195
}
6296

63-
v, err := base64.StdEncoding.DecodeString(wt.Value)
64-
if err != nil {
65-
return sdk.WorkflowTemplateResult{}, sdk.WrapError(err, "cannot parse workflow template")
97+
var data map[string]interface{}
98+
if instance != nil {
99+
data = map[string]interface{}{
100+
"id": instance.ID,
101+
"name": instance.Request.WorkflowName,
102+
"params": prepareParams(wt, instance.Request),
103+
}
66104
}
67105

68-
out, err := executeTemplate(string(v), data)
106+
var multiErr sdk.MultiError
107+
108+
v, err := decodeTemplateValue(wt.Value)
69109
if err != nil {
70-
return sdk.WorkflowTemplateResult{}, err
110+
return result, err
71111
}
72-
73-
res := sdk.WorkflowTemplateResult{
74-
Workflow: out,
75-
Pipelines: make([]string, len(wt.Pipelines)),
76-
Applications: make([]string, len(wt.Applications)),
77-
Environments: make([]string, len(wt.Environments)),
112+
if tmpl, err := parseTemplate("workflow", 0, v); err != nil {
113+
multiErr.Append(err)
114+
} else {
115+
if data != nil {
116+
result.Workflow, err = executeTemplate(tmpl, data)
117+
if err != nil {
118+
return result, err
119+
}
120+
}
78121
}
79122

80123
for i, p := range wt.Pipelines {
81-
v, err := base64.StdEncoding.DecodeString(p.Value)
124+
v, err := decodeTemplateValue(p.Value)
82125
if err != nil {
83-
return sdk.WorkflowTemplateResult{}, sdk.WrapError(err, "cannot parse pipeline template")
126+
return result, err
84127
}
85128

86-
out, err := executeTemplate(string(v), data)
87-
if err != nil {
88-
return sdk.WorkflowTemplateResult{}, err
129+
if tmpl, err := parseTemplate("pipeline", i, v); err != nil {
130+
multiErr.Append(err)
131+
} else {
132+
result.Pipelines[i], err = executeTemplate(tmpl, data)
133+
if err != nil {
134+
return result, err
135+
}
89136
}
90-
res.Pipelines[i] = out
91137
}
92138

93139
for i, a := range wt.Applications {
94-
v, err := base64.StdEncoding.DecodeString(a.Value)
140+
v, err := decodeTemplateValue(a.Value)
95141
if err != nil {
96-
return sdk.WorkflowTemplateResult{}, sdk.WrapError(err, "cannot parse application template")
142+
return result, err
97143
}
98144

99-
out, err := executeTemplate(string(v), data)
100-
if err != nil {
101-
return sdk.WorkflowTemplateResult{}, err
145+
if tmpl, err := parseTemplate("application", i, v); err != nil {
146+
multiErr.Append(err)
147+
} else {
148+
if data != nil {
149+
result.Applications[i], err = executeTemplate(tmpl, data)
150+
if err != nil {
151+
return result, err
152+
}
153+
}
102154
}
103-
res.Applications[i] = out
104155
}
105156

106157
for i, e := range wt.Environments {
107-
v, err := base64.StdEncoding.DecodeString(e.Value)
158+
v, err := decodeTemplateValue(e.Value)
108159
if err != nil {
109-
return sdk.WorkflowTemplateResult{}, sdk.WrapError(err, "cannot parse environment template")
160+
return result, err
110161
}
111162

112-
out, err := executeTemplate(string(v), data)
113-
if err != nil {
114-
return sdk.WorkflowTemplateResult{}, err
163+
if tmpl, err := parseTemplate("environment", i, v); err != nil {
164+
multiErr.Append(err)
165+
} else {
166+
if data != nil {
167+
result.Environments[i], err = executeTemplate(tmpl, data)
168+
if err != nil {
169+
return result, err
170+
}
171+
}
172+
}
173+
}
174+
175+
if !multiErr.IsEmpty() {
176+
var errs []sdk.WorkflowTemplateError
177+
causes := make([]string, len(multiErr))
178+
for i, err := range multiErr {
179+
cause := sdk.Cause(err)
180+
if e, ok := cause.(sdk.WorkflowTemplateError); ok {
181+
errs = append(errs, e)
182+
}
183+
causes[i] = cause.Error()
115184
}
116-
res.Environments[i] = out
185+
return result, sdk.NewErrorFrom(sdk.Error{
186+
ID: sdk.ErrCannotParseTemplate.ID,
187+
Status: sdk.ErrCannotParseTemplate.Status,
188+
Data: errs,
189+
}, strings.Join(causes, ", "))
117190
}
118191

119-
return res, nil
192+
return result, nil
120193
}
121194

122195
// Tar returns in buffer the a tar file that contains all generated stuff in template result.

engine/api/workflowtemplate/execute_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,57 @@ values:
124124
type: string
125125
value: value1`, res.Environments[0])
126126
}
127+
128+
func TestExecuteTemplateWithError(t *testing.T) {
129+
tmpl := &sdk.WorkflowTemplate{
130+
ID: 42,
131+
Parameters: []sdk.WorkflowTemplateParameter{
132+
{Key: "withDeploy", Type: sdk.ParameterTypeBoolean, Required: true},
133+
{Key: "deployWhen", Type: sdk.ParameterTypeString},
134+
{Key: "repo", Type: sdk.ParameterTypeRepository},
135+
},
136+
Value: base64.StdEncoding.EncodeToString([]byte(`
137+
name: [[.name]
138+
description: Test simple workflow with error
139+
version: v1.0`)),
140+
Pipelines: []sdk.PipelineTemplate{{
141+
Value: base64.StdEncoding.EncodeToString([]byte(`
142+
version: v1.0
143+
name: Pipeline-[[error .id]]
144+
stages:
145+
- Stage 1`)),
146+
}},
147+
Applications: []sdk.ApplicationTemplate{{
148+
Value: base64.StdEncoding.EncodeToString([]byte(`
149+
version: v1.0
150+
name: [[`)),
151+
}},
152+
Environments: []sdk.EnvironmentTemplate{{
153+
Value: base64.StdEncoding.EncodeToString([]byte(`
154+
name: Environment-[[if .id]]`)),
155+
}},
156+
}
157+
158+
_, err := workflowtemplate.Execute(tmpl, nil)
159+
assert.NotNil(t, err)
160+
e := sdk.ExtractHTTPError(err, "")
161+
assert.Equal(t, sdk.ErrCannotParseTemplate.ID, e.ID)
162+
errs := []sdk.WorkflowTemplateError{{
163+
Type: "workflow",
164+
Line: 2,
165+
Message: "unexpected \"]\" in operand",
166+
}, {
167+
Type: "pipeline",
168+
Line: 3,
169+
Message: "function \"error\" not defined",
170+
}, {
171+
Type: "application",
172+
Line: 3,
173+
Message: "unexpected unclosed action in command",
174+
}, {
175+
Type: "environment",
176+
Line: 2,
177+
Message: "unexpected EOF",
178+
}}
179+
assert.Equal(t, errs, e.Data)
180+
}

engine/api/workflowtemplate/import.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ func Push(db gorp.SqlExecutor, u *sdk.User, tr *tar.Reader) ([]sdk.Message, *sdk
108108
new := sdk.WorkflowTemplate(*old)
109109
new.Update(wt)
110110

111+
// execute template with no instance only to check if parsing is ok
112+
if _, err := Execute(&new, nil); err != nil {
113+
return nil, nil, err
114+
}
115+
111116
if err := Update(db, &new); err != nil {
112117
return nil, nil, err
113118
}

sdk/error.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ var (
173173
ErrWorkflowNodeRootUpdate = Error{ID: 156, Status: http.StatusBadRequest}
174174
ErrWorkflowAlreadyAsCode = Error{ID: 157, Status: http.StatusBadRequest}
175175
ErrNoDBMigrationID = Error{ID: 158, Status: http.StatusNotFound}
176+
ErrCannotParseTemplate = Error{ID: 159, Status: http.StatusBadRequest}
176177
)
177178

178179
var errorsAmericanEnglish = map[int]string{
@@ -328,6 +329,7 @@ var errorsAmericanEnglish = map[int]string{
328329
ErrWorkflowNodeRootUpdate.ID: "Unable to update/delete the root node of your workflow",
329330
ErrWorkflowAlreadyAsCode.ID: "Workflow is already as-code or there is already a pull-request to transform it",
330331
ErrNoDBMigrationID.ID: "ID does not exist in table gorp_migration",
332+
ErrCannotParseTemplate.ID: "Cannot parse workflow template",
331333
}
332334

333335
var errorsFrench = map[int]string{
@@ -483,6 +485,7 @@ var errorsFrench = map[int]string{
483485
ErrWorkflowNodeRootUpdate.ID: "Impossible de mettre à jour ou supprimer le noeud racine du workflow",
484486
ErrWorkflowAlreadyAsCode.ID: "Le workflow est déjà as-code ou il y a déjà une pull-request pour le transformer",
485487
ErrNoDBMigrationID.ID: "Cet id n'existe pas dans la table gorp_migrations",
488+
ErrCannotParseTemplate.ID: "Impossible de parser le modèle de workflow",
486489
}
487490

488491
var errorsLanguages = []map[int]string{
@@ -492,11 +495,12 @@ var errorsLanguages = []map[int]string{
492495

493496
// Error type.
494497
type Error struct {
495-
ID int `json:"id"`
496-
Status int `json:"-"`
497-
Message string `json:"message"`
498-
UUID string `json:"uuid,omitempty"`
499-
StackTrace string `json:"stack_trace,omitempty"`
498+
ID int `json:"id"`
499+
Status int `json:"-"`
500+
Message string `json:"message"`
501+
Data interface{} `json:"data"`
502+
UUID string `json:"uuid,omitempty"`
503+
StackTrace string `json:"stack_trace,omitempty"`
500504
from string
501505
}
502506

sdk/workflow_template.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package sdk
33
import (
44
"database/sql/driver"
55
json "encoding/json"
6+
"fmt"
67
"strings"
78

89
"github.com/ovh/cds/sdk/slug"
@@ -352,3 +353,15 @@ func WorkflowTemplateInstancesToWorkflowTemplateIDs(wtis []*WorkflowTemplateInst
352353
}
353354
return ids
354355
}
356+
357+
// WorkflowTemplateError contains info about template parsing error.
358+
type WorkflowTemplateError struct {
359+
Type string `json:"type"`
360+
Number int `json:"number"`
361+
Line int `json:"line"`
362+
Message string `json:"message"`
363+
}
364+
365+
func (w WorkflowTemplateError) Error() string {
366+
return fmt.Sprintf("error '%s' in %s.%d at line %d", w.Message, w.Type, w.Number, w.Line)
367+
}

ui/src/app/model/workflow-template.model.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,10 @@ export class WorkflowTemplateInstance {
5353
workflow_template_version: number;
5454
request: WorkflowTemplateRequest;
5555
}
56+
57+
export class WorkflowTemplateError {
58+
type: string;
59+
number: number;
60+
line: number;
61+
message: string;
62+
}

ui/src/app/shared/diff/item/diff.item.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ export class Mode {
1212
styleUrls: ['./diff.item.scss']
1313
})
1414
export class DiffItemComponent implements OnChanges {
15-
@ViewChild('codeLeft') codeLeft;
16-
@ViewChild('codeRight') codeRight;
15+
@ViewChild('codeLeft') codeLeft: any;
16+
@ViewChild('codeRight') codeRight: any;
1717
@Input() original: string;
1818
@Input() updated: string;
1919
@Input() mode: Mode = Mode.UNIFIED;

ui/src/app/views/admin/hook-task/show/hook-task.show.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class HookTaskShowComponent {
9090
}
9191

9292
selectExecution(e: TaskExecution) {
93-
return _ => {
93+
return () => {
9494
this.selectedExecution = e
9595
this.selectedExecutionBody = null;
9696
if (e.webhook) {

0 commit comments

Comments
 (0)
0