bake: add formattimestamp and tighten unix timestamp parsing
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package hclparser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/big"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
@@ -63,7 +64,8 @@ var stdlibFunctions = []funcDef{
|
||||
{name: "flatten", fn: stdlib.FlattenFunc},
|
||||
{name: "floor", fn: stdlib.FloorFunc},
|
||||
{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: "greaterthan", fn: stdlib.GreaterThanFunc},
|
||||
{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.
|
||||
//
|
||||
// This function was imported from Terraform's datetime utilities.
|
||||
@@ -345,8 +381,10 @@ func unixtimestampParseFunc() function.Function {
|
||||
"iso_week": cty.Number,
|
||||
})),
|
||||
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
ts, _ := args[0].AsBigFloat().Int64()
|
||||
unixTime := time.Unix(ts, 0).UTC()
|
||||
unixTime, err := unixTimestampValue(args[0])
|
||||
if err != nil {
|
||||
return cty.DynamicVal, function.NewArgError(0, err)
|
||||
}
|
||||
isoYear, isoWeek := unixTime.ISOWeek()
|
||||
return cty.ObjectVal(map[string]cty.Value{
|
||||
"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 {
|
||||
funcs := make(map[string]function.Function, len(stdlibFunctions))
|
||||
for _, v := range stdlibFunctions {
|
||||
|
||||
@@ -260,12 +260,15 @@ func TestSemverCmp(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUnixTimestampParseFunc(t *testing.T) {
|
||||
fn := unixtimestampParseFunc()
|
||||
input := cty.NumberIntVal(1690328596)
|
||||
got, err := fn.Call([]cty.Value{input})
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := map[string]cty.Value{
|
||||
type testCase struct {
|
||||
input cty.Value
|
||||
want map[string]cty.Value
|
||||
wantErr bool
|
||||
}
|
||||
tests := map[string]testCase{
|
||||
"positive timestamp": {
|
||||
input: cty.NumberIntVal(1690328596),
|
||||
want: map[string]cty.Value{
|
||||
"year": cty.NumberIntVal(2023),
|
||||
"year_day": cty.NumberIntVal(206),
|
||||
"day": cty.NumberIntVal(25),
|
||||
@@ -279,8 +282,113 @@ func TestUnixTimestampParseFunc(t *testing.T) {
|
||||
"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 k, v := range expected {
|
||||
|
||||
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
@@ -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. |
|
||||
| [`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\". |
|
||||
| [`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\". |
|
||||
| [`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. |
|
||||
| [`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. |
|
||||
@@ -533,6 +534,10 @@ target "webapp-dev" {
|
||||
|
||||
## `formatdate`
|
||||
|
||||
> [!WARNING]
|
||||
> Deprecated: use `formattimestamp` instead. `formatdate` only accepts RFC3339
|
||||
> timestamp strings.
|
||||
|
||||
```hcl
|
||||
# docker-bake.hcl
|
||||
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`
|
||||
|
||||
```hcl
|
||||
|
||||
Reference in New Issue
Block a user