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

Updating templates and moving tests #428

Merged
merged 11 commits into from
Nov 21, 2024
130 changes: 109 additions & 21 deletions pkg/config/draftconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ import (
"github.com/blang/semver/v4"
)

type VariableCondition string

const (
EqualTo VariableCondition = "equals"
NotEqualTo VariableCondition = "notequals"
)

func (v VariableCondition) String() string {
return string(v)
}

const draftConfigFile = "draft.yaml"

type VariableValidator func(string) error
Expand All @@ -32,15 +43,16 @@ type DraftConfig struct {
}

type BuilderVar struct {
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"`
Name string `yaml:"name"`
ActiveWhenConstraints []ActiveWhenConstraint `yaml:"activeWhen"`
Default BuilderVarDefault `yaml:"default"`
Description string `yaml:"description"`
ExampleValues []string `yaml:"exampleValues"`
AllowedValues []string `yaml:"allowedValues"`
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
Expand All @@ -50,9 +62,11 @@ type BuilderVarDefault struct {
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"`
// ActiveWhenConstraints holds information on when a variable is actively used by a template based off other variable values
type ActiveWhenConstraint struct {
VariableName string `yaml:"variableName"`
Value string `yaml:"value"`
Condition VariableCondition `yaml:"condition"`
}

func NewConfigFromFS(fileSys fs.FS, path string) (*DraftConfig, error) {
Expand Down Expand Up @@ -188,6 +202,15 @@ func (d *DraftConfig) ApplyDefaultVariables() error {
variable.Value = defaultVal
}

isVarActive, err := d.CheckActiveWhenConstraint(variable)
if err != nil {
return fmt.Errorf("unable to check ActiveWhen constraint: %w", err)
}

if !isVarActive {
continue
}

if variable.Value == "" {
if variable.Default.Value != "" {
log.Infof("Variable %s defaulting to value %s", variable.Name, variable.Default.Value)
Expand Down Expand Up @@ -244,6 +267,15 @@ func (d *DraftConfig) ApplyDefaultVariablesForVersion(version string) error {
variable.Value = defaultVal
}

isVarActive, err := d.CheckActiveWhenConstraint(variable)
if err != nil {
return fmt.Errorf("unable to check ActiveWhen constraint: %w", err)
}

if !isVarActive {
continue
}

if variable.Value == "" {
if variable.Default.Value != "" {
log.Infof("Variable %s defaulting to value %s", variable.Name, variable.Default.Value)
Expand All @@ -258,6 +290,49 @@ func (d *DraftConfig) ApplyDefaultVariablesForVersion(version string) error {
return nil
}

func (d *DraftConfig) CheckActiveWhenConstraint(variable *BuilderVar) (bool, error) {
if len(variable.ActiveWhenConstraints) > 0 {
isVarActive := true
for _, activeWhen := range variable.ActiveWhenConstraints {
refVar, err := d.GetVariable(activeWhen.VariableName)
if err != nil {
return false, fmt.Errorf("unable to get ActiveWhen reference variable: %w", err)
}

checkValue := refVar.Value
if checkValue == "" {
if refVar.Default.Value != "" {
checkValue = refVar.Default.Value
}

if refVar.Default.ReferenceVar != "" {
refValue, err := d.recurseReferenceVars(refVar, refVar, true)
if err != nil {
return false, err
}
if refValue == "" {
return false, errors.New("reference variable has no value")
}

checkValue = refValue
}
}

switch activeWhen.Condition {
case EqualTo:
isVarActive = checkValue == activeWhen.Value
case NotEqualTo:
isVarActive = checkValue != activeWhen.Value
default:
return false, fmt.Errorf("invalid activeWhen condition: %s", activeWhen.Condition)
}
}
return isVarActive, nil
}

return true, nil
}

// recurseReferenceVars recursively checks each variable's ReferenceVar if it doesn't have a custom input. If there's no more ReferenceVars, it will return the default value of the last ReferenceVar.
func (d *DraftConfig) recurseReferenceVars(referenceVar *BuilderVar, variableCheck *BuilderVar, isFirst bool) (string, error) {
if !isFirst && referenceVar.Name == variableCheck.Name {
Expand Down Expand Up @@ -319,20 +394,33 @@ func (d *DraftConfig) DeepCopy() *DraftConfig {

func (bv *BuilderVar) DeepCopy() *BuilderVar {
newVar := &BuilderVar{
Name: bv.Name,
Default: bv.Default,
Description: bv.Description,
Type: bv.Type,
Kind: bv.Kind,
Value: bv.Value,
Versions: bv.Versions,
ExampleValues: make([]string, len(bv.ExampleValues)),
Name: bv.Name,
Default: bv.Default,
Description: bv.Description,
Type: bv.Type,
Kind: bv.Kind,
Value: bv.Value,
Versions: bv.Versions,
ExampleValues: make([]string, len(bv.ExampleValues)),
AllowedValues: make([]string, len(bv.AllowedValues)),
ActiveWhenConstraints: make([]ActiveWhenConstraint, len(bv.ActiveWhenConstraints)),
}

for i, awc := range bv.ActiveWhenConstraints {
newVar.ActiveWhenConstraints[i] = *awc.DeepCopy()
}
copy(newVar.AllowedValues, bv.AllowedValues)
copy(newVar.ExampleValues, bv.ExampleValues)
return newVar
}

func (awc ActiveWhenConstraint) DeepCopy() *ActiveWhenConstraint {
return &ActiveWhenConstraint{
VariableName: awc.VariableName,
Value: awc.Value,
Condition: awc.Condition,
}
}

// TemplateVariableRecorder is an interface for recording variables that are read using draft configs
type TemplateVariableRecorder interface {
Record(key, value string)
Expand Down
63 changes: 35 additions & 28 deletions pkg/config/draftconfig_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"io/fs"
"regexp"
"slices"
"strings"
"testing"

Expand Down Expand Up @@ -44,11 +45,14 @@ var validVariableKinds = map[string]bool{
"filePath": true,
"flag": true,
"helmChartOverrides": true,
"imagePullPolicy": true,
"ingressHostName": true,
"kubernetesNamespace": true,
"kubernetesProbeHttpPath": true,
"kubernetesProbePeriod": true,
"kubernetesProbeTimeout": true,
"kubernetesProbeThreshold": true,
"kubernetesProbeType": true,
"kubernetesProbeDelay": true,
"kubernetesResourceLimit": true,
"kubernetesResourceName": true,
Expand Down Expand Up @@ -122,7 +126,7 @@ func loadTemplatesWithValidation() error {
}

referenceVarMap := map[string]*BuilderVar{}
conditionRefMap := map[string]*BuilderVar{}
activeWhenRefMap := map[string]*BuilderVar{}
allVariables := map[string]*BuilderVar{}
for _, variable := range currTemplate.Variables {
if variable.Name == "" {
Expand All @@ -146,8 +150,13 @@ func loadTemplatesWithValidation() error {
referenceVarMap[variable.Name] = variable
}

if variable.ConditionalRef.ReferenceVar != "" {
conditionRefMap[variable.Name] = variable
for _, activeWhen := range variable.ActiveWhenConstraints {
if activeWhen.VariableName != "" {
activeWhenRefMap[variable.Name] = variable
}
if !isValidVariableCondition(activeWhen.Condition) {
return fmt.Errorf("template %s has a variable %s with an invalid activeWhen condition: %s", path, variable.Name, activeWhen.Condition)
}
}
}

Expand All @@ -166,14 +175,25 @@ func loadTemplatesWithValidation() error {
}
}

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 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)
for _, currVar := range activeWhenRefMap {

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

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

if refVar.Type == "bool" {
if activeWhen.Value != "true" && activeWhen.Value != "false" {
return fmt.Errorf("template %s has a variable %s with ActiveWhen reference to a non-boolean value: %s", path, currVar.Name, activeWhen.Value)
}
} else if !slices.Contains(refVar.AllowedValues, activeWhen.Value) {
return fmt.Errorf("template %s has a variable %s with ActiveWhen reference to a non-existent allowed value: %s", path, currVar.Name, activeWhen.Value)
}
}
}

Expand Down Expand Up @@ -204,24 +224,11 @@ func isCyclicalDefaultVariableReference(initialVar, currRefVar *BuilderVar, allV
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 {
func isValidVariableCondition(condition VariableCondition) bool {
switch condition {
case EqualTo, NotEqualTo:
return true
}

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

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

visited[currRefVar.Name] = true
return isCyclicalConditionalVariableReference(initialVar, refVar, allVariables, visited)
}
41 changes: 37 additions & 4 deletions pkg/config/validators/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,52 @@ import (
func GetValidator(variableKind string) func(string) error {
switch variableKind {
case "envVarMap":
return KeyValueMapValidator
return keyValueMapValidator
case "imagePullPolicy":
return imagePullPolicyValidator
case "kubernetesProbeType":
return kubernetesProbeTypeValidator
case "scalingResourceType":
return scalingResourceTypeValidator
default:
return DefaultValidator
return defaultValidator
}
}

func KeyValueMapValidator(input string) error {
func imagePullPolicyValidator(input string) error {
switch input {
case "Always", "IfNotPresent", "Never":
return nil
default:
return fmt.Errorf("invalid image pull policy: %s. valid values: Always, IfNotPresent, Never", input)
}
}

func scalingResourceTypeValidator(input string) error {
switch input {
case "cpu", "memory":
return nil
default:
return fmt.Errorf("invalid scaling resource type: %s. valid values: cpu, memory", input)
}
}

func kubernetesProbeTypeValidator(input string) error {
switch input {
case "httpGet", "tcpSocket":
return nil
default:
return fmt.Errorf("invalid probe type: %s. valid values: httpGet, tcpSocket", input)
}
}

func keyValueMapValidator(input string) error {
if err := json.Unmarshal([]byte(input), &map[string]string{}); err != nil {
return fmt.Errorf("failed to unmarshal variable as map[string]string: %s", err)
}
return nil
}

func DefaultValidator(input string) error {
func defaultValidator(input string) error {
return nil
}
26 changes: 25 additions & 1 deletion pkg/config/validators/validators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,29 @@ func TestGetValidator(t *testing.T) {
}

func TestDefaultValidator(t *testing.T) {
assert.Nil(t, DefaultValidator("test"))
assert.Nil(t, defaultValidator("test"))
}

func TestKubernetesProbeTypeValidator(t *testing.T) {
assert.Nil(t, kubernetesProbeTypeValidator("httpGet"))
assert.Nil(t, kubernetesProbeTypeValidator("tcpSocket"))
assert.NotNil(t, kubernetesProbeTypeValidator("exec"))
}

func TestKeyValueMapValidator(t *testing.T) {
assert.Nil(t, keyValueMapValidator(`{"key": "value"}`))
assert.NotNil(t, keyValueMapValidator(`{"key": "value"`))
}

func TestScalingResourceTypeValidator(t *testing.T) {
assert.Nil(t, scalingResourceTypeValidator("cpu"))
assert.Nil(t, scalingResourceTypeValidator("memory"))
assert.NotNil(t, scalingResourceTypeValidator("disk"))
}

func TestImagePullPolicyValidator(t *testing.T) {
assert.Nil(t, imagePullPolicyValidator("Always"))
assert.Nil(t, imagePullPolicyValidator("IfNotPresent"))
assert.Nil(t, imagePullPolicyValidator("Never"))
assert.NotNil(t, imagePullPolicyValidator("Sometimes"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,16 @@ spec:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
startupProbe:
{{- toYaml .Values.startupProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
envFrom:
- configMapRef:
name: {{ include "testapp.fullname" . }}-config
- secretRef:
name: secret-ref
optional: true
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
Expand Down
Loading
Loading