-
Notifications
You must be signed in to change notification settings - Fork 3
/
dwdparse.go
257 lines (212 loc) · 7.45 KB
/
dwdparse.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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
/*
* Copyright 2021, 2022 Hewlett Packard Enterprise Development LP
* Other additional copyright holders may be indicated within.
*
* The entirety of this work is licensed under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
*
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dwdparse
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// DWDirectiveRuleDef defines the DWDirective parser rules
// +kubebuilder:object:generate=true
type DWDirectiveRuleDef struct {
Key string `json:"key"`
Type string `json:"type"`
Pattern string `json:"pattern,omitempty"`
Min int `json:"min,omitempty"`
Max int `json:"max,omitempty"`
IsRequired bool `json:"isRequired,omitempty"`
IsValueRequired bool `json:"isValueRequired,omitempty"`
UniqueWithin string `json:"uniqueWithin,omitempty"`
}
// DWDirectiveRuleSpec defines the desired state of DWDirective
// +kubebuilder:object:generate=true
type DWDirectiveRuleSpec struct {
// Name of the #DW command. jobdw, stage_in, etc.
Command string `json:"command"`
// Override for the Driver ID. If left empty this defaults to the
// name of the DWDirectiveRule
DriverLabel string `json:"driverLabel,omitempty"`
// Comma separated list of states that this rule wants to register for.
// These watch states will result in an entry in the driver status array
// in the Workflow resource
WatchStates string `json:"watchStates,omitempty"`
// List of key/value pairs this #DW command is expected to have
RuleDefs []DWDirectiveRuleDef `json:"ruleDefs"`
}
// BuildArgsMap builds a map of the DWDirective's arguments in the form: args["key"] = value
func BuildArgsMap(dwd string) (map[string]string, error) {
dwdArgs := strings.Fields(dwd)
if len(dwdArgs) == 0 || dwdArgs[0] != "#DW" {
return nil, fmt.Errorf("missing '#DW' prefix in directive '%s'", dwd)
}
argsMap := make(map[string]string)
argsMap["command"] = dwdArgs[1]
for i := 2; i < len(dwdArgs); i++ {
keyValue := strings.Split(dwdArgs[i], "=")
// Don't allow repeated arguments
_, ok := argsMap[keyValue[0]]
if ok {
return nil, fmt.Errorf("repeated argument '%s' in directive '%s'", keyValue[0], dwd)
}
if len(keyValue) == 1 {
argsMap[keyValue[0]] = "true"
} else if len(keyValue) == 2 {
argsMap[keyValue[0]] = keyValue[1]
} else {
keyValue := strings.SplitN(dwdArgs[i], "=", 2)
argsMap[keyValue[0]] = keyValue[1]
}
}
return argsMap, nil
}
// Compile this regex outside the loop for better performance.
var boolMatcher = regexp.MustCompile(`(?i)^(true|false)$`) // (?i) -> case-insensitve comparison
// ValidateArgs validates a map of arguments against the rule specification
func ValidateArgs(spec DWDirectiveRuleSpec, args map[string]string, uniqueMap map[string]bool) error {
command, found := args["command"]
if !found {
return fmt.Errorf("no command in arguments")
}
if command != spec.Command {
return fmt.Errorf("command '%s' does not match rule '%s'", command, spec.Command)
}
// Create a map that maps a directive rule definition to an argument that correctly matches it
// key: DWDirectiveRule value: argument that matches that rule
// Required to check that all DWDirectiveRuleDef's have been met
argToRuleMap := map[*DWDirectiveRuleDef]string{}
findRuleDefinition := func(key string) (*DWDirectiveRuleDef, error) {
for index, rule := range spec.RuleDefs {
re, err := regexp.Compile(rule.Key)
if err != nil {
return nil, fmt.Errorf("invalid rule regular expression '%s'", rule.Key)
}
if re.MatchString(key) {
return &spec.RuleDefs[index], nil
}
}
return nil, fmt.Errorf("unsupported argument '%s'", key)
}
// Iterate over all arguments and validate each based on the associated rule
for k, v := range args {
if k == "command" {
continue
}
rule, err := findRuleDefinition(k)
if err != nil {
return err
}
if rule.IsValueRequired && len(v) == 0 {
return fmt.Errorf("argument '%s' requires value", k)
}
switch rule.Type {
case "integer":
i, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("argument '%s' invalid integer '%s'", k, v)
}
if rule.Max != 0 && i > rule.Max {
return fmt.Errorf("argument '%s' specified integer value %d is greather than maximum value %d", k, i, rule.Max)
}
if rule.Min != 0 && i < rule.Min {
return fmt.Errorf("argument '%s' specified integer value %d is less than minimum value %d", k, i, rule.Min)
}
case "bool":
if rule.Pattern != "" {
if !boolMatcher.MatchString(v) {
return fmt.Errorf("argument '%s' invalid boolean '%s'", k, v)
}
}
case "string":
if rule.Pattern != "" {
re, err := regexp.Compile(rule.Pattern)
if err != nil {
return fmt.Errorf("invalid regular expression '%s'", rule.Pattern)
}
if !re.MatchString(v) {
return fmt.Errorf("argument '%s' invalid string '%s'", k, v)
}
}
default:
return fmt.Errorf("unsupported rule type '%s'", rule.Type)
}
if rule.UniqueWithin != "" {
_, ok := uniqueMap[rule.UniqueWithin+"/"+v]
if ok {
return fmt.Errorf("value '%s' must be unique within '%s'", v, rule.UniqueWithin)
}
uniqueMap[rule.UniqueWithin+"/"+v] = true
}
// NOTE: We know that we don't have repeated arguments here because the arguments
// come to us in a map indexed by the argment name.
argToRuleMap[rule] = k
}
// Iterate over the rules to ensure all required rules have an argument
for index := range spec.RuleDefs {
rule := &spec.RuleDefs[index]
if rule.IsRequired {
if _, found := argToRuleMap[rule]; !found {
return fmt.Errorf("missing required argument '%v'", rule.Key)
}
}
}
return nil
}
// Validate a list of directives against the supplied rules. When a directive is valid
// for a particular rule, the `onValidDirectiveFunc` function is called.
func Validate(rules []DWDirectiveRuleSpec, directives []string, onValidDirectiveFunc func(index int, rule DWDirectiveRuleSpec)) error {
// Create a map to track argument uniqueness within the directives for
// rules that contain `UniqueWithin`
uniqueMap := make(map[string]bool)
for index, directive := range directives {
// A directive is validated against all rules; any one rule that is valid for a directive
// makes that directive valid.
validDirective := false
for _, rule := range rules {
valid, err := validateDWDirective(rule, directive, uniqueMap)
if err != nil {
return err
}
if valid {
validDirective = true
onValidDirectiveFunc(index, rule)
}
}
if !validDirective {
return fmt.Errorf("invalid directive '%s'", directive)
}
}
return nil
}
// validateDWDirective validates an individual directive against a rule
func validateDWDirective(rule DWDirectiveRuleSpec, dwd string, uniqueMap map[string]bool) (bool, error) {
// Build a map of the #DW commands and arguments
argsMap, err := BuildArgsMap(dwd)
if err != nil {
return false, err
}
if argsMap["command"] != rule.Command {
return false, nil
}
err = ValidateArgs(rule, argsMap, uniqueMap)
if err != nil {
return false, err
}
return true, nil
}