From 3836aeb949a2f89fba561f58a745adf6736ac2cf Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:10:18 +0100 Subject: [PATCH] bake: add semvercmp func to stdlib Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com> --- bake/hclparser/stdlib.go | 32 +++++++++++++++++++ bake/hclparser/stdlib_test.go | 58 +++++++++++++++++++++++++++++++++++ docs/bake-stdlib.md | 26 ++++++++++++++++ 3 files changed, 116 insertions(+) diff --git a/bake/hclparser/stdlib.go b/bake/hclparser/stdlib.go index 09cb6e9b8705..638f82f76653 100644 --- a/bake/hclparser/stdlib.go +++ b/bake/hclparser/stdlib.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/Masterminds/semver/v3" "github.com/docker/cli/cli/config" "github.com/hashicorp/go-cty-funcs/cidr" "github.com/hashicorp/go-cty-funcs/crypto" @@ -103,6 +104,7 @@ var stdlibFunctions = []funcDef{ {name: "reverselist", fn: stdlib.ReverseListFunc}, {name: "rsadecrypt", fn: crypto.RsaDecryptFunc, descriptionAlt: `Decrypts an RSA-encrypted ciphertext.`}, {name: "sanitize", factory: sanitizeFunc}, + {name: "semvercmp", factory: semvercmpFunc}, {name: "sethaselement", fn: stdlib.SetHasElementFunc}, {name: "setintersection", fn: stdlib.SetIntersectionFunc}, {name: "setproduct", fn: stdlib.SetProductFunc}, @@ -246,6 +248,36 @@ func sanitizeFunc() function.Function { }) } +// semvercmpFunc constructs a function that checks if a version satisfies a +// constraint. +func semvercmpFunc() function.Function { + return function.New(&function.Spec{ + Description: `Returns true if version satisfies a constraint.`, + Params: []function.Parameter{ + { + Name: "version", + Type: cty.String, + }, + { + Name: "contraint", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.Bool), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + version, err := semver.NewVersion(args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.Bool), err + } + constraint, err := semver.NewConstraint(args[1].AsString()) + if err != nil { + return cty.UnknownVal(cty.Bool), err + } + return cty.BoolVal(constraint.Check(version)), nil + }, + }) +} + // timestampFunc constructs a function that returns a string representation of the current date and time. // // This function was imported from terraform's datetime utilities. diff --git a/bake/hclparser/stdlib_test.go b/bake/hclparser/stdlib_test.go index bd745a601157..cf05210bd610 100644 --- a/bake/hclparser/stdlib_test.go +++ b/bake/hclparser/stdlib_test.go @@ -205,3 +205,61 @@ func TestHomedir(t *testing.T) { require.NotEmpty(t, home.AsString()) require.True(t, filepath.IsAbs(home.AsString())) } + +func TestSemverCmp(t *testing.T) { + type testCase struct { + version cty.Value + constraint cty.Value + want cty.Value + wantErr bool + } + tests := map[string]testCase{ + "valid constraint satisfied": { + version: cty.StringVal("1.2.3"), + constraint: cty.StringVal(">= 1.0.0"), + want: cty.BoolVal(true), + }, + "valid constraint not satisfied": { + version: cty.StringVal("2.1.0"), + constraint: cty.StringVal("< 2.0.0"), + want: cty.BoolVal(false), + }, + "valid constraint satisfied without patch": { + version: cty.StringVal("3.22"), + constraint: cty.StringVal(">= 3.20"), + want: cty.BoolVal(true), + }, + "invalid version": { + version: cty.StringVal("not-a-version"), + constraint: cty.StringVal(">= 1.0.0"), + wantErr: true, + }, + "invalid constraint": { + version: cty.StringVal("1.2.3"), + constraint: cty.StringVal("not-a-constraint"), + wantErr: true, + }, + "empty version": { + version: cty.StringVal(""), + constraint: cty.StringVal(">= 1.0.0"), + wantErr: true, + }, + "empty constraint": { + version: cty.StringVal("1.2.3"), + constraint: cty.StringVal(""), + wantErr: true, + }, + } + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + got, err := semvercmpFunc().Call([]cty.Value{test.version, test.constraint}) + if test.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, test.want, got) + } + }) + } +} diff --git a/docs/bake-stdlib.md b/docs/bake-stdlib.md index a02981e4b376..a5503966e583 100644 --- a/docs/bake-stdlib.md +++ b/docs/bake-stdlib.md @@ -79,6 +79,7 @@ title: Bake standard library functions | [`reverselist`](#reverselist) | Returns the given list with its elements in reverse order. | | [`rsadecrypt`](#rsadecrypt) | Decrypts an RSA-encrypted ciphertext. | | [`sanitize`](#sanitize) | Replaces all non-alphanumeric characters with a underscore, leaving only characters that are valid for a Bake target name. | +| [`semvercmp`](#semvercmp) | Returns true if version satisfies a constraint. | | [`sethaselement`](#sethaselement) | Returns true if the given set contains the given element, or false otherwise. | | [`setintersection`](#setintersection) | Returns the intersection of all given sets. | | [`setproduct`](#setproduct) | Calculates the cartesian product of two or more sets. | @@ -1065,6 +1066,31 @@ target "webapp-dev" { } ``` +## `semvercmp` + +This function checks if a semantic version fits within a set of constraints. +See [Checking Version Constraints](https://github.com/Masterminds/semver?tab=readme-ov-file#checking-version-constraints) +for details. + +```hcl +# docker-bake.hcl +variable "ALPINE_VERSION" { + default = "3.23" +} + +target "webapp-dev" { + dockerfile = "Dockerfile.webapp" + platforms = semvercmp(ALPINE_VERSION, ">= 3.20") ? [ + "linux/amd64", + "linux/arm64", + "linux/riscv64" + ] : [ + "linux/amd64", + "linux/arm64" + ] +} +``` + ## `sethaselement` ```hcl