variable.go (219 lines of code) (raw):
package golden
import (
"fmt"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/typeexpr"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"os"
"strings"
)
var _ Variable = &VariableBlock{}
var _ PrePlanBlock = &VariableBlock{}
var _ PlanBlock = &VariableBlock{}
var _ CustomDecode = &VariableBlock{}
var _ BlockCustomizedRefType = &VariableBlock{}
type Variable interface {
SingleValueBlock
Variable()
}
type VariableValidation struct {
Condition bool `hcl:"condition"`
ErrorMessage string `hcl:"error_message"`
}
type VariableBlock struct {
*BaseBlock
Description *string
Validations []VariableValidation
variableType *cty.Type
variableValue *cty.Value
}
func (v *VariableBlock) Decode(block *HclBlock, context *hcl.EvalContext) error {
return nil
}
func (v *VariableBlock) ExecuteDuringPlan() error {
return nil
}
func (v *VariableBlock) Type() string {
return ""
}
func (v *VariableBlock) BlockType() string {
return "variable"
}
func (v *VariableBlock) CustomizedRefType() string {
return "var"
}
func (v *VariableBlock) Address() string {
return fmt.Sprintf("var.%s", v.Name())
}
func (v *VariableBlock) AddressLength() int {
return 2
}
func (v *VariableBlock) CanExecutePrePlan() bool {
return true
}
func (v *VariableBlock) Value() cty.Value {
if v.variableValue == nil {
return cty.NilVal
}
return *v.variableValue
}
func (v *VariableBlock) Variable() {}
func (v *VariableBlock) ExecuteBeforePlan() error {
err := v.parseDescription()
if err != nil {
return err
}
if err = v.parseVariableType(); err != nil {
return err
}
variableRead, err := v.readValue()
if err != nil {
return err
}
if variableRead.Error != nil {
return variableRead.Error
}
value := variableRead.Value
if value == nil {
return fmt.Errorf("cannot evaluate value for var.%s", v.Name())
}
if v.variableType != nil && value.Type() != *v.variableType {
convertedValue, err := convert.Convert(*value, *v.variableType)
if err != nil {
return fmt.Errorf("incompatible type for var.%s, want %s, got %s", v.Name(), v.variableType.GoString(), value.Type().GoString())
}
value = &convertedValue
}
v.variableValue = value
return v.validationCheck()
}
func (v *VariableBlock) parseVariableType() error {
typeAttr, ok := v.HclBlock().Body.Attributes["type"]
if !ok {
v.variableType = nil
return nil
}
t, diag := typeexpr.Type(typeAttr.Expr)
if diag.HasErrors() {
return diag
}
v.variableType = &t
return nil
}
func (v *VariableBlock) readValue() (VariableValueRead, error) {
variables, err := v.c.readInputVariables()
if err != nil {
return NoValue, err
}
read, ok := variables[v.Name()]
if ok && read != NoValue {
return read, nil
}
defaultRead := v.readDefaultValue()
if defaultRead != NoValue {
return defaultRead, nil
}
return v.readFromPromote()
}
func (v *VariableBlock) readValueFromEnv() VariableValueRead {
env := os.Getenv(fmt.Sprintf("%s_VAR_%s", strings.ToUpper(v.c.DslAbbreviation()), v.name))
return v.parseVariableValueFromString(env, true)
}
func (v *VariableBlock) readDefaultValue() VariableValueRead {
defaultAttr, hasDefault := v.HclBlock().Body.Attributes["default"]
if !hasDefault {
return NoValue
}
value, diag := defaultAttr.Expr.Value(nil)
if diag.HasErrors() {
return NewVariableValueRead(v.Name(), nil, diag)
}
return NewVariableValueRead(v.Name(), &value, nil)
}
func (v *VariableBlock) parseVariableValueFromString(rawValue string, treatEmptyAsNoValue bool) VariableValueRead {
if rawValue == "" && treatEmptyAsNoValue {
return NoValue
}
for {
exp, diag := hclsyntax.ParseExpression([]byte(rawValue), "", hcl.InitialPos)
if diag.HasErrors() {
return NewVariableValueRead(v.Name(), nil, diag)
}
value, diag := exp.Value(nil)
if strings.Contains(diag.Error(), "Variables not allowed") {
rawValue = fmt.Sprintf(`"%s"`, rawValue)
continue
}
if diag.HasErrors() {
return NewVariableValueRead(v.Name(), nil, diag)
}
return NewVariableValueRead(v.Name(), &value, nil)
}
}
func (v *VariableBlock) readFromPromote() (VariableValueRead, error) {
promoterMutex.Lock()
defer promoterMutex.Unlock()
_, _ = valuePromoter.printf("var.%s\n", v.Name())
if v.Description != nil {
_, _ = valuePromoter.printf(" %s\n\n", *v.Description)
}
_, _ = valuePromoter.printf(" Enter a value: ")
var in string
_, err := valuePromoter.scanln(&in)
if err != nil {
return NoValue, err
}
_, _ = valuePromoter.printf("\n")
return v.parseVariableValueFromString(in, false), nil
}
func (v *VariableBlock) parseDescription() error {
attr, ok := v.HclBlock().Attributes()["description"]
if !ok {
return nil
}
value, diag := attr.Expr.Value(nil)
if diag.HasErrors() {
return diag
}
if value.Type() != cty.String {
return fmt.Errorf("incorrect type for `description` %s, got %s, want %s", attr.Range().String(), value.Type().GoString(), cty.String.GoString())
}
desc := value.AsString()
v.Description = &desc
return nil
}
func (v *VariableBlock) validationCheck() error {
var err error
for _, nb := range v.HclBlock().NestedBlocks() {
if nb.Type != "validation" {
continue
}
ctx := v.c.EmptyEvalContext()
ctx.Variables = map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
v.Name(): *v.variableValue,
}),
}
var vb VariableValidation
diag := gohcl.DecodeBody(nb.Body, ctx, &vb)
if diag.HasErrors() {
err = multierror.Append(err, diag)
continue
}
v.Validations = append(v.Validations, vb)
}
if err != nil {
return err
}
for _, validation := range v.Validations {
if validation.Condition {
continue
}
err = multierror.Append(err, fmt.Errorf("invalid value for variable %s\n%s\n%s", v.Name(), v.HclBlock().Range().String(), validation.ErrorMessage))
}
return err
}