Skip to content

Commit dd66d6e

Browse files
authored
Add Validators and Transformers (#409)
1 parent 67bd519 commit dd66d6e

File tree

19 files changed

+459
-88
lines changed

19 files changed

+459
-88
lines changed

pkg/config/draftconfig.go

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"io/fs"
77

8+
"github.com/Azure/draft/pkg/config/transformers"
9+
"github.com/Azure/draft/pkg/config/validators"
810
log "github.com/sirupsen/logrus"
911
"gopkg.in/yaml.v2"
1012

@@ -13,34 +15,46 @@ import (
1315

1416
const draftConfigFile = "draft.yaml"
1517

18+
type VariableValidator func(string) error
19+
type VariableTransformer func(string) (string, error)
20+
1621
type DraftConfig struct {
17-
TemplateName string `yaml:"templateName"`
18-
DisplayName string `yaml:"displayName"`
19-
Description string `yaml:"description"`
20-
Type string `yaml:"type"`
21-
Versions string `yaml:"versions"`
22-
DefaultVersion string `yaml:"defaultVersion"`
23-
Variables []*BuilderVar `yaml:"variables"`
24-
FileNameOverrideMap map[string]string `yaml:"filenameOverrideMap"`
22+
TemplateName string `yaml:"templateName"`
23+
DisplayName string `yaml:"displayName"`
24+
Description string `yaml:"description"`
25+
Type string `yaml:"type"`
26+
Versions string `yaml:"versions"`
27+
DefaultVersion string `yaml:"defaultVersion"`
28+
Variables []*BuilderVar `yaml:"variables"`
29+
FileNameOverrideMap map[string]string `yaml:"filenameOverrideMap"`
30+
Validators map[string]VariableValidator `yaml:"validators"`
31+
Transformers map[string]VariableTransformer `yaml:"transformers"`
2532
}
2633

2734
type BuilderVar struct {
28-
Name string `yaml:"name"`
29-
Default BuilderVarDefault `yaml:"default"`
30-
Description string `yaml:"description"`
31-
ExampleValues []string `yaml:"exampleValues"`
32-
Type string `yaml:"type"`
33-
Kind string `yaml:"kind"`
34-
Value string `yaml:"value"`
35-
Versions string `yaml:"versions"`
35+
Name string `yaml:"name"`
36+
ConditionalRef BuilderVarConditionalReference `yaml:"conditionalReference"`
37+
Default BuilderVarDefault `yaml:"default"`
38+
Description string `yaml:"description"`
39+
ExampleValues []string `yaml:"exampleValues"`
40+
Type string `yaml:"type"`
41+
Kind string `yaml:"kind"`
42+
Value string `yaml:"value"`
43+
Versions string `yaml:"versions"`
3644
}
3745

46+
// BuilderVarDefault holds info on the default value of a variable
3847
type BuilderVarDefault struct {
3948
IsPromptDisabled bool `yaml:"disablePrompt"`
4049
ReferenceVar string `yaml:"referenceVar"`
4150
Value string `yaml:"value"`
4251
}
4352

53+
// BuilderVarConditionalReference holds a reference to a variable thats value can effect validation/transformation of the associated variable
54+
type BuilderVarConditionalReference struct {
55+
ReferenceVar string `yaml:"referenceVar"`
56+
}
57+
4458
func NewConfigFromFS(fileSys fs.FS, path string) (*DraftConfig, error) {
4559
configBytes, err := fs.ReadFile(fileSys, path)
4660
if err != nil {
@@ -91,7 +105,17 @@ func (d *DraftConfig) GetVariableValue(name string) (string, error) {
91105
if variable.Value == "" {
92106
return "", fmt.Errorf("variable %s has no value", name)
93107
}
94-
return variable.Value, nil
108+
109+
if err := d.GetVariableValidator(variable.Kind)(variable.Value); err != nil {
110+
return "", fmt.Errorf("failed variable validation: %w", err)
111+
}
112+
113+
response, err := d.GetVariableTransformer(variable.Kind)(variable.Value)
114+
if err != nil {
115+
return "", fmt.Errorf("failed variable transformation: %w", err)
116+
}
117+
118+
return response, nil
95119
}
96120
}
97121

@@ -109,6 +133,44 @@ func (d *DraftConfig) SetVariable(name, value string) {
109133
}
110134
}
111135

136+
// GetVariableTransformer returns the transformer for a specific variable kind
137+
func (d *DraftConfig) GetVariableTransformer(kind string) VariableTransformer {
138+
// user overrides
139+
if transformer, ok := d.Transformers[kind]; ok {
140+
return transformer
141+
}
142+
143+
// internally defined transformers
144+
return transformers.GetTransformer(kind)
145+
}
146+
147+
// GetVariableValidator returns the validator for a specific variable kind
148+
func (d *DraftConfig) GetVariableValidator(kind string) VariableValidator {
149+
// user overrides
150+
if validator, ok := d.Validators[kind]; ok {
151+
return validator
152+
}
153+
154+
// internally defined validators
155+
return validators.GetValidator(kind)
156+
}
157+
158+
// SetVariableTransformer sets the transformer for a specific variable kind
159+
func (d *DraftConfig) SetVariableTransformer(kind string, transformer VariableTransformer) {
160+
if d.Transformers == nil {
161+
d.Transformers = make(map[string]VariableTransformer)
162+
}
163+
d.Transformers[kind] = transformer
164+
}
165+
166+
// SetVariableValidator sets the validator for a specific variable kind
167+
func (d *DraftConfig) SetVariableValidator(kind string, validator VariableValidator) {
168+
if d.Validators == nil {
169+
d.Validators = make(map[string]VariableValidator)
170+
}
171+
d.Validators[kind] = validator
172+
}
173+
112174
// ApplyDefaultVariables will apply the defaults to variables that are not already set
113175
func (d *DraftConfig) ApplyDefaultVariables() error {
114176
for _, variable := range d.Variables {

pkg/config/draftconfig_template_test.go

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ package config
33
import (
44
"fmt"
55
"io/fs"
6+
"regexp"
67
"strings"
78
"testing"
89

910
"github.com/Azure/draft/template"
11+
"github.com/blang/semver/v4"
1012
"github.com/stretchr/testify/assert"
1113
)
1214

15+
const alphaNumUnderscoreHyphen = "^[A-Za-z][A-Za-z0-9-_]{1,62}[A-Za-z0-9]$"
16+
1317
var allTemplates = map[string]*DraftConfig{}
1418

1519
var validTemplateTypes = map[string]bool{
@@ -67,6 +71,7 @@ func TestTempalteValidation(t *testing.T) {
6771
}
6872

6973
func loadTemplatesWithValidation() error {
74+
regexp := regexp.MustCompile(alphaNumUnderscoreHyphen)
7075
return fs.WalkDir(template.Templates, ".", func(path string, d fs.DirEntry, err error) error {
7176
if err != nil {
7277
return err
@@ -93,6 +98,10 @@ func loadTemplatesWithValidation() error {
9398
return fmt.Errorf("template %s has no template name", path)
9499
}
95100

101+
if !regexp.MatchString(currTemplate.TemplateName) {
102+
return fmt.Errorf("template %s name must match the alpha-numeric-underscore-hyphen regex: %s", path, currTemplate.TemplateName)
103+
}
104+
96105
if _, ok := allTemplates[strings.ToLower(currTemplate.TemplateName)]; ok {
97106
return fmt.Errorf("template %s has a duplicate template name", path)
98107
}
@@ -101,12 +110,12 @@ func loadTemplatesWithValidation() error {
101110
return fmt.Errorf("template %s has an invalid type: %s", path, currTemplate.Type)
102111
}
103112

104-
// version range check once we define versions
105-
// if _, err := semver.ParseRange(currTemplate.Versions); err != nil {
106-
// return fmt.Errorf("template %s has an invalid version range: %s", path, currTemplate.Versions)
107-
// }
113+
if _, err := semver.ParseRange(currTemplate.Versions); err != nil {
114+
return fmt.Errorf("template %s has an invalid version range: %s", path, currTemplate.Versions)
115+
}
108116

109117
referenceVarMap := map[string]*BuilderVar{}
118+
conditionRefMap := map[string]*BuilderVar{}
110119
allVariables := map[string]*BuilderVar{}
111120
for _, variable := range currTemplate.Variables {
112121
if variable.Name == "" {
@@ -121,29 +130,43 @@ func loadTemplatesWithValidation() error {
121130
return fmt.Errorf("template %s has an invalid variable kind: %s", path, variable.Kind)
122131
}
123132

124-
// version range check once we define versions
125-
// if _, err := semver.ParseRange(variable.Versions); err != nil {
126-
// return fmt.Errorf("template %s has an invalid version range: %s", path, variable.Versions)
127-
// }
133+
if _, err := semver.ParseRange(variable.Versions); err != nil {
134+
return fmt.Errorf("template %s has an invalid version range: %s", path, variable.Versions)
135+
}
128136

129137
allVariables[variable.Name] = variable
130138
if variable.Default.ReferenceVar != "" {
131139
referenceVarMap[variable.Name] = variable
132140
}
141+
142+
if variable.ConditionalRef.ReferenceVar != "" {
143+
conditionRefMap[variable.Name] = variable
144+
}
133145
}
134146

135147
for _, currVar := range referenceVarMap {
136148
refVar, ok := allVariables[currVar.Default.ReferenceVar]
137149
if !ok {
138-
return fmt.Errorf("template %s has a variable %s with reference to a non-existent variable: %s", path, currVar.Name, currVar.Default.ReferenceVar)
150+
return fmt.Errorf("template %s has a variable %s with default reference to a non-existent variable: %s", path, currVar.Name, currVar.Default.ReferenceVar)
139151
}
140152

141153
if currVar.Name == refVar.Name {
142-
return fmt.Errorf("template %s has a variable with cyclical reference to itself: %s", path, currVar.Name)
154+
return fmt.Errorf("template %s has a variable with cyclical default reference to itself: %s", path, currVar.Name)
155+
}
156+
157+
if isCyclicalDefaultVariableReference(currVar, refVar, allVariables, map[string]bool{}) {
158+
return fmt.Errorf("template %s has a variable with cyclical default reference to itself: %s", path, currVar.Name)
159+
}
160+
}
161+
162+
for _, currVar := range conditionRefMap {
163+
refVar, ok := allVariables[currVar.ConditionalRef.ReferenceVar]
164+
if !ok {
165+
return fmt.Errorf("template %s has a variable %s with conditional reference to a non-existent variable: %s", path, currVar.Name, currVar.ConditionalRef.ReferenceVar)
143166
}
144167

145-
if isCyclicalVariableReference(currVar, refVar, allVariables, map[string]bool{}) {
146-
return fmt.Errorf("template %s has a variable with cyclical reference to itself: %s", path, currVar.Name)
168+
if isCyclicalConditionalVariableReference(currVar, refVar, allVariables, map[string]bool{}) {
169+
return fmt.Errorf("template %s has a variable with cyclical conditional reference to itself or references a non existing variable: %s", path, currVar.Name)
147170
}
148171
}
149172

@@ -152,7 +175,7 @@ func loadTemplatesWithValidation() error {
152175
})
153176
}
154177

155-
func isCyclicalVariableReference(initialVar, currRefVar *BuilderVar, allVariables map[string]*BuilderVar, visited map[string]bool) bool {
178+
func isCyclicalDefaultVariableReference(initialVar, currRefVar *BuilderVar, allVariables map[string]*BuilderVar, visited map[string]bool) bool {
156179
if initialVar.Name == currRefVar.Name {
157180
return true
158181
}
@@ -171,5 +194,27 @@ func isCyclicalVariableReference(initialVar, currRefVar *BuilderVar, allVariable
171194
}
172195

173196
visited[currRefVar.Name] = true
174-
return isCyclicalVariableReference(initialVar, refVar, allVariables, visited)
197+
return isCyclicalDefaultVariableReference(initialVar, refVar, allVariables, visited)
198+
}
199+
200+
func isCyclicalConditionalVariableReference(initialVar, currRefVar *BuilderVar, allVariables map[string]*BuilderVar, visited map[string]bool) bool {
201+
if initialVar.Name == currRefVar.Name {
202+
return true
203+
}
204+
205+
if _, ok := visited[currRefVar.Name]; ok {
206+
return true
207+
}
208+
209+
if currRefVar.ConditionalRef.ReferenceVar == "" {
210+
return false
211+
}
212+
213+
refVar, ok := allVariables[currRefVar.ConditionalRef.ReferenceVar]
214+
if !ok {
215+
return false
216+
}
217+
218+
visited[currRefVar.Name] = true
219+
return isCyclicalConditionalVariableReference(initialVar, refVar, allVariables, visited)
175220
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package transformers
2+
3+
func GetTransformer(variableKind string) func(string) (string, error) {
4+
switch variableKind {
5+
default:
6+
return DefaultTransformer
7+
}
8+
}
9+
10+
func DefaultTransformer(inputVar string) (string, error) {
11+
return inputVar, nil
12+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package transformers
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestGetTransformer(t *testing.T) {
10+
assert.NotNil(t, GetTransformer("NonExistentKind"))
11+
}
12+
13+
func TestDefaultTransformer(t *testing.T) {
14+
res, err := DefaultTransformer("test")
15+
assert.Nil(t, err)
16+
assert.Equal(t, "test", res)
17+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package validators
2+
3+
func GetValidator(variableKind string) func(string) error {
4+
switch variableKind {
5+
default:
6+
return DefaultValidator
7+
}
8+
}
9+
10+
func DefaultValidator(input string) error {
11+
return nil
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package validators
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestGetValidator(t *testing.T) {
10+
assert.NotNil(t, GetValidator("NonExistentKind"))
11+
}
12+
13+
func TestDefaultValidator(t *testing.T) {
14+
assert.Nil(t, DefaultValidator("test"))
15+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
apiVersion: autoscaling/v2
2+
kind: HorizontalPodAutoscaler
3+
metadata:
4+
name: test-app
5+
labels:
6+
app.kubernetes.io/name: test-app
7+
app.kubernetes.io/part-of: test-app-project
8+
kubernetes.azure.com/generator: draft
9+
spec:
10+
scaleTargetRef:
11+
apiVersion: apps/v1
12+
kind: Deployment
13+
name: test-app
14+
minReplicas: 2
15+
maxReplicas: 5
16+
metrics:
17+
- type: Resource
18+
resource:
19+
name: cpu
20+
target:
21+
type: Utilization
22+
averageUtilization: 80
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
apiVersion: policy/v1
2+
kind: PodDisruptionBudget
3+
metadata:
4+
name: test-app
5+
labels:
6+
app.kubernetes.io/name: test-app
7+
app.kubernetes.io/part-of: test-app-project
8+
kubernetes.azure.com/generator: draft
9+
spec:
10+
maxUnavailable: 1
11+
selector:
12+
matchLabels:
13+
app: test-app
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: test-app
5+
labels:
6+
app.kubernetes.io/name: test-app
7+
app.kubernetes.io/part-of: test-app-project
8+
kubernetes.azure.com/generator: draft
9+
spec:
10+
type: ClusterIP
11+
selector:
12+
app: test-app
13+
ports:
14+
- protocol: TCP
15+
port: 80
16+
targetPort: 80

0 commit comments

Comments
 (0)