Skip to content

Commit

Permalink
Add Validators and Transformers (#409)
Browse files Browse the repository at this point in the history
  • Loading branch information
bfoley13 authored Nov 4, 2024
1 parent 67bd519 commit dd66d6e
Show file tree
Hide file tree
Showing 19 changed files with 459 additions and 88 deletions.
96 changes: 79 additions & 17 deletions pkg/config/draftconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"io/fs"

"github.com/Azure/draft/pkg/config/transformers"
"github.com/Azure/draft/pkg/config/validators"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"

Expand All @@ -13,34 +15,46 @@ import (

const draftConfigFile = "draft.yaml"

type VariableValidator func(string) error
type VariableTransformer func(string) (string, error)

type DraftConfig struct {
TemplateName string `yaml:"templateName"`
DisplayName string `yaml:"displayName"`
Description string `yaml:"description"`
Type string `yaml:"type"`
Versions string `yaml:"versions"`
DefaultVersion string `yaml:"defaultVersion"`
Variables []*BuilderVar `yaml:"variables"`
FileNameOverrideMap map[string]string `yaml:"filenameOverrideMap"`
TemplateName string `yaml:"templateName"`
DisplayName string `yaml:"displayName"`
Description string `yaml:"description"`
Type string `yaml:"type"`
Versions string `yaml:"versions"`
DefaultVersion string `yaml:"defaultVersion"`
Variables []*BuilderVar `yaml:"variables"`
FileNameOverrideMap map[string]string `yaml:"filenameOverrideMap"`
Validators map[string]VariableValidator `yaml:"validators"`
Transformers map[string]VariableTransformer `yaml:"transformers"`
}

type BuilderVar struct {
Name string `yaml:"name"`
Default BuilderVarDefault `yaml:"default"`
Description string `yaml:"description"`
ExampleValues []string `yaml:"exampleValues"`
Type string `yaml:"type"`
Kind string `yaml:"kind"`
Value string `yaml:"value"`
Versions string `yaml:"versions"`
Name string `yaml:"name"`
ConditionalRef BuilderVarConditionalReference `yaml:"conditionalReference"`
Default BuilderVarDefault `yaml:"default"`
Description string `yaml:"description"`
ExampleValues []string `yaml:"exampleValues"`
Type string `yaml:"type"`
Kind string `yaml:"kind"`
Value string `yaml:"value"`
Versions string `yaml:"versions"`
}

// BuilderVarDefault holds info on the default value of a variable
type BuilderVarDefault struct {
IsPromptDisabled bool `yaml:"disablePrompt"`
ReferenceVar string `yaml:"referenceVar"`
Value string `yaml:"value"`
}

// BuilderVarConditionalReference holds a reference to a variable thats value can effect validation/transformation of the associated variable
type BuilderVarConditionalReference struct {
ReferenceVar string `yaml:"referenceVar"`
}

func NewConfigFromFS(fileSys fs.FS, path string) (*DraftConfig, error) {
configBytes, err := fs.ReadFile(fileSys, path)
if err != nil {
Expand Down Expand Up @@ -91,7 +105,17 @@ func (d *DraftConfig) GetVariableValue(name string) (string, error) {
if variable.Value == "" {
return "", fmt.Errorf("variable %s has no value", name)
}
return variable.Value, nil

if err := d.GetVariableValidator(variable.Kind)(variable.Value); err != nil {
return "", fmt.Errorf("failed variable validation: %w", err)
}

response, err := d.GetVariableTransformer(variable.Kind)(variable.Value)
if err != nil {
return "", fmt.Errorf("failed variable transformation: %w", err)
}

return response, nil
}
}

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

// GetVariableTransformer returns the transformer for a specific variable kind
func (d *DraftConfig) GetVariableTransformer(kind string) VariableTransformer {
// user overrides
if transformer, ok := d.Transformers[kind]; ok {
return transformer
}

// internally defined transformers
return transformers.GetTransformer(kind)
}

// GetVariableValidator returns the validator for a specific variable kind
func (d *DraftConfig) GetVariableValidator(kind string) VariableValidator {
// user overrides
if validator, ok := d.Validators[kind]; ok {
return validator
}

// internally defined validators
return validators.GetValidator(kind)
}

// SetVariableTransformer sets the transformer for a specific variable kind
func (d *DraftConfig) SetVariableTransformer(kind string, transformer VariableTransformer) {
if d.Transformers == nil {
d.Transformers = make(map[string]VariableTransformer)
}
d.Transformers[kind] = transformer
}

// SetVariableValidator sets the validator for a specific variable kind
func (d *DraftConfig) SetVariableValidator(kind string, validator VariableValidator) {
if d.Validators == nil {
d.Validators = make(map[string]VariableValidator)
}
d.Validators[kind] = validator
}

// ApplyDefaultVariables will apply the defaults to variables that are not already set
func (d *DraftConfig) ApplyDefaultVariables() error {
for _, variable := range d.Variables {
Expand Down
73 changes: 59 additions & 14 deletions pkg/config/draftconfig_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ package config
import (
"fmt"
"io/fs"
"regexp"
"strings"
"testing"

"github.com/Azure/draft/template"
"github.com/blang/semver/v4"
"github.com/stretchr/testify/assert"
)

const alphaNumUnderscoreHyphen = "^[A-Za-z][A-Za-z0-9-_]{1,62}[A-Za-z0-9]$"

var allTemplates = map[string]*DraftConfig{}

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

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

if !regexp.MatchString(currTemplate.TemplateName) {
return fmt.Errorf("template %s name must match the alpha-numeric-underscore-hyphen regex: %s", path, currTemplate.TemplateName)
}

if _, ok := allTemplates[strings.ToLower(currTemplate.TemplateName)]; ok {
return fmt.Errorf("template %s has a duplicate template name", path)
}
Expand All @@ -101,12 +110,12 @@ func loadTemplatesWithValidation() error {
return fmt.Errorf("template %s has an invalid type: %s", path, currTemplate.Type)
}

// version range check once we define versions
// if _, err := semver.ParseRange(currTemplate.Versions); err != nil {
// return fmt.Errorf("template %s has an invalid version range: %s", path, currTemplate.Versions)
// }
if _, err := semver.ParseRange(currTemplate.Versions); err != nil {
return fmt.Errorf("template %s has an invalid version range: %s", path, currTemplate.Versions)
}

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

// version range check once we define versions
// if _, err := semver.ParseRange(variable.Versions); err != nil {
// return fmt.Errorf("template %s has an invalid version range: %s", path, variable.Versions)
// }
if _, err := semver.ParseRange(variable.Versions); err != nil {
return fmt.Errorf("template %s has an invalid version range: %s", path, variable.Versions)
}

allVariables[variable.Name] = variable
if variable.Default.ReferenceVar != "" {
referenceVarMap[variable.Name] = variable
}

if variable.ConditionalRef.ReferenceVar != "" {
conditionRefMap[variable.Name] = variable
}
}

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

if currVar.Name == refVar.Name {
return fmt.Errorf("template %s has a variable with cyclical reference to itself: %s", path, currVar.Name)
return fmt.Errorf("template %s has a variable with cyclical default reference to itself: %s", path, currVar.Name)
}

if isCyclicalDefaultVariableReference(currVar, refVar, allVariables, map[string]bool{}) {
return fmt.Errorf("template %s has a variable with cyclical default reference to itself: %s", path, currVar.Name)
}
}

for _, currVar := range conditionRefMap {
refVar, ok := allVariables[currVar.ConditionalRef.ReferenceVar]
if !ok {
return fmt.Errorf("template %s has a variable %s with conditional reference to a non-existent variable: %s", path, currVar.Name, currVar.ConditionalRef.ReferenceVar)
}

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

Expand All @@ -152,7 +175,7 @@ func loadTemplatesWithValidation() error {
})
}

func isCyclicalVariableReference(initialVar, currRefVar *BuilderVar, allVariables map[string]*BuilderVar, visited map[string]bool) bool {
func isCyclicalDefaultVariableReference(initialVar, currRefVar *BuilderVar, allVariables map[string]*BuilderVar, visited map[string]bool) bool {
if initialVar.Name == currRefVar.Name {
return true
}
Expand All @@ -171,5 +194,27 @@ func isCyclicalVariableReference(initialVar, currRefVar *BuilderVar, allVariable
}

visited[currRefVar.Name] = true
return isCyclicalVariableReference(initialVar, refVar, allVariables, visited)
return isCyclicalDefaultVariableReference(initialVar, refVar, allVariables, visited)
}

func isCyclicalConditionalVariableReference(initialVar, currRefVar *BuilderVar, allVariables map[string]*BuilderVar, visited map[string]bool) bool {
if initialVar.Name == currRefVar.Name {
return true
}

if _, ok := visited[currRefVar.Name]; ok {
return true
}

if currRefVar.ConditionalRef.ReferenceVar == "" {
return false
}

refVar, ok := allVariables[currRefVar.ConditionalRef.ReferenceVar]
if !ok {
return false
}

visited[currRefVar.Name] = true
return isCyclicalConditionalVariableReference(initialVar, refVar, allVariables, visited)
}
12 changes: 12 additions & 0 deletions pkg/config/transformers/transformers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package transformers

func GetTransformer(variableKind string) func(string) (string, error) {
switch variableKind {
default:
return DefaultTransformer
}
}

func DefaultTransformer(inputVar string) (string, error) {
return inputVar, nil
}
17 changes: 17 additions & 0 deletions pkg/config/transformers/transformers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package transformers

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetTransformer(t *testing.T) {
assert.NotNil(t, GetTransformer("NonExistentKind"))
}

func TestDefaultTransformer(t *testing.T) {
res, err := DefaultTransformer("test")
assert.Nil(t, err)
assert.Equal(t, "test", res)
}
12 changes: 12 additions & 0 deletions pkg/config/validators/validators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package validators

func GetValidator(variableKind string) func(string) error {
switch variableKind {
default:
return DefaultValidator
}
}

func DefaultValidator(input string) error {
return nil
}
15 changes: 15 additions & 0 deletions pkg/config/validators/validators_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package validators

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetValidator(t *testing.T) {
assert.NotNil(t, GetValidator("NonExistentKind"))
}

func TestDefaultValidator(t *testing.T) {
assert.Nil(t, DefaultValidator("test"))
}
22 changes: 22 additions & 0 deletions pkg/fixtures/manifests/hpa/hpa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: test-app
labels:
app.kubernetes.io/name: test-app
app.kubernetes.io/part-of: test-app-project
kubernetes.azure.com/generator: draft
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: test-app
minReplicas: 2
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80
13 changes: 13 additions & 0 deletions pkg/fixtures/manifests/pdb/pdb.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: test-app
labels:
app.kubernetes.io/name: test-app
app.kubernetes.io/part-of: test-app-project
kubernetes.azure.com/generator: draft
spec:
maxUnavailable: 1
selector:
matchLabels:
app: test-app
16 changes: 16 additions & 0 deletions pkg/fixtures/manifests/service/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: test-app
labels:
app.kubernetes.io/name: test-app
app.kubernetes.io/part-of: test-app-project
kubernetes.azure.com/generator: draft
spec:
type: ClusterIP
selector:
app: test-app
ports:
- protocol: TCP
port: 80
targetPort: 80
Loading

0 comments on commit dd66d6e

Please sign in to comment.