Skip to content
This repository has been archived by the owner on Jan 8, 2024. It is now read-only.

Allow dynamic configuration as a default for input variables #2889

Merged
merged 17 commits into from
Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func Load(path string, opts *LoadOptions) (*Config, error) {
if diags.HasErrors() {
return nil, diags
}
vs, diags := variables.DecodeVariableBlocks(content)
vs, diags := variables.DecodeVariableBlocks(ctx, content)
if diags.HasErrors() {
return nil, diags
}
Expand Down
4 changes: 4 additions & 0 deletions internal/config/variables/testdata/invalid_type_dynamic.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
variable "rate" {
default = configdynamic("vault", {})
type = number
}
5 changes: 5 additions & 0 deletions internal/config/variables/testdata/valid.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@ variable "envs" {
type = number
env = ["foo", "bar"]
}

variable "dynamic" {
type = string
default = configdynamic("vault", {})
}
69 changes: 52 additions & 17 deletions internal/config/variables/variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ import (
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite"
hcljson "github.com/hashicorp/hcl/v2/json"
pb "github.com/hashicorp/waypoint/internal/server/gen"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/gocty"

"github.com/hashicorp/waypoint/internal/config/dynamic"
pb "github.com/hashicorp/waypoint/internal/server/gen"
)

const (
Expand Down Expand Up @@ -99,7 +102,7 @@ type Variable struct {
// to verify HCL syntax.
type HclVariable struct {
Name string `hcl:",label"`
Default cty.Value `hcl:"default,optional"`
Default hcl.Expression `hcl:"default,optional"`
Type hcl.Expression `hcl:"type,optional"`
Description string `hcl:"description,optional"`
Env []string `hcl:"env,optional"`
Expand All @@ -123,11 +126,14 @@ type Value struct {
// DecodeVariableBlocks uses the hclConfig schema to iterate over all
// variable blocks, validating names and types and checking for duplicates.
// It returns the final map of Variables to store for later reference.
func DecodeVariableBlocks(content *hcl.BodyContent) (map[string]*Variable, hcl.Diagnostics) {
func DecodeVariableBlocks(
ctx *hcl.EvalContext,
content *hcl.BodyContent,
) (map[string]*Variable, hcl.Diagnostics) {
var diags hcl.Diagnostics
vs := map[string]*Variable{}
for _, block := range content.Blocks.OfType("variable") {
v, diags := decodeVariableBlock(block)
v, diags := decodeVariableBlock(ctx, block)
if diags.HasErrors() {
return nil, diags
}
Expand All @@ -150,7 +156,10 @@ func DecodeVariableBlocks(content *hcl.BodyContent) (map[string]*Variable, hcl.D

// decodeVariableBlock validates each part of the variable block,
// building out a defined *Variable
func decodeVariableBlock(block *hcl.Block) (*Variable, hcl.Diagnostics) {
func decodeVariableBlock(
ctx *hcl.EvalContext,
block *hcl.Block,
) (*Variable, hcl.Diagnostics) {
name := block.Labels[0]
v := Variable{
Name: name,
Expand Down Expand Up @@ -194,27 +203,53 @@ func decodeVariableBlock(block *hcl.Block) (*Variable, hcl.Diagnostics) {
}

if attr, exists := content.Attributes["default"]; exists {
val, valDiags := attr.Expr.Value(nil)
defaultCtx := ctx.NewChild()
defaultCtx.Functions = dynamic.Register(map[string]function.Function{})

val, valDiags := attr.Expr.Value(defaultCtx)
diags = append(diags, valDiags...)
if diags.HasErrors() {
return nil, diags
}
// Convert the default to the expected type so we can catch invalid
// defaults early and allow later code to assume validity.
// Note that this depends on us having already processed any "type"
// attribute above.
if v.Type != cty.NilType {
var err error
val, err = convert.Convert(val, v.Type)
if err != nil {

// Depending on the value type, we behave differently.
switch val.Type() {
case dynamic.Type:
// For dynamic types we don't do conversion because we don't yet
// have the value. For now we require v.Type to be string so that
// the user isn't surprised by anything. Users can use explicit
// type conversion such as `tonumber`.
Copy link
Contributor

@izaaklauer izaaklauer Jan 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering how you'd handle types - requiring an hcl conversion is a good idea for now I think, and we can do fancier typing in the future if we want to. I was kind of dreading handling the object type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think strings are the way to go.

For numerics, HCL is arbitrary precision numbers too so any floats will work fine and easy to convert.
For boolean, easy to use tobool (uses strconv.parseBool)
For objects, easy to use yamldecode jsondecode jsonnetdecode etc. depending on format, and easy to add new format support if they have it.

if v.Type != cty.String {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Invalid default value for variable %q", name),
Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err),
Subject: attr.Expr.Range().Ptr(),
Summary: fmt.Sprintf("type for variable %q must be string for dynamic values", name),
Detail: "When using dynamically sourced configuration values, " +
"the variable type must be string. You may use explicit " +
"type conversion functions such as `tonumber` when using " +
"the variable.",
Subject: attr.Expr.Range().Ptr(),
})
val = cty.DynamicVal
}

default:
// Convert the default to the expected type so we can catch invalid
// defaults early and allow later code to assume validity.
// Note that this depends on us having already processed any "type"
// attribute above.
if v.Type != cty.NilType {
var err error
val, err = convert.Convert(val, v.Type)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Invalid default value for variable %q", name),
Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err),
Subject: attr.Expr.Range().Ptr(),
})
val = cty.DynamicVal
}
}
}

v.Default = &Value{
Expand Down
8 changes: 6 additions & 2 deletions internal/config/variables/variables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ func TestVariables_DecodeVariableBlock(t *testing.T) {
"invalid_def.hcl",
"Invalid default value",
},
{
"invalid_type_dynamic.hcl",
"must be string",
},
}

for _, tt := range cases {
Expand All @@ -55,7 +59,7 @@ func TestVariables_DecodeVariableBlock(t *testing.T) {
for _, block := range content.Blocks {
switch block.Type {
case "variable":
v, decodeDiag := decodeVariableBlock(block)
v, decodeDiag := decodeVariableBlock(nil, block)
vs[block.Labels[0]] = v
if decodeDiag.HasErrors() {
diags = append(diags, decodeDiag...)
Expand Down Expand Up @@ -317,7 +321,7 @@ func TestVariables_EvalInputValues(t *testing.T) {
for _, block := range content.Blocks {
switch block.Type {
case "variable":
v, decodeDiag := decodeVariableBlock(block)
v, decodeDiag := decodeVariableBlock(nil, block)
vs[block.Labels[0]] = v
if decodeDiag.HasErrors() {
diags = append(diags, decodeDiag...)
Expand Down