Skip to content

Commit

Permalink
address comments
Browse files Browse the repository at this point in the history
  • Loading branch information
bfoley13 committed Nov 20, 2024
1 parent 0186b85 commit ede9b50
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 87 deletions.
145 changes: 122 additions & 23 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,16 +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"`
AllowedValues []string `yaml:"allowedValues"`
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 @@ -51,10 +62,11 @@ type BuilderVarDefault struct {
Value string `yaml:"value"`
}

// BuilderVarConditionalReference holds a reference to a variable thats value can effect usage/validation/transformation of the associated variable
type BuilderVarConditionalReference struct {
ReferenceVar string `yaml:"referenceVar"`
ConditionValue string `yaml:"conditionValue"`
// 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 @@ -190,6 +202,28 @@ func (d *DraftConfig) ApplyDefaultVariables() 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 Down Expand Up @@ -246,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 @@ -260,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
}

// 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 @@ -321,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
60 changes: 32 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 @@ -125,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 @@ -149,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 @@ -169,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 @@ -207,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)
}
22 changes: 22 additions & 0 deletions pkg/prompts/prompts.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ func RunPromptsFromConfigWithSkipsIO(draftConfig *config.DraftConfig, Stdin io.R
continue
}

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

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

if !isConditionTrue {
isVarActive = false
}
}
if !isVarActive {
continue
}
}

log.Debugf("constructing prompt for: %s", variable.Name)
if variable.Type == "bool" {
input, err := RunBoolPrompt(variable, Stdin, Stdout)
Expand Down
20 changes: 8 additions & 12 deletions template/deployments/helm/draft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,10 @@ variables:
- name: "PROBEHTTPPATH"
type: "string"
kind: "kubernetesProbeHttpPath"
default:
disablePrompt: true
value: "/"
conditionalReference:
variable: "PROBETYPE"
condition: "httpGet"
activeWhen:
- variableName: "PROBETYPE"
value: "httpGet"
condition: "equals"
description: "The path to use for the httpGet probes"
versions: ">=0.0.1"
- name: "STARTUPPERIOD"
Expand Down Expand Up @@ -218,12 +216,10 @@ variables:
- name: "SERVICEACCOUNT"
type: "string"
kind: "kubernetesResourceName"
conditionalReference:
variable: "ENABLEWORKLOADIDENTITY"
conditionValue: true
default:
disablePrompt: true
value: "service-account"
activeWhen:
- variableName: "ENABLEWORKLOADIDENTITY"
value: "true"
condition: "equals"
description: "the name of the service account to use with workload identity"
versions: ">=0.0.1"
- name: "ENVSECRETREF"
Expand Down
20 changes: 8 additions & 12 deletions template/deployments/kustomize/draft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,10 @@ variables:
- name: "PROBEHTTPPATH"
type: "string"
kind: "kubernetesProbeHttpPath"
default:
disablePrompt: true
value: "/"
conditionalReference:
variable: "PROBETYPE"
conditionValue: "httpGet"
activeWhen:
- variableName: "PROBETYPE"
value: "httpGet"
condition: "equals"
description: "The path to use for the httpGet probes"
versions: ">=0.0.1"
- name: "STARTUPPERIOD"
Expand Down Expand Up @@ -226,11 +224,9 @@ variables:
- name: "SERVICEACCOUNT"
type: "string"
kind: "kubernetesResourceName"
conditionalReference:
variable: "ENABLEWORKLOADIDENTITY"
conditionValue: true
default:
disablePrompt: true
value: "service-account"
activeWhen:
- variableName: "ENABLEWORKLOADIDENTITY"
value: "true"
condition: "equals"
description: "the name of the service account to use with workload identity"
versions: ">=0.0.1"
Loading

0 comments on commit ede9b50

Please sign in to comment.