pkg/config/draftconfig.go (345 lines of code) (raw):
package config
import (
"errors"
"fmt"
"io/fs"
"slices"
"github.com/Azure/draft/pkg/config/transformers"
"github.com/Azure/draft/pkg/config/validators"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"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
type VariableTransformer func(string) (any, 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"`
Validators map[string]VariableValidator `yaml:"validators"`
Transformers map[string]VariableTransformer `yaml:"transformers"`
}
type BuilderVar struct {
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
type BuilderVarDefault struct {
IsPromptDisabled bool `yaml:"disablePrompt"`
ReferenceVar string `yaml:"referenceVar"`
Value string `yaml:"value"`
}
// 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) {
configBytes, err := fs.ReadFile(fileSys, path)
if err != nil {
return nil, err
}
var draftConfig DraftConfig
if err = yaml.Unmarshal(configBytes, &draftConfig); err != nil {
return nil, err
}
return &draftConfig, nil
}
func (d *DraftConfig) GetVariableExampleValues() map[string][]string {
variableExampleValues := make(map[string][]string)
for _, variable := range d.Variables {
if len(variable.ExampleValues) > 0 {
variableExampleValues[variable.Name] = variable.ExampleValues
}
}
return variableExampleValues
}
// Returns a map of variable names to values used in Gotemplate
func (d *DraftConfig) GetVariableMap() map[string]string {
variableMap := make(map[string]string)
for _, variable := range d.Variables {
variableMap[variable.Name] = variable.Value
}
return variableMap
}
func (d *DraftConfig) GetVariable(name string) (*BuilderVar, error) {
for _, variable := range d.Variables {
if variable.Name == name {
return variable, nil
}
}
return nil, fmt.Errorf("variable %s not found", name)
}
func (d *DraftConfig) GetVariableValue(name string) (any, error) {
for _, variable := range d.Variables {
if variable.Name == name {
if variable.Value == "" {
return "", fmt.Errorf("variable %s has no value", name)
}
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
}
}
return "", fmt.Errorf("variable %s not found", name)
}
func (d *DraftConfig) SetVariable(name, value string) {
if variable, err := d.GetVariable(name); err != nil {
d.Variables = append(d.Variables, &BuilderVar{
Name: name,
Value: value,
})
} else {
variable.Value = value
}
}
// 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 {
if variable.Value == "" {
if variable.Default.ReferenceVar != "" {
referenceVar, err := d.GetVariable(variable.Default.ReferenceVar)
if err != nil {
return fmt.Errorf("apply default variables: %w", err)
}
defaultVal, err := d.recurseReferenceVars(referenceVar, referenceVar, true)
if err != nil {
return fmt.Errorf("apply default variables: %w", err)
}
log.Infof("Variable %s defaulting to value %s", variable.Name, defaultVal)
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)
variable.Value = variable.Default.Value
} else {
return errors.New("variable " + variable.Name + " has no default value")
}
}
} else {
log.Infof("Variable %s already set to value %s", variable.Name, variable.Value)
}
}
return nil
}
// ApplyDefaultVariablesForVersion will apply the defaults to variables that are not already set for a specific template version
func (d *DraftConfig) ApplyDefaultVariablesForVersion(version string) error {
v, err := semver.Parse(version)
if err != nil {
return fmt.Errorf("invalid version: %w", err)
}
if !slices.Contains(d.Versions, version) {
return fmt.Errorf("requested version outside of valid versions: %s", version)
}
for _, variable := range d.Variables {
if variable.Value == "" {
expectedRange, err := semver.ParseRange(variable.Versions)
if err != nil {
return fmt.Errorf("invalid variable versions: %w", err)
}
if !expectedRange(v) {
log.Infof("Variable %s versions %s is outside input version %s, skipping", variable.Name, variable.Versions, version)
continue
}
if variable.Default.ReferenceVar != "" {
referenceVar, err := d.GetVariable(variable.Default.ReferenceVar)
if err != nil {
return fmt.Errorf("apply default variables: %w", err)
}
defaultVal, err := d.recurseReferenceVars(referenceVar, referenceVar, true)
if err != nil {
return fmt.Errorf("apply default variables: %w", err)
}
log.Infof("Variable %s defaulting to value %s", variable.Name, defaultVal)
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)
variable.Value = variable.Default.Value
} else {
return errors.New("variable " + variable.Name + " has no default value")
}
}
}
}
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 {
return "", errors.New("cyclical reference detected")
}
// If referenceVar has a custom value, return it, else check its ReferenceVar, else return its default value
if referenceVar.Value != "" {
return referenceVar.Value, nil
} else if referenceVar.Default.ReferenceVar != "" {
referenceVar, err := d.GetVariable(referenceVar.Default.ReferenceVar)
if err != nil {
return "", fmt.Errorf("recurse reference vars: %w", err)
}
return d.recurseReferenceVars(referenceVar, variableCheck, false)
}
return referenceVar.Default.Value, nil
}
// handles flags that are meant to represent template variables
func (d *DraftConfig) VariableMapToDraftConfig(flagVariablesMap map[string]string) {
for flagName, flagValue := range flagVariablesMap {
log.Debugf("flag variable %s=%s", flagName, flagValue)
d.SetVariable(flagName, flagValue)
}
}
// SetFileNameOverride sets the filename override for a specific file
func (d *DraftConfig) SetFileNameOverride(input, override string) {
if d.FileNameOverrideMap == nil {
d.FileNameOverrideMap = make(map[string]string)
}
d.FileNameOverrideMap[input] = override
}
func (d *DraftConfig) DeepCopy() *DraftConfig {
newConfig := &DraftConfig{
TemplateName: d.TemplateName,
DisplayName: d.DisplayName,
Description: d.Description,
Type: d.Type,
Versions: make([]string, len(d.Versions)),
DefaultVersion: d.DefaultVersion,
Variables: make([]*BuilderVar, len(d.Variables)),
FileNameOverrideMap: make(map[string]string),
}
for i, version := range d.Versions {
newConfig.Versions[i] = version
}
for i, variable := range d.Variables {
newConfig.Variables[i] = variable.DeepCopy()
}
for k, v := range d.FileNameOverrideMap {
newConfig.FileNameOverrideMap[k] = v
}
return newConfig
}
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)),
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)
}