Skip to content

Commit e51cdca

Browse files
committed
bake: basic variable validation
Signed-off-by: CrazyMax <[email protected]>
1 parent 3b943bd commit e51cdca

File tree

2 files changed

+193
-5
lines changed

2 files changed

+193
-5
lines changed

bake/bake_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,3 +1856,155 @@ func TestNetNone(t *testing.T) {
18561856
require.Len(t, bo["app"].Allow, 0)
18571857
require.Equal(t, "none", bo["app"].NetworkMode)
18581858
}
1859+
1860+
func TestVariableValidation(t *testing.T) {
1861+
fp := File{
1862+
Name: "docker-bake.hcl",
1863+
Data: []byte(`
1864+
variable "FOO" {
1865+
validation {
1866+
condition = FOO != ""
1867+
error_message = "FOO is required."
1868+
}
1869+
}
1870+
target "app" {
1871+
args = {
1872+
FOO = FOO
1873+
}
1874+
}
1875+
`),
1876+
}
1877+
1878+
ctx := context.TODO()
1879+
1880+
t.Run("Valid", func(t *testing.T) {
1881+
t.Setenv("FOO", "bar")
1882+
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
1883+
require.NoError(t, err)
1884+
})
1885+
1886+
t.Run("Invalid", func(t *testing.T) {
1887+
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
1888+
require.Error(t, err)
1889+
require.Contains(t, err.Error(), "FOO is required.")
1890+
})
1891+
}
1892+
1893+
func TestVariableValidationMulti(t *testing.T) {
1894+
fp := File{
1895+
Name: "docker-bake.hcl",
1896+
Data: []byte(`
1897+
variable "FOO" {
1898+
validation {
1899+
condition = FOO != ""
1900+
error_message = "FOO is required."
1901+
}
1902+
validation {
1903+
condition = strlen(FOO) > 4
1904+
error_message = "FOO must be longer than 4 characters."
1905+
}
1906+
}
1907+
target "app" {
1908+
args = {
1909+
FOO = FOO
1910+
}
1911+
}
1912+
`),
1913+
}
1914+
1915+
ctx := context.TODO()
1916+
1917+
t.Run("Valid", func(t *testing.T) {
1918+
t.Setenv("FOO", "barbar")
1919+
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
1920+
require.NoError(t, err)
1921+
})
1922+
1923+
t.Run("InvalidLength", func(t *testing.T) {
1924+
t.Setenv("FOO", "bar")
1925+
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
1926+
require.Error(t, err)
1927+
require.Contains(t, err.Error(), "FOO must be longer than 4 characters.")
1928+
})
1929+
1930+
t.Run("InvalidEmpty", func(t *testing.T) {
1931+
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
1932+
require.Error(t, err)
1933+
require.Contains(t, err.Error(), "FOO is required.")
1934+
})
1935+
}
1936+
1937+
func TestVariableValidationWithDeps(t *testing.T) {
1938+
fp := File{
1939+
Name: "docker-bake.hcl",
1940+
Data: []byte(`
1941+
variable "FOO" {}
1942+
variable "BAR" {
1943+
validation {
1944+
condition = FOO != ""
1945+
error_message = "BAR requires FOO to be set."
1946+
}
1947+
}
1948+
target "app" {
1949+
args = {
1950+
BAR = BAR
1951+
}
1952+
}
1953+
`),
1954+
}
1955+
1956+
ctx := context.TODO()
1957+
1958+
t.Run("Valid", func(t *testing.T) {
1959+
t.Setenv("FOO", "bar")
1960+
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
1961+
require.NoError(t, err)
1962+
})
1963+
1964+
t.Run("SetBar", func(t *testing.T) {
1965+
t.Setenv("FOO", "bar")
1966+
t.Setenv("BAR", "baz")
1967+
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
1968+
require.NoError(t, err)
1969+
})
1970+
1971+
t.Run("Invalid", func(t *testing.T) {
1972+
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
1973+
require.Error(t, err)
1974+
require.Contains(t, err.Error(), "BAR requires FOO to be set.")
1975+
})
1976+
}
1977+
1978+
func TestVariableValidationTyped(t *testing.T) {
1979+
fp := File{
1980+
Name: "docker-bake.hcl",
1981+
Data: []byte(`
1982+
variable "FOO" {
1983+
default = 0
1984+
validation {
1985+
condition = FOO > 5
1986+
error_message = "FOO must be greater than 5."
1987+
}
1988+
}
1989+
target "app" {
1990+
args = {
1991+
FOO = FOO
1992+
}
1993+
}
1994+
`),
1995+
}
1996+
1997+
ctx := context.TODO()
1998+
1999+
t.Run("Valid", func(t *testing.T) {
2000+
t.Setenv("FOO", "10")
2001+
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
2002+
require.NoError(t, err)
2003+
})
2004+
2005+
t.Run("Invalid", func(t *testing.T) {
2006+
_, _, err := ReadTargets(ctx, []File{fp}, []string{"app"}, nil, nil)
2007+
require.Error(t, err)
2008+
require.Contains(t, err.Error(), "FOO must be greater than 5.")
2009+
})
2010+
}

bake/hclparser/hclparser.go

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,17 @@ type Opt struct {
2525
}
2626

2727
type variable struct {
28-
Name string `json:"-" hcl:"name,label"`
29-
Default *hcl.Attribute `json:"default,omitempty" hcl:"default,optional"`
30-
Description string `json:"description,omitempty" hcl:"description,optional"`
31-
Body hcl.Body `json:"-" hcl:",body"`
32-
Remain hcl.Body `json:"-" hcl:",remain"`
28+
Name string `json:"-" hcl:"name,label"`
29+
Default *hcl.Attribute `json:"default,omitempty" hcl:"default,optional"`
30+
Description string `json:"description,omitempty" hcl:"description,optional"`
31+
Validations []*variableValidation `json:"validation,omitempty" hcl:"validation,block"`
32+
Body hcl.Body `json:"-" hcl:",body"`
33+
Remain hcl.Body `json:"-" hcl:",remain"`
34+
}
35+
36+
type variableValidation struct {
37+
Condition hcl.Expression `json:"condition" hcl:"condition"`
38+
ErrorMessage hcl.Expression `json:"error_message" hcl:"error_message"`
3339
}
3440

3541
type functionDef struct {
@@ -541,6 +547,33 @@ func (p *parser) resolveBlockNames(block *hcl.Block) ([]string, error) {
541547
return names, nil
542548
}
543549

550+
func (p *parser) validateVariables(vars map[string]*variable, ectx *hcl.EvalContext) hcl.Diagnostics {
551+
var diags hcl.Diagnostics
552+
for _, v := range vars {
553+
for _, validation := range v.Validations {
554+
condition, condDiags := validation.Condition.Value(ectx)
555+
if condDiags.HasErrors() {
556+
diags = append(diags, condDiags...)
557+
continue
558+
}
559+
if !condition.True() {
560+
message, msgDiags := validation.ErrorMessage.Value(ectx)
561+
if msgDiags.HasErrors() {
562+
diags = append(diags, msgDiags...)
563+
continue
564+
}
565+
diags = append(diags, &hcl.Diagnostic{
566+
Severity: hcl.DiagError,
567+
Summary: "Validation failed",
568+
Detail: message.AsString(),
569+
Subject: validation.Condition.Range().Ptr(),
570+
})
571+
}
572+
}
573+
}
574+
return diags
575+
}
576+
544577
type Variable struct {
545578
Name string
546579
Description string
@@ -686,6 +719,9 @@ func Parse(b hcl.Body, opt Opt, val interface{}) (*ParseMeta, hcl.Diagnostics) {
686719
}
687720
vars = append(vars, v)
688721
}
722+
if diags := p.validateVariables(p.vars, p.ectx); diags.HasErrors() {
723+
return nil, diags
724+
}
689725

690726
for k := range p.funcs {
691727
if err := p.resolveFunction(p.ectx, k); err != nil {

0 commit comments

Comments
 (0)