bake: add formattimestamp and tighten unix timestamp parsing

Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2026-03-25 11:46:49 +01:00
parent 11395747a6
commit 2798e134e9
3 changed files with 207 additions and 25 deletions
+50 -3
View File
@@ -2,6 +2,7 @@ package hclparser
import ( import (
"errors" "errors"
"math/big"
"os" "os"
"os/user" "os/user"
"path" "path"
@@ -63,7 +64,8 @@ var stdlibFunctions = []funcDef{
{name: "flatten", fn: stdlib.FlattenFunc}, {name: "flatten", fn: stdlib.FlattenFunc},
{name: "floor", fn: stdlib.FloorFunc}, {name: "floor", fn: stdlib.FloorFunc},
{name: "format", fn: stdlib.FormatFunc}, {name: "format", fn: stdlib.FormatFunc},
{name: "formatdate", fn: stdlib.FormatDateFunc}, {name: "formatdate", fn: stdlib.FormatDateFunc, descriptionAlt: `Deprecated: use formattimestamp instead. Formats a timestamp given in RFC 3339 syntax into another timestamp in some other machine-oriented time syntax, as described in the format string.`},
{name: "formattimestamp", factory: formatTimestampFunc},
{name: "formatlist", fn: stdlib.FormatListFunc}, {name: "formatlist", fn: stdlib.FormatListFunc},
{name: "greaterthan", fn: stdlib.GreaterThanFunc}, {name: "greaterthan", fn: stdlib.GreaterThanFunc},
{name: "greaterthanorequalto", fn: stdlib.GreaterThanOrEqualToFunc}, {name: "greaterthanorequalto", fn: stdlib.GreaterThanOrEqualToFunc},
@@ -279,6 +281,40 @@ func semvercmpFunc() function.Function {
}) })
} }
// formatTimestampFunc constructs a function that formats either an RFC3339
// timestamp string or a unix timestamp integer using the same format verbs as
// formatdate.
func formatTimestampFunc() function.Function {
return function.New(&function.Spec{
Description: `Formats a timestamp string in RFC 3339 syntax or a unix timestamp integer into another timestamp in some other machine-oriented time syntax, as described in the format string.`,
Params: []function.Parameter{
{
Name: "format",
Type: cty.String,
},
{
Name: "time",
Type: cty.DynamicPseudoType,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
switch args[1].Type() {
case cty.String:
return stdlib.FormatDateFunc.Call([]cty.Value{args[0], args[1]})
case cty.Number:
t, err := unixTimestampValue(args[1])
if err != nil {
return cty.DynamicVal, function.NewArgError(1, err)
}
return stdlib.FormatDateFunc.Call([]cty.Value{args[0], cty.StringVal(t.Format(time.RFC3339))})
default:
return cty.DynamicVal, function.NewArgErrorf(1, "must be a string timestamp or a unix timestamp number")
}
},
})
}
// timestampFunc constructs a function that returns a string representation of the current date and time. // timestampFunc constructs a function that returns a string representation of the current date and time.
// //
// This function was imported from Terraform's datetime utilities. // This function was imported from Terraform's datetime utilities.
@@ -345,8 +381,10 @@ func unixtimestampParseFunc() function.Function {
"iso_week": cty.Number, "iso_week": cty.Number,
})), })),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
ts, _ := args[0].AsBigFloat().Int64() unixTime, err := unixTimestampValue(args[0])
unixTime := time.Unix(ts, 0).UTC() if err != nil {
return cty.DynamicVal, function.NewArgError(0, err)
}
isoYear, isoWeek := unixTime.ISOWeek() isoYear, isoWeek := unixTime.ISOWeek()
return cty.ObjectVal(map[string]cty.Value{ return cty.ObjectVal(map[string]cty.Value{
"year": cty.NumberIntVal(int64(unixTime.Year())), "year": cty.NumberIntVal(int64(unixTime.Year())),
@@ -367,6 +405,15 @@ func unixtimestampParseFunc() function.Function {
}) })
} }
func unixTimestampValue(v cty.Value) (time.Time, error) {
bf := v.AsBigFloat()
ts, acc := bf.Int64()
if acc != big.Exact {
return time.Time{}, errors.New("unix timestamp must be an integer")
}
return time.Unix(ts, 0).UTC(), nil
}
func Stdlib() map[string]function.Function { func Stdlib() map[string]function.Function {
funcs := make(map[string]function.Function, len(stdlibFunctions)) funcs := make(map[string]function.Function, len(stdlibFunctions))
for _, v := range stdlibFunctions { for _, v := range stdlibFunctions {
+129 -21
View File
@@ -260,27 +260,135 @@ func TestSemverCmp(t *testing.T) {
} }
func TestUnixTimestampParseFunc(t *testing.T) { func TestUnixTimestampParseFunc(t *testing.T) {
fn := unixtimestampParseFunc() type testCase struct {
input := cty.NumberIntVal(1690328596) input cty.Value
got, err := fn.Call([]cty.Value{input}) want map[string]cty.Value
require.NoError(t, err) wantErr bool
expected := map[string]cty.Value{
"year": cty.NumberIntVal(2023),
"year_day": cty.NumberIntVal(206),
"day": cty.NumberIntVal(25),
"month": cty.NumberIntVal(7),
"month_name": cty.StringVal("July"),
"weekday": cty.NumberIntVal(2),
"weekday_name": cty.StringVal("Tuesday"),
"hour": cty.NumberIntVal(23),
"minute": cty.NumberIntVal(43),
"second": cty.NumberIntVal(16),
"rfc3339": cty.StringVal("2023-07-25T23:43:16Z"),
"iso_year": cty.NumberIntVal(2023),
"iso_week": cty.NumberIntVal(30),
} }
for k, v := range expected { tests := map[string]testCase{
require.True(t, got.GetAttr(k).RawEquals(v), "field %s: got %v, want %v", k, got.GetAttr(k), v) "positive timestamp": {
input: cty.NumberIntVal(1690328596),
want: map[string]cty.Value{
"year": cty.NumberIntVal(2023),
"year_day": cty.NumberIntVal(206),
"day": cty.NumberIntVal(25),
"month": cty.NumberIntVal(7),
"month_name": cty.StringVal("July"),
"weekday": cty.NumberIntVal(2),
"weekday_name": cty.StringVal("Tuesday"),
"hour": cty.NumberIntVal(23),
"minute": cty.NumberIntVal(43),
"second": cty.NumberIntVal(16),
"rfc3339": cty.StringVal("2023-07-25T23:43:16Z"),
"iso_year": cty.NumberIntVal(2023),
"iso_week": cty.NumberIntVal(30),
},
},
"zero timestamp": {
input: cty.NumberIntVal(0),
want: map[string]cty.Value{
"year": cty.NumberIntVal(1970),
"year_day": cty.NumberIntVal(1),
"day": cty.NumberIntVal(1),
"month": cty.NumberIntVal(1),
"month_name": cty.StringVal("January"),
"weekday": cty.NumberIntVal(4),
"weekday_name": cty.StringVal("Thursday"),
"hour": cty.NumberIntVal(0),
"minute": cty.NumberIntVal(0),
"second": cty.NumberIntVal(0),
"rfc3339": cty.StringVal("1970-01-01T00:00:00Z"),
"iso_year": cty.NumberIntVal(1970),
"iso_week": cty.NumberIntVal(1),
},
},
"negative timestamp": {
input: cty.NumberIntVal(-1),
want: map[string]cty.Value{
"year": cty.NumberIntVal(1969),
"year_day": cty.NumberIntVal(365),
"day": cty.NumberIntVal(31),
"month": cty.NumberIntVal(12),
"month_name": cty.StringVal("December"),
"weekday": cty.NumberIntVal(3),
"weekday_name": cty.StringVal("Wednesday"),
"hour": cty.NumberIntVal(23),
"minute": cty.NumberIntVal(59),
"second": cty.NumberIntVal(59),
"rfc3339": cty.StringVal("1969-12-31T23:59:59Z"),
"iso_year": cty.NumberIntVal(1970),
"iso_week": cty.NumberIntVal(1),
},
},
"fractional timestamp": {
input: cty.NumberFloatVal(1.2),
wantErr: true,
},
"string timestamp": {
input: cty.StringVal("0"),
wantErr: true,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got, err := unixtimestampParseFunc().Call([]cty.Value{test.input})
if test.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
for k, v := range test.want {
require.True(t, got.GetAttr(k).RawEquals(v), "field %s: got %v, want %v", k, got.GetAttr(k), v)
}
})
}
}
func TestFormatTimestampFunc(t *testing.T) {
type testCase struct {
format cty.Value
input cty.Value
want cty.Value
wantErr bool
}
tests := map[string]testCase{
"rfc3339 string input": {
format: cty.StringVal("YYYY-MM-DD"),
input: cty.StringVal("2025-09-16T12:00:00Z"),
want: cty.StringVal("2025-09-16"),
},
"unix timestamp input": {
format: cty.StringVal("YYYY-MM-DD'T'hh:mm:ssZ"),
input: cty.NumberIntVal(1690328596),
want: cty.StringVal("2023-07-25T23:43:16Z"),
},
"negative unix timestamp input": {
format: cty.StringVal("YYYY-MM-DD'T'hh:mm:ssZ"),
input: cty.NumberIntVal(-1),
want: cty.StringVal("1969-12-31T23:59:59Z"),
},
"fractional unix timestamp input": {
format: cty.StringVal("YYYY-MM-DD"),
input: cty.NumberFloatVal(1.2),
wantErr: true,
},
"invalid string input": {
format: cty.StringVal("YYYY-MM-DD"),
input: cty.StringVal("0"),
wantErr: true,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got, err := formatTimestampFunc().Call([]cty.Value{test.format, test.input})
if test.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, test.want, got)
}
})
} }
} }
+28 -1
View File
@@ -38,8 +38,9 @@ title: Bake standard library functions
| [`flatten`](#flatten) | Transforms a list, set, or tuple value into a tuple by replacing any given elements that are themselves sequences with a flattened tuple of all of the nested elements concatenated together. | | [`flatten`](#flatten) | Transforms a list, set, or tuple value into a tuple by replacing any given elements that are themselves sequences with a flattened tuple of all of the nested elements concatenated together. |
| [`floor`](#floor) | Returns the greatest whole number that is less than or equal to the given value. | | [`floor`](#floor) | Returns the greatest whole number that is less than or equal to the given value. |
| [`format`](#format) | Constructs a string by applying formatting verbs to a series of arguments, using a similar syntax to the C function \"printf\". | | [`format`](#format) | Constructs a string by applying formatting verbs to a series of arguments, using a similar syntax to the C function \"printf\". |
| [`formatdate`](#formatdate) | Formats a timestamp given in RFC 3339 syntax into another timestamp in some other machine-oriented time syntax, as described in the format string. | | [`formatdate`](#formatdate) | Deprecated: use formattimestamp instead. Formats a timestamp given in RFC 3339 syntax into another timestamp in some other machine-oriented time syntax, as described in the format string. |
| [`formatlist`](#formatlist) | Constructs a list of strings by applying formatting verbs to a series of arguments, using a similar syntax to the C function \"printf\". | | [`formatlist`](#formatlist) | Constructs a list of strings by applying formatting verbs to a series of arguments, using a similar syntax to the C function \"printf\". |
| [`formattimestamp`](#formattimestamp) | Formats a timestamp string in RFC 3339 syntax or a unix timestamp integer into another timestamp in some other machine-oriented time syntax, as described in the format string. |
| [`greaterthan`](#greaterthan) | Returns true if and only if the second number is greater than the first. | | [`greaterthan`](#greaterthan) | Returns true if and only if the second number is greater than the first. |
| [`greaterthanorequalto`](#greaterthanorequalto) | Returns true if and only if the second number is greater than or equal to the first. | | [`greaterthanorequalto`](#greaterthanorequalto) | Returns true if and only if the second number is greater than or equal to the first. |
| [`hasindex`](#hasindex) | Returns true if if the given collection can be indexed with the given key without producing an error, or false otherwise. | | [`hasindex`](#hasindex) | Returns true if if the given collection can be indexed with the given key without producing an error, or false otherwise. |
@@ -533,6 +534,10 @@ target "webapp-dev" {
## `formatdate` ## `formatdate`
> [!WARNING]
> Deprecated: use `formattimestamp` instead. `formatdate` only accepts RFC3339
> timestamp strings.
```hcl ```hcl
# docker-bake.hcl # docker-bake.hcl
target "webapp-dev" { target "webapp-dev" {
@@ -544,6 +549,28 @@ target "webapp-dev" {
} }
``` ```
## `formattimestamp`
Formats either an RFC3339 timestamp string or a unix timestamp integer.
```hcl
# docker-bake.hcl
variable "SOURCE_DATE_EPOCH" {
type = number
default = 1690328596
}
target "default" {
dockerfile = "Dockerfile"
labels = {
"org.opencontainers.image.created" = formattimestamp("YYYY-MM-DD'T'hh:mm:ssZ", SOURCE_DATE_EPOCH) # => "2023-07-25T23:43:16Z"
}
args = {
build_date = formattimestamp("YYYY-MM-DD", "2025-09-16T12:00:00Z") # => "2025-09-16"
}
}
```
## `formatlist` ## `formatlist`
```hcl ```hcl