Skip to content

Commit

Permalink
bake: basic variable validation
Browse files Browse the repository at this point in the history
Signed-off-by: CrazyMax <[email protected]>
  • Loading branch information
crazy-max committed Nov 19, 2024
1 parent 3b943bd commit d088a79
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 5 deletions.
118 changes: 118 additions & 0 deletions bake/bake_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1856,3 +1856,121 @@ func TestNetNone(t *testing.T) {
require.Len(t, bo["app"].Allow, 0)
require.Equal(t, "none", bo["app"].NetworkMode)
}

func TestVariableValidation(t *testing.T) {
fp := File{
Name: "docker-bake.hcl",
Data: []byte(`
variable "FOO" {
validation {
condition = FOO != ""
error_message = "FOO is required."
}
}
target "app" {
args = {
FOO = FOO
}
}
`),
}

ctx := context.TODO()

t.Run("Valid", func(t *testing.T) {
t.Setenv("FOO", "bar")
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
require.NoError(t, err)
})

t.Run("Invalid", func(t *testing.T) {
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "FOO is required.")
})
}

func TestVariableValidationMulti(t *testing.T) {
fp := File{
Name: "docker-bake.hcl",
Data: []byte(`
variable "FOO" {
validation {
condition = FOO != ""
error_message = "FOO is required."
}
validation {
condition = strlen(FOO) > 4
error_message = "FOO must be longer than 4 characters."
}
}
target "app" {
args = {
FOO = FOO
}
}
`),
}

ctx := context.TODO()

t.Run("Valid", func(t *testing.T) {
t.Setenv("FOO", "barbar")
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
require.NoError(t, err)
})

t.Run("InvalidLength", func(t *testing.T) {
t.Setenv("FOO", "bar")
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "FOO must be longer than 4 characters.")
})

t.Run("InvalidEmpty", func(t *testing.T) {
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "FOO is required.")
})
}

func TestVariableValidationWithDeps(t *testing.T) {
fp := File{
Name: "docker-bake.hcl",
Data: []byte(`
variable "FOO" {}
variable "BAR" {
validation {
condition = FOO != ""
error_message = "BAR requires FOO to be set."
}
}
target "app" {
args = {
BAR = BAR
}
}
`),
}

ctx := context.TODO()

t.Run("Valid", func(t *testing.T) {
t.Setenv("FOO", "bar")
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
require.NoError(t, err)
})

t.Run("SetBar", func(t *testing.T) {
t.Setenv("FOO", "bar")
t.Setenv("BAR", "baz")
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
require.NoError(t, err)
})

t.Run("Invalid", func(t *testing.T) {
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "BAR requires FOO to be set.")
})
}
58 changes: 53 additions & 5 deletions bake/hclparser/hclparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,18 @@ type Opt struct {
}

type variable struct {
Name string `json:"-" hcl:"name,label"`
Default *hcl.Attribute `json:"default,omitempty" hcl:"default,optional"`
Description string `json:"description,omitempty" hcl:"description,optional"`
Body hcl.Body `json:"-" hcl:",body"`
Remain hcl.Body `json:"-" hcl:",remain"`
Name string `json:"-" hcl:"name,label"`
Default *hcl.Attribute `json:"default,omitempty" hcl:"default,optional"`
Description string `json:"description,omitempty" hcl:"description,optional"`
Validations []*variableValidation `json:"validation,omitempty" hcl:"validation,block"`
Body hcl.Body `json:"-" hcl:",body"`
Remain hcl.Body `json:"-" hcl:",remain"`
Range hcl.Range `json:"-"`
}

type variableValidation struct {
Condition hcl.Expression `json:"condition" hcl:"condition"`
ErrorMessage hcl.Expression `json:"error_message" hcl:"error_message"`
}

type functionDef struct {
Expand Down Expand Up @@ -541,6 +548,33 @@ func (p *parser) resolveBlockNames(block *hcl.Block) ([]string, error) {
return names, nil
}

func (p *parser) validateVariables(vars map[string]*variable, ectx *hcl.EvalContext) hcl.Diagnostics {
var diags hcl.Diagnostics
for _, v := range vars {
for _, validation := range v.Validations {
condition, condDiags := validation.Condition.Value(ectx)
if condDiags.HasErrors() {
diags = append(diags, condDiags...)
continue
}
if !condition.True() {
message, msgDiags := validation.ErrorMessage.Value(ectx)
if msgDiags.HasErrors() {
diags = append(diags, msgDiags...)
continue
}
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Validation failed",
Detail: message.AsString(),
Subject: validation.Condition.Range().Ptr(),
})
}
}
}
return diags
}

type Variable struct {
Name string
Description string
Expand Down Expand Up @@ -605,6 +639,7 @@ func Parse(b hcl.Body, opt Opt, val interface{}) (*ParseMeta, hcl.Diagnostics) {
}

for _, v := range defs.Variables {
v.Range = v.Body.MissingItemRange()
// TODO: validate name
if _, ok := reserved[v.Name]; ok {
continue
Expand Down Expand Up @@ -829,6 +864,19 @@ func Parse(b hcl.Body, opt Opt, val interface{}) (*ParseMeta, hcl.Diagnostics) {
}
}

// resolve variables before performing validation
for _, v := range p.vars {
if err := p.resolveValue(p.ectx, v.Name); err != nil {
if diags, ok := err.(hcl.Diagnostics); ok {
return nil, diags
}
return nil, wrapErrorDiagnostic("Invalid variable", err, &v.Range, &v.Range)
}
}
if diags := p.validateVariables(p.vars, p.ectx); diags.HasErrors() {
return nil, diags
}

return &ParseMeta{
Renamed: renamed,
AllVariables: vars,
Expand Down

0 comments on commit d088a79

Please sign in to comment.