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
143 changes: 122 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,28 @@ func (d *DraftConfig) ApplyDefaultVariables() error {
variable.Value = defaultVal
}

if len(variable.ActiveWhenConstraints) > 0 {
bfoley13 marked this conversation as resolved.
Show resolved Hide resolved
isVarActive := true
for _, activeWhen := range variable.ActiveWhenConstraints {
refVar, err := d.GetVariable(activeWhen.VariableName)
if err != nil {
return fmt.Errorf("unable to get ActiveWhen reference variable: %w", err)
}

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

if !isConditionTrue {
isVarActive = false
}
}
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 +280,28 @@ func (d *DraftConfig) ApplyDefaultVariablesForVersion(version string) error {
variable.Value = defaultVal
}

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

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

if !isConditionTrue {
isVarActive = false
}
}
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 +316,36 @@ func (d *DraftConfig) ApplyDefaultVariablesForVersion(version string) error {
return nil
}

func (d *DraftConfig) CheckActiveWhenConstraint(refVar *BuilderVar, activeWhen ActiveWhenConstraint) (bool, error) {
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:
return checkValue == activeWhen.Value, nil
case NotEqualTo:
return checkValue != activeWhen.Value, nil
}

return false, nil
bfoley13 marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 +407,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", input)
bfoley13 marked this conversation as resolved.
Show resolved Hide resolved
}
}

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

func kubernetesProbeTypeValidator(input string) error {
switch input {
case "httpGet", "tcpSocket":
return nil
default:
return fmt.Errorf("invalid probe type: %s", 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"))
}
Loading
Loading