Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Validators and Transformers #409

Merged
merged 11 commits into from
Nov 4, 2024
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"
)
bfoley13 marked this conversation as resolved.
Show resolved Hide resolved

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 {
bfoley13 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading