From 06a670830d031c461a7e07f3b2b93937a6a78817 Mon Sep 17 00:00:00 2001 From: Brandon Foley Date: Fri, 18 Oct 2024 13:50:01 -0400 Subject: [PATCH 1/9] Add Validators and Transformers --- pkg/config/draftconfig.go | 73 +++++++++++++-- pkg/config/transformers/transformers.go | 12 +++ pkg/config/transformers/transformers_test.go | 13 +++ pkg/config/validators/validators.go | 12 +++ pkg/config/validators/validators_test.go | 15 +++ pkg/handlers/template_test.go | 99 +++++++++++++++++++- 6 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 pkg/config/transformers/transformers.go create mode 100644 pkg/config/transformers/transformers_test.go create mode 100644 pkg/config/validators/validators.go create mode 100644 pkg/config/validators/validators_test.go diff --git a/pkg/config/draftconfig.go b/pkg/config/draftconfig.go index b3f69e94..275abb91 100644 --- a/pkg/config/draftconfig.go +++ b/pkg/config/draftconfig.go @@ -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" @@ -13,15 +15,20 @@ 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 { @@ -91,7 +98,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 } } @@ -109,6 +126,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.DefaultValidator +} + +// 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 { diff --git a/pkg/config/transformers/transformers.go b/pkg/config/transformers/transformers.go new file mode 100644 index 00000000..48fce997 --- /dev/null +++ b/pkg/config/transformers/transformers.go @@ -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 +} diff --git a/pkg/config/transformers/transformers_test.go b/pkg/config/transformers/transformers_test.go new file mode 100644 index 00000000..57466935 --- /dev/null +++ b/pkg/config/transformers/transformers_test.go @@ -0,0 +1,13 @@ +package transformers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultTransformer(t *testing.T) { + res, err := DefaultTransformer("test") + assert.Nil(t, err) + assert.Equal(t, "test", res) +} diff --git a/pkg/config/validators/validators.go b/pkg/config/validators/validators.go new file mode 100644 index 00000000..2c7e5c9f --- /dev/null +++ b/pkg/config/validators/validators.go @@ -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 +} diff --git a/pkg/config/validators/validators_test.go b/pkg/config/validators/validators_test.go new file mode 100644 index 00000000..7e8a6e1b --- /dev/null +++ b/pkg/config/validators/validators_test.go @@ -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")) +} diff --git a/pkg/handlers/template_test.go b/pkg/handlers/template_test.go index 472ea6d6..a6598ebb 100644 --- a/pkg/handlers/template_test.go +++ b/pkg/handlers/template_test.go @@ -4,6 +4,7 @@ import ( "fmt" "path/filepath" "reflect" + "regexp" "strings" "testing" @@ -12,6 +13,25 @@ import ( "github.com/stretchr/testify/assert" ) +func AlwaysFailingValidator(value string) error { + return fmt.Errorf("this is a failing validator") +} + +func AlwaysFailingTransformer(value string) (string, error) { + return "", fmt.Errorf("this is a failing transformer") +} + +func K8sLabelValidator(value string) error { + labelRegex, err := regexp.Compile("^((A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$") + if err != nil { + return err + } + if !labelRegex.MatchString(value) { + return fmt.Errorf("invalid label: %s", value) + } + return nil +} + func TestDeepCopy(t *testing.T) { // This will fail on adding a new field to the undelying structs that arent handled in DeepCopy testTemplate, err := GetTemplate("deployment-manifests", "0.0.1", ".", &writers.FileMapWriter{}) @@ -33,6 +53,8 @@ func TestTemplateHandlerValidation(t *testing.T) { varMap map[string]string fileNameOverride map[string]string expectedErr error + validators map[string]func(string) error + transformers map[string]func(string) (string, error) }{ { name: "valid manifest deployment", @@ -337,6 +359,69 @@ func TestTemplateHandlerValidation(t *testing.T) { "service-port": "80", }, }, + { + name: "manifest deployment vars with err from validators", + templateName: "deployment-manifests", + fixturesBaseDir: "../fixtures/deployments/manifest", + version: "0.0.1", + dest: ".", + templateWriter: &writers.FileMapWriter{}, + varMap: map[string]string{ + "APPNAME": "testapp", + "NAMESPACE": "default", + "PORT": "80", + "IMAGENAME": "testimage", + "IMAGETAG": "latest", + "GENERATORLABEL": "draft", + "SERVICEPORT": "80", + }, + validators: map[string]func(string) error{ + "kubernetesResourceName": AlwaysFailingValidator, + }, + expectedErr: fmt.Errorf("this is a failing validator"), + }, + { + name: "manifest deployment vars with err from transformers", + templateName: "deployment-manifests", + fixturesBaseDir: "../fixtures/deployments/manifest", + version: "0.0.1", + dest: ".", + templateWriter: &writers.FileMapWriter{}, + varMap: map[string]string{ + "APPNAME": "testapp", + "NAMESPACE": "default", + "PORT": "80", + "IMAGENAME": "testimage", + "IMAGETAG": "latest", + "GENERATORLABEL": "draft", + "SERVICEPORT": "80", + }, + transformers: map[string]func(string) (string, error){ + "kubernetesResourceName": AlwaysFailingTransformer, + }, + expectedErr: fmt.Errorf("this is a failing transformer"), + }, + { + name: "manifest deployment vars with err from label validator", + templateName: "deployment-manifests", + fixturesBaseDir: "../fixtures/deployments/manifest", + version: "0.0.1", + dest: ".", + templateWriter: &writers.FileMapWriter{}, + varMap: map[string]string{ + "APPNAME": "*myTestApp", + "NAMESPACE": "default", + "PORT": "80", + "IMAGENAME": "testimage", + "IMAGETAG": "latest", + "GENERATORLABEL": "draft", + "SERVICEPORT": "80", + }, + validators: map[string]func(string) error{ + "kubernetesResourceName": K8sLabelValidator, + }, + expectedErr: fmt.Errorf("invalid label: *myTestApp"), + }, } for _, tt := range tests { @@ -349,6 +434,14 @@ func TestTemplateHandlerValidation(t *testing.T) { template.Config.SetVariable(k, v) } + for k, v := range tt.validators { + template.Config.SetVariableValidator(k, v) + } + + for k, v := range tt.transformers { + template.Config.SetVariableTransformer(k, v) + } + overrideReverseLookup := make(map[string]string) for k, v := range tt.fileNameOverride { template.Config.SetFileNameOverride(k, v) @@ -357,7 +450,11 @@ func TestTemplateHandlerValidation(t *testing.T) { err = template.Generate() if tt.expectedErr != nil { - assert.Equal(t, tt.expectedErr.Error(), err.Error()) + if err == nil { + t.Errorf("expected error %v, got nil", tt.expectedErr) + return + } + assert.True(t, strings.Contains(err.Error(), tt.expectedErr.Error())) return } assert.Nil(t, err) From a4a9d19a22d55452db2dd0eef7ac481aa48782ce Mon Sep 17 00:00:00 2001 From: Brandon Foley Date: Mon, 21 Oct 2024 11:22:19 -0400 Subject: [PATCH 2/9] update validator defaulting --- pkg/config/draftconfig.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/draftconfig.go b/pkg/config/draftconfig.go index 275abb91..f83f9763 100644 --- a/pkg/config/draftconfig.go +++ b/pkg/config/draftconfig.go @@ -145,7 +145,7 @@ func (d *DraftConfig) GetVariableValidator(kind string) VariableValidator { } // internally defined validators - return validators.DefaultValidator + return validators.GetValidator(kind) } // SetVariableTransformer sets the transformer for a specific variable kind From 6885973e961bf8a22a462bb9c0e0c7180f476196 Mon Sep 17 00:00:00 2001 From: Brandon Foley Date: Mon, 21 Oct 2024 11:23:56 -0400 Subject: [PATCH 3/9] Add GetTransformer Test --- pkg/config/transformers/transformers_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/config/transformers/transformers_test.go b/pkg/config/transformers/transformers_test.go index 57466935..72c4286e 100644 --- a/pkg/config/transformers/transformers_test.go +++ b/pkg/config/transformers/transformers_test.go @@ -6,6 +6,10 @@ import ( "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) From 2fa805ae9fa85fe081ca856dad8290f9852771ce Mon Sep 17 00:00:00 2001 From: Brandon Foley Date: Tue, 29 Oct 2024 11:01:44 -0400 Subject: [PATCH 4/9] conditional reference --- pkg/config/draftconfig.go | 23 +++++++---- pkg/config/draftconfig_template_test.go | 54 ++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/pkg/config/draftconfig.go b/pkg/config/draftconfig.go index f83f9763..1fa8465f 100644 --- a/pkg/config/draftconfig.go +++ b/pkg/config/draftconfig.go @@ -32,22 +32,29 @@ type DraftConfig struct { } 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 { diff --git a/pkg/config/draftconfig_template_test.go b/pkg/config/draftconfig_template_test.go index c7e705a7..83bc06b5 100644 --- a/pkg/config/draftconfig_template_test.go +++ b/pkg/config/draftconfig_template_test.go @@ -107,6 +107,7 @@ func loadTemplatesWithValidation() error { // } referenceVarMap := map[string]*BuilderVar{} + conditionRefMap := map[string]*BuilderVar{} allVariables := map[string]*BuilderVar{} for _, variable := range currTemplate.Variables { if variable.Name == "" { @@ -130,20 +131,39 @@ func loadTemplatesWithValidation() error { 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 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 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 currVar.Name == refVar.Name { + return fmt.Errorf("template %s has a variable with cyclical conditional 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: %s", path, currVar.Name) } } @@ -152,7 +172,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 } @@ -171,5 +191,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) } From 6e670b4f20d5ea08b26ece6cd4a6ec03a15fa6df Mon Sep 17 00:00:00 2001 From: Brandon Foley Date: Tue, 29 Oct 2024 14:35:57 -0400 Subject: [PATCH 5/9] updating templates --- pkg/config/draftconfig_template_test.go | 8 +++++ pkg/fixtures/manifests/hpa/hpa.yaml | 22 ++++++++++++ pkg/fixtures/manifests/pdb/pdb.yaml | 13 +++++++ pkg/fixtures/manifests/service/service.yaml | 16 +++++++++ pkg/handlers/template_test.go | 36 +++++++++++++++++++ .../manifests}/draft.yaml | 11 +++++- .../manifests/hpa.yaml | 22 ++++++++++++ .../manifest/hpa.yaml | 22 ------------ .../PodDisruptionBudget/manifest/pdb.yaml | 13 ------- .../{manifest => manifests}/draft.yaml | 8 ++++- .../PodDisruptionBudget/manifests/pdb.yaml | 13 +++++++ .../Service/{ => manifests}/draft.yaml | 9 ++++- .../manifests/Service/manifests/service.yaml | 16 +++++++++ template/manifests/Service/service.yaml | 16 --------- 14 files changed, 171 insertions(+), 54 deletions(-) create mode 100644 pkg/fixtures/manifests/hpa/hpa.yaml create mode 100644 pkg/fixtures/manifests/pdb/pdb.yaml create mode 100644 pkg/fixtures/manifests/service/service.yaml rename template/manifests/{HorizontalPodAutoscaling/manifest => HorizontalPodAutoscaler/manifests}/draft.yaml (83%) create mode 100644 template/manifests/HorizontalPodAutoscaler/manifests/hpa.yaml delete mode 100644 template/manifests/HorizontalPodAutoscaling/manifest/hpa.yaml delete mode 100644 template/manifests/PodDisruptionBudget/manifest/pdb.yaml rename template/manifests/PodDisruptionBudget/{manifest => manifests}/draft.yaml (80%) create mode 100644 template/manifests/PodDisruptionBudget/manifests/pdb.yaml rename template/manifests/Service/{ => manifests}/draft.yaml (81%) create mode 100644 template/manifests/Service/manifests/service.yaml delete mode 100644 template/manifests/Service/service.yaml diff --git a/pkg/config/draftconfig_template_test.go b/pkg/config/draftconfig_template_test.go index 83bc06b5..ec661142 100644 --- a/pkg/config/draftconfig_template_test.go +++ b/pkg/config/draftconfig_template_test.go @@ -3,6 +3,7 @@ package config import ( "fmt" "io/fs" + "regexp" "strings" "testing" @@ -10,6 +11,8 @@ import ( "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{ @@ -67,6 +70,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 @@ -93,6 +97,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) } diff --git a/pkg/fixtures/manifests/hpa/hpa.yaml b/pkg/fixtures/manifests/hpa/hpa.yaml new file mode 100644 index 00000000..dd13b9cd --- /dev/null +++ b/pkg/fixtures/manifests/hpa/hpa.yaml @@ -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 \ No newline at end of file diff --git a/pkg/fixtures/manifests/pdb/pdb.yaml b/pkg/fixtures/manifests/pdb/pdb.yaml new file mode 100644 index 00000000..40e9063f --- /dev/null +++ b/pkg/fixtures/manifests/pdb/pdb.yaml @@ -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 \ No newline at end of file diff --git a/pkg/fixtures/manifests/service/service.yaml b/pkg/fixtures/manifests/service/service.yaml new file mode 100644 index 00000000..802987ce --- /dev/null +++ b/pkg/fixtures/manifests/service/service.yaml @@ -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 \ No newline at end of file diff --git a/pkg/handlers/template_test.go b/pkg/handlers/template_test.go index 02d3946d..29d3bb15 100644 --- a/pkg/handlers/template_test.go +++ b/pkg/handlers/template_test.go @@ -422,6 +422,42 @@ func TestTemplateHandlerValidation(t *testing.T) { }, expectedErr: fmt.Errorf("invalid label: *myTestApp"), }, + { + name: "valid hpa manifest", + templateName: "horizontalPodAutoscaler-manifests", + fixturesBaseDir: "../fixtures/manifests/hpa", + version: "0.0.1", + dest: ".", + templateWriter: &writers.FileMapWriter{}, + varMap: map[string]string{ + "APPNAME": "test-app", + "PARTOF": "test-app-project", + }, + }, + { + name: "valid pdb manifest", + templateName: "podDisruptionBudget-manifests", + fixturesBaseDir: "../fixtures/manifests/pdb", + version: "0.0.1", + dest: ".", + templateWriter: &writers.FileMapWriter{}, + varMap: map[string]string{ + "APPNAME": "test-app", + "PARTOF": "test-app-project", + }, + }, + { + name: "valid service manifest", + templateName: "service-manifests", + fixturesBaseDir: "../fixtures/manifests/service", + version: "0.0.1", + dest: ".", + templateWriter: &writers.FileMapWriter{}, + varMap: map[string]string{ + "APPNAME": "test-app", + "PARTOF": "test-app-project", + }, + }, } for _, tt := range tests { diff --git a/template/manifests/HorizontalPodAutoscaling/manifest/draft.yaml b/template/manifests/HorizontalPodAutoscaler/manifests/draft.yaml similarity index 83% rename from template/manifests/HorizontalPodAutoscaling/manifest/draft.yaml rename to template/manifests/HorizontalPodAutoscaler/manifests/draft.yaml index 85aa7fb0..c2dcc98e 100644 --- a/template/manifests/HorizontalPodAutoscaling/manifest/draft.yaml +++ b/template/manifests/HorizontalPodAutoscaler/manifests/draft.yaml @@ -1,42 +1,51 @@ -templateName: "horizontalPodAutoscaling-manifest" +templateName: "horizontalPodAutoscaler-manifests" description: "This template is used to create a horizontalPodAutoscaling for an application" +versions: "0.0.1" +defaultVersion: "0.0.1" type: "manifest" variables: - name: "APPNAME" type: "string" kind: "kubernetesResourceName" description: "the name of the application" + versions: ">=0.0.1" - name: "PARTOF" type: "string" kind: "label" description: "the label to identify which project the resource belong to" + versions: ">=0.0.1" - name: "GENERATORLABEL" type: "string" kind: "label" description: "the label to identify who generated the resource" + versions: ">=0.0.1" default: value: "draft" - name: "MINIMUMREPLICAS" type: "int" kind: "replicaCount" description: "specifies the minimum number of pod replicas that the deployment should have" + versions: ">=0.0.1" default: value: 2 - name: "MAXIMUMREPLICAS" type: "int" kind: "replicaCount" description: "defines the maximum number of pod replicas the deployment can scale to" + versions: ">=0.0.1" default: value: 5 - name: "RESOURCETYPE" type: "string" kind: "scalingResourceType" description: "specifies the resource type (e.g., cpu or memory) to be monitored for scaling" + versions: ">=0.0.1" default: value: "cpu" - name: "AVGUTILIZATION" type: "int" kind: "scalingResourceUtilization" description: "specifies the average utilization for the monitored resource, triggering scaling when exceeded" + versions: ">=0.0.1" default: value: 80 \ No newline at end of file diff --git a/template/manifests/HorizontalPodAutoscaler/manifests/hpa.yaml b/template/manifests/HorizontalPodAutoscaler/manifests/hpa.yaml new file mode 100644 index 00000000..be8bb34a --- /dev/null +++ b/template/manifests/HorizontalPodAutoscaler/manifests/hpa.yaml @@ -0,0 +1,22 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ .Config.GetVariableValue "APPNAME" }} + labels: + app.kubernetes.io/name: {{ .Config.GetVariableValue "APPNAME" }} + app.kubernetes.io/part-of: {{ .Config.GetVariableValue "PARTOF" }} + kubernetes.azure.com/generator: {{ .Config.GetVariableValue "GENERATORLABEL" }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ .Config.GetVariableValue "APPNAME" }} + minReplicas: {{ .Config.GetVariableValue "MINIMUMREPLICAS" }} + maxReplicas: {{ .Config.GetVariableValue "MAXIMUMREPLICAS" }} + metrics: + - type: Resource + resource: + name: {{ .Config.GetVariableValue "RESOURCETYPE" }} + target: + type: Utilization + averageUtilization: {{ .Config.GetVariableValue "AVGUTILIZATION"}} \ No newline at end of file diff --git a/template/manifests/HorizontalPodAutoscaling/manifest/hpa.yaml b/template/manifests/HorizontalPodAutoscaling/manifest/hpa.yaml deleted file mode 100644 index 3250dd6e..00000000 --- a/template/manifests/HorizontalPodAutoscaling/manifest/hpa.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{.APPNAME}} - labels: - app.kubernetes.io/name: {{.APPNAME}} - app.kubernetes.io/part-of: {{.PARTOF}} - kubernetes.azure.com/generator: {{.GENERATORLABEL}} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{.APPNAME}} - minReplicas: {{.MINIMUMREPLICAS}} - maxReplicas: {{.MAXIMUMREPLICAS}} - metrics: - - type: Resource - resource: - name: {{.RESOURCETYPE}} - target: - type: Utilization - averageUtilization: {{.AVGUTILIZATION}} \ No newline at end of file diff --git a/template/manifests/PodDisruptionBudget/manifest/pdb.yaml b/template/manifests/PodDisruptionBudget/manifest/pdb.yaml deleted file mode 100644 index 71f25c83..00000000 --- a/template/manifests/PodDisruptionBudget/manifest/pdb.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - name: {{.APPNAME}} - labels: - app.kubernetes.io/name: {{.APPNAME}} - app.kubernetes.io/part-of: {{.PARTOF}} - kubernetes.azure.com/generator: {{.GENERATORLABEL}} -spec: - maxUnavailable: {{.MAXUNAVAILABLE}} - selector: - matchLabels: - app: {{.APPNAME}} \ No newline at end of file diff --git a/template/manifests/PodDisruptionBudget/manifest/draft.yaml b/template/manifests/PodDisruptionBudget/manifests/draft.yaml similarity index 80% rename from template/manifests/PodDisruptionBudget/manifest/draft.yaml rename to template/manifests/PodDisruptionBudget/manifests/draft.yaml index 58bc20fa..7602d2d4 100644 --- a/template/manifests/PodDisruptionBudget/manifest/draft.yaml +++ b/template/manifests/PodDisruptionBudget/manifests/draft.yaml @@ -1,24 +1,30 @@ -templateName: "podDisruptionBudget-manifest" +templateName: "podDisruptionBudget-manifests" description: "This template is used to create a PodDisruptionBudget for an application" +versions: "0.0.1" +defaultVersions: "0.0.1" type: "manifest" variables: - name: "APPNAME" type: "string" kind: "kubernetesResourceName" description: "the name of the application" + versions: ">=0.0.1" - name: "PARTOF" type: "string" kind: "label" description: "the label to identify which project the resource belong to" + versions: ">=0.0.1" - name: "GENERATORLABEL" type: "string" kind: "label" description: "the label to identify who generated the resource" + versions: ">=0.0.1" default: value: "draft" - name: "MAXUNAVAILABLE" type: "int" kind: "resourceLimit" description: "specifies the maximum number of pods that can be unavailable during a disruption, such as a pod eviction" + versions: ">=0.0.1" default: value: 1 \ No newline at end of file diff --git a/template/manifests/PodDisruptionBudget/manifests/pdb.yaml b/template/manifests/PodDisruptionBudget/manifests/pdb.yaml new file mode 100644 index 00000000..a9ea9bb2 --- /dev/null +++ b/template/manifests/PodDisruptionBudget/manifests/pdb.yaml @@ -0,0 +1,13 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{.Config.GetVariableValue "APPNAME" }} + labels: + app.kubernetes.io/name: {{ .Config.GetVariableValue "APPNAME"}} + app.kubernetes.io/part-of: {{ .Config.GetVariableValue "PARTOF" }} + kubernetes.azure.com/generator: {{ .Config.GetVariableValue "GENERATORLABEL"}} +spec: + maxUnavailable: {{ .Config.GetVariableValue "MAXUNAVAILABLE" }} + selector: + matchLabels: + app: {{ .Config.GetVariableValue "APPNAME" }} \ No newline at end of file diff --git a/template/manifests/Service/draft.yaml b/template/manifests/Service/manifests/draft.yaml similarity index 81% rename from template/manifests/Service/draft.yaml rename to template/manifests/Service/manifests/draft.yaml index b66fc0c1..3a884919 100644 --- a/template/manifests/Service/draft.yaml +++ b/template/manifests/Service/manifests/draft.yaml @@ -1,30 +1,37 @@ -templateName: "Service" +templateName: "service-manifests" description: "This template is used to create a generic Service for an application" +versions: "0.0.1" +defaultVersion: "0.0.1" type: "manifest" variables: - name: "PORT" type: "int" kind: "port" description: "the port the service uses to make the application accessible from outside the cluster" + versions: ">=0.0.1" default: value: 80 - name: "APPNAME" type: "string" kind: "kubernetesResourceName" description: "the name of the application" + versions: ">=0.0.1" - name: "PARTOF" type: "string" kind: "label" description: "the label to identify which project the resource belong to" + versions: ">=0.0.1" - name: "GENERATORLABEL" type: "string" kind: "label" description: "the label to identify who generated the resource" + versions: ">=0.0.1" default: value: "draft" - name: "TARGETPORT" type: "int" kind: "port" description: "the port exposed in the application" + versions: ">=0.0.1" default: referenceVar: "PORT" \ No newline at end of file diff --git a/template/manifests/Service/manifests/service.yaml b/template/manifests/Service/manifests/service.yaml new file mode 100644 index 00000000..4cac4368 --- /dev/null +++ b/template/manifests/Service/manifests/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Config.GetVariableValue "APPNAME" }} + labels: + app.kubernetes.io/name: {{ .Config.GetVariableValue "APPNAME" }} + app.kubernetes.io/part-of: {{ .Config.GetVariableValue "PARTOF" }} + kubernetes.azure.com/generator: {{ .Config.GetVariableValue "GENERATORLABEL" }} +spec: + type: ClusterIP + selector: + app: {{ .Config.GetVariableValue "APPNAME" }} + ports: + - protocol: TCP + port: {{ .Config.GetVariableValue "PORT" }} + targetPort: {{ .Config.GetVariableValue "TARGETPORT" }} \ No newline at end of file diff --git a/template/manifests/Service/service.yaml b/template/manifests/Service/service.yaml deleted file mode 100644 index 2ab33e33..00000000 --- a/template/manifests/Service/service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{.APPNAME}} - labels: - app.kubernetes.io/name: {{.APPNAME}} - app.kubernetes.io/part-of: {{.PARTOF}} - kubernetes.azure.com/generator: {{.GENERATORLABEL}} -spec: - type: ClusterIP - selector: - app: {{.APPNAME}} - ports: - - protocol: TCP - port: {{.PORT}} - targetPort: {{.TARGETPORT}} \ No newline at end of file From 99f5994329fe3b82281c2e126d1fd974d0c94a67 Mon Sep 17 00:00:00 2001 From: Brandon Foley Date: Wed, 30 Oct 2024 12:58:36 -0400 Subject: [PATCH 6/9] updates --- pkg/handlers/template_test.go | 73 +++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/pkg/handlers/template_test.go b/pkg/handlers/template_test.go index 29d3bb15..7083f857 100644 --- a/pkg/handlers/template_test.go +++ b/pkg/handlers/template_test.go @@ -312,9 +312,9 @@ func TestTemplateHandlerValidation(t *testing.T) { }, }, { - name: "valid azpipeline manifest deployment", - templateName: "azure-pipeline-manifest", - fixturesBaseDir: "../fixtures/workflows/azurepipelines/manifest", + name: "valid azpipeline manifests deployment", + templateName: "azure-pipeline-manifests", + fixturesBaseDir: "../fixtures/workflows/azurepipelines/manifests", version: "0.0.1", dest: ".", templateWriter: &writers.FileMapWriter{}, @@ -422,6 +422,73 @@ func TestTemplateHandlerValidation(t *testing.T) { }, expectedErr: fmt.Errorf("invalid label: *myTestApp"), }, + { + name: "valid helm workflow", + templateName: "github-workflow-helm", + fixturesBaseDir: "../fixtures/workflows/github/helm", + version: "0.0.1", + dest: ".", + templateWriter: &writers.FileMapWriter{}, + varMap: map[string]string{ + "WORKFLOWNAME": "testWorkflow", + "BRANCHNAME": "testBranch", + "ACRRESOURCEGROUP": "testAcrRG", + "AZURECONTAINERREGISTRY": "testAcr", + "CONTAINERNAME": "testContainer", + "CLUSTERRESOURCEGROUP": "testClusterRG", + "CLUSTERNAME": "testCluster", + "KUSTOMIZEPATH": "./overlays/production", + "DEPLOYMENTMANIFESTPATH": "./manifests", + "DOCKERFILE": "./Dockerfile", + "BUILDCONTEXTPATH": "test", + "CHARTPATH": "testPath", + "CHARTOVERRIDEPATH": "testOverridePath", + "CHARTOVERRIDES": "replicas:2", + "NAMESPACE": "default", + }, + }, + { + name: "valid helm workflow", + templateName: "github-workflow-kustomize", + fixturesBaseDir: "../fixtures/workflows/github/kustomize", + version: "0.0.1", + dest: ".", + templateWriter: &writers.FileMapWriter{}, + varMap: map[string]string{ + "WORKFLOWNAME": "testWorkflow", + "BRANCHNAME": "testBranch", + "ACRRESOURCEGROUP": "testAcrRG", + "AZURECONTAINERREGISTRY": "testAcr", + "CONTAINERNAME": "testContainer", + "CLUSTERRESOURCEGROUP": "testClusterRG", + "CLUSTERNAME": "testCluster", + "DEPLOYMENTMANIFESTPATH": "./manifests", + "DOCKERFILE": "./Dockerfile", + "BUILDCONTEXTPATH": "test", + "NAMESPACE": "default", + }, + }, + { + name: "valid manifest workflow", + templateName: "github-workflow-manifests", + fixturesBaseDir: "../fixtures/workflows/github/manifests", + version: "0.0.1", + dest: ".", + templateWriter: &writers.FileMapWriter{}, + varMap: map[string]string{ + "WORKFLOWNAME": "testWorkflow", + "BRANCHNAME": "testBranch", + "ACRRESOURCEGROUP": "testAcrRG", + "AZURECONTAINERREGISTRY": "testAcr", + "CONTAINERNAME": "testContainer", + "CLUSTERRESOURCEGROUP": "testClusterRG", + "CLUSTERNAME": "testCluster", + "DEPLOYMENTMANIFESTPATH": "./manifests", + "DOCKERFILE": "./Dockerfile", + "BUILDCONTEXTPATH": "test", + "NAMESPACE": "default", + }, + }, { name: "valid hpa manifest", templateName: "horizontalPodAutoscaler-manifests", From 63c29439a801f4e5cab3d52a9e751f7a66051570 Mon Sep 17 00:00:00 2001 From: Brandon Foley Date: Wed, 30 Oct 2024 13:26:07 -0400 Subject: [PATCH 7/9] missed this save --- pkg/handlers/template_test.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pkg/handlers/template_test.go b/pkg/handlers/template_test.go index e0c6aeca..7083f857 100644 --- a/pkg/handlers/template_test.go +++ b/pkg/handlers/template_test.go @@ -360,7 +360,6 @@ func TestTemplateHandlerValidation(t *testing.T) { }, }, { -<<<<<<< HEAD name: "manifest deployment vars with err from validators", templateName: "deployment-manifests", fixturesBaseDir: "../fixtures/deployments/manifest", @@ -424,8 +423,6 @@ func TestTemplateHandlerValidation(t *testing.T) { expectedErr: fmt.Errorf("invalid label: *myTestApp"), }, { -======= ->>>>>>> origin name: "valid helm workflow", templateName: "github-workflow-helm", fixturesBaseDir: "../fixtures/workflows/github/helm", @@ -449,12 +446,8 @@ func TestTemplateHandlerValidation(t *testing.T) { "CHARTOVERRIDES": "replicas:2", "NAMESPACE": "default", }, -<<<<<<< HEAD }, { -======= - }, { ->>>>>>> origin name: "valid helm workflow", templateName: "github-workflow-kustomize", fixturesBaseDir: "../fixtures/workflows/github/kustomize", @@ -474,12 +467,8 @@ func TestTemplateHandlerValidation(t *testing.T) { "BUILDCONTEXTPATH": "test", "NAMESPACE": "default", }, -<<<<<<< HEAD }, { -======= - }, { ->>>>>>> origin name: "valid manifest workflow", templateName: "github-workflow-manifests", fixturesBaseDir: "../fixtures/workflows/github/manifests", @@ -500,7 +489,6 @@ func TestTemplateHandlerValidation(t *testing.T) { "NAMESPACE": "default", }, }, -<<<<<<< HEAD { name: "valid hpa manifest", templateName: "horizontalPodAutoscaler-manifests", @@ -537,8 +525,6 @@ func TestTemplateHandlerValidation(t *testing.T) { "PARTOF": "test-app-project", }, }, -======= ->>>>>>> origin } for _, tt := range tests { From 729ef1703d125fe9585ab32cf541a15f7af57518 Mon Sep 17 00:00:00 2001 From: Brandon Foley Date: Wed, 30 Oct 2024 13:28:04 -0400 Subject: [PATCH 8/9] Add version check into validation --- pkg/config/draftconfig_template_test.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pkg/config/draftconfig_template_test.go b/pkg/config/draftconfig_template_test.go index ec661142..22066a9d 100644 --- a/pkg/config/draftconfig_template_test.go +++ b/pkg/config/draftconfig_template_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/Azure/draft/template" + "github.com/blang/semver/v4" "github.com/stretchr/testify/assert" ) @@ -109,10 +110,9 @@ 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{} @@ -130,10 +130,9 @@ 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 != "" { From 9b4a6de380e28eaacdec167be1ebe6de4a190701 Mon Sep 17 00:00:00 2001 From: Brandon Foley Date: Fri, 1 Nov 2024 11:15:38 -0400 Subject: [PATCH 9/9] validation error message updates --- pkg/config/draftconfig_template_test.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/config/draftconfig_template_test.go b/pkg/config/draftconfig_template_test.go index 22066a9d..6eac2791 100644 --- a/pkg/config/draftconfig_template_test.go +++ b/pkg/config/draftconfig_template_test.go @@ -165,12 +165,8 @@ func loadTemplatesWithValidation() error { 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 currVar.Name == refVar.Name { - return fmt.Errorf("template %s has a variable with cyclical conditional 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: %s", path, currVar.Name) + return fmt.Errorf("template %s has a variable with cyclical conditional reference to itself or references a non existing variable: %s", path, currVar.Name) } }