-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathvalidate.go
144 lines (119 loc) · 3.59 KB
/
validate.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2024-Present Harry Randazzo
package vai
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/url"
"regexp"
"slices"
"strings"
"sync"
"github.com/goccy/go-yaml"
"github.com/xeipuuv/gojsonschema"
)
// TaskNamePattern is a regular expression for valid task names, it is also used for step IDs
var TaskNamePattern = regexp.MustCompile("^[_a-zA-Z][a-zA-Z0-9_-]*$")
// EnvVariablePattern is a regular expression for valid environment variable names
var EnvVariablePattern = regexp.MustCompile("^[a-zA-Z_]+[a-zA-Z0-9_]*$")
// Read reads a workflow from a file
func Read(r io.Reader) (Workflow, error) {
if rs, ok := r.(io.Seeker); ok {
_, err := rs.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
}
b, err := io.ReadAll(r)
if err != nil {
return nil, err
}
wf := Workflow{}
return wf, yaml.Unmarshal(b, &wf)
}
var _schema string
var _schemaOnce sync.Once
// Validate validates a workflow
func Validate(wf Workflow) error {
for name, task := range wf {
if ok := TaskNamePattern.MatchString(name); !ok {
return fmt.Errorf("task name %q does not satisfy %q", name, TaskNamePattern.String())
}
ids := make(map[string]int, len(task))
for idx, step := range task {
// ensure that only one of run or uses or eval fields is set
// if more than one is set, return an error
// if none are set, return an error
switch {
case step.Uses != "" && step.Run != "":
return fmt.Errorf(".%s[%d] has both run and uses fields set", name, idx)
case step.Uses != "" && step.Eval != "":
return fmt.Errorf(".%s[%d] has both eval and uses fields set", name, idx)
case step.Run != "" && step.Eval != "":
return fmt.Errorf(".%s[%d] has both run and eval fields set", name, idx)
case step.Uses == "" && step.Run == "" && step.Eval == "":
return fmt.Errorf(".%s[%d] must have one of [eval, run, uses] fields set", name, idx)
}
if step.ID != "" {
if ok := TaskNamePattern.MatchString(step.ID); !ok {
return fmt.Errorf(".%s[%d].id %q does not satisfy %q", name, idx, step.ID, TaskNamePattern.String())
}
if _, ok := ids[step.ID]; ok {
return fmt.Errorf(".%s[%d] and .%s[%d] have the same ID %q", name, ids[step.ID], name, idx, step.ID)
}
ids[step.ID] = idx
}
if step.Uses != "" {
u, err := url.Parse(step.Uses)
if err != nil {
return fmt.Errorf(".%s[%d].uses %w", name, idx, err)
}
if u.Scheme == "" {
// if step.Uses == name {
// return fmt.Errorf(".%s[%d].uses cannot reference itself", name, idx)
// }
_, ok := wf.Find(step.Uses)
if !ok {
return fmt.Errorf(".%s[%d].uses %q not found", name, idx, step.Uses)
}
} else {
schemes := []string{"file", "http", "https", "pkg"}
if !slices.Contains(schemes, u.Scheme) {
return fmt.Errorf(".%s[%d].uses %q is not one of [%s]", name, idx, u.Scheme, strings.Join(schemes, ", "))
}
}
}
}
}
_schemaOnce.Do(func() {
s := WorkFlowSchema()
b, err := json.Marshal(s)
if err != nil {
panic(err)
}
_schema = string(b)
})
schemaLoader := gojsonschema.NewStringLoader(_schema)
result, err := gojsonschema.Validate(schemaLoader, gojsonschema.NewGoLoader(wf))
if err != nil {
return err
}
if result.Valid() {
return nil
}
var resErr error
for _, err := range result.Errors() {
resErr = errors.Join(resErr, errors.New(err.String()))
}
return resErr
}
// ReadAndValidate reads and validates a workflow
func ReadAndValidate(r io.Reader) (Workflow, error) {
wf, err := Read(r)
if err != nil {
return nil, err
}
return wf, Validate(wf)
}