bake: add basename, dirname and sanitize functions

These functions help with dealing with path inputs and
using parts of them to configure targets.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi 2024-08-13 11:41:10 +03:00
parent 07a85a544b
commit a4adae3d6b
No known key found for this signature in database
GPG Key ID: AFA9DE5F8AB7AF39
2 changed files with 224 additions and 8 deletions

View File

@ -2,6 +2,8 @@ package hclparser
import ( import (
"errors" "errors"
"path"
"strings"
"time" "time"
"github.com/hashicorp/go-cty-funcs/cidr" "github.com/hashicorp/go-cty-funcs/cidr"
@ -27,6 +29,7 @@ var stdlibFunctions = []funcDef{
{name: "and", fn: stdlib.AndFunc}, {name: "and", fn: stdlib.AndFunc},
{name: "base64decode", fn: encoding.Base64DecodeFunc}, {name: "base64decode", fn: encoding.Base64DecodeFunc},
{name: "base64encode", fn: encoding.Base64EncodeFunc}, {name: "base64encode", fn: encoding.Base64EncodeFunc},
{name: "basename", factory: basenameFunc},
{name: "bcrypt", fn: crypto.BcryptFunc}, {name: "bcrypt", fn: crypto.BcryptFunc},
{name: "byteslen", fn: stdlib.BytesLenFunc}, {name: "byteslen", fn: stdlib.BytesLenFunc},
{name: "bytesslice", fn: stdlib.BytesSliceFunc}, {name: "bytesslice", fn: stdlib.BytesSliceFunc},
@ -45,6 +48,7 @@ var stdlibFunctions = []funcDef{
{name: "contains", fn: stdlib.ContainsFunc}, {name: "contains", fn: stdlib.ContainsFunc},
{name: "convert", fn: typeexpr.ConvertFunc}, {name: "convert", fn: typeexpr.ConvertFunc},
{name: "csvdecode", fn: stdlib.CSVDecodeFunc}, {name: "csvdecode", fn: stdlib.CSVDecodeFunc},
{name: "dirname", factory: dirnameFunc},
{name: "distinct", fn: stdlib.DistinctFunc}, {name: "distinct", fn: stdlib.DistinctFunc},
{name: "divide", fn: stdlib.DivideFunc}, {name: "divide", fn: stdlib.DivideFunc},
{name: "element", fn: stdlib.ElementFunc}, {name: "element", fn: stdlib.ElementFunc},
@ -91,6 +95,7 @@ var stdlibFunctions = []funcDef{
{name: "reverse", fn: stdlib.ReverseFunc}, {name: "reverse", fn: stdlib.ReverseFunc},
{name: "reverselist", fn: stdlib.ReverseListFunc}, {name: "reverselist", fn: stdlib.ReverseListFunc},
{name: "rsadecrypt", fn: crypto.RsaDecryptFunc}, {name: "rsadecrypt", fn: crypto.RsaDecryptFunc},
{name: "sanitize", factory: sanitizeFunc},
{name: "sethaselement", fn: stdlib.SetHasElementFunc}, {name: "sethaselement", fn: stdlib.SetHasElementFunc},
{name: "setintersection", fn: stdlib.SetIntersectionFunc}, {name: "setintersection", fn: stdlib.SetIntersectionFunc},
{name: "setproduct", fn: stdlib.SetProductFunc}, {name: "setproduct", fn: stdlib.SetProductFunc},
@ -170,6 +175,67 @@ func indexOfFunc() function.Function {
}) })
} }
// basenameFunc constructs a function that returns the last element of a path.
func basenameFunc() function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "path",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := args[0].AsString()
return cty.StringVal(path.Base(in)), nil
},
})
}
// dirnameFunc constructs a function that returns the directory of a path.
func dirnameFunc() function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "path",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := args[0].AsString()
return cty.StringVal(path.Dir(in)), nil
},
})
}
// sanitizyFunc constructs a function that replaces all non-alphanumeric characters with a underscore,
// leaving only characters that are valid for a Bake target name.
func sanitizeFunc() function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "name",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := args[0].AsString()
// only [a-zA-Z0-9_-]+ is allowed
var b strings.Builder
for _, r := range in {
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-' {
b.WriteRune(r)
} else {
b.WriteRune('_')
}
}
return cty.StringVal(b.String()), nil
},
})
}
// 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.

View File

@ -3,6 +3,7 @@ package hclparser
import ( import (
"testing" "testing"
"github.com/stretchr/testify/require"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
@ -35,15 +36,164 @@ func TestIndexOf(t *testing.T) {
name, test := name, test name, test := name, test
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
got, err := indexOfFunc().Call([]cty.Value{test.input, test.key}) got, err := indexOfFunc().Call([]cty.Value{test.input, test.key})
if err != nil { if test.wantErr {
if test.wantErr { require.Error(t, err)
return } else {
} require.NoError(t, err)
t.Fatalf("unexpected error: %s", err) require.Equal(t, test.want, got)
}
if !got.RawEquals(test.want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
} }
}) })
} }
} }
func TestBasename(t *testing.T) {
type testCase struct {
input cty.Value
want cty.Value
wantErr bool
}
tests := map[string]testCase{
"empty": {
input: cty.StringVal(""),
want: cty.StringVal("."),
},
"slash": {
input: cty.StringVal("/"),
want: cty.StringVal("/"),
},
"simple": {
input: cty.StringVal("/foo/bar"),
want: cty.StringVal("bar"),
},
"simple no slash": {
input: cty.StringVal("foo/bar"),
want: cty.StringVal("bar"),
},
"dot": {
input: cty.StringVal("/foo/bar."),
want: cty.StringVal("bar."),
},
"dotdot": {
input: cty.StringVal("/foo/bar.."),
want: cty.StringVal("bar.."),
},
"dotdotdot": {
input: cty.StringVal("/foo/bar..."),
want: cty.StringVal("bar..."),
},
}
for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
got, err := basenameFunc().Call([]cty.Value{test.input})
if test.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, test.want, got)
}
})
}
}
func TestDirname(t *testing.T) {
type testCase struct {
input cty.Value
want cty.Value
wantErr bool
}
tests := map[string]testCase{
"empty": {
input: cty.StringVal(""),
want: cty.StringVal("."),
},
"slash": {
input: cty.StringVal("/"),
want: cty.StringVal("/"),
},
"simple": {
input: cty.StringVal("/foo/bar"),
want: cty.StringVal("/foo"),
},
"simple no slash": {
input: cty.StringVal("foo/bar"),
want: cty.StringVal("foo"),
},
"dot": {
input: cty.StringVal("/foo/bar."),
want: cty.StringVal("/foo"),
},
"dotdot": {
input: cty.StringVal("/foo/bar.."),
want: cty.StringVal("/foo"),
},
"dotdotdot": {
input: cty.StringVal("/foo/bar..."),
want: cty.StringVal("/foo"),
},
}
for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
got, err := dirnameFunc().Call([]cty.Value{test.input})
if test.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, test.want, got)
}
})
}
}
func TestSanitize(t *testing.T) {
type testCase struct {
input cty.Value
want cty.Value
}
tests := map[string]testCase{
"empty": {
input: cty.StringVal(""),
want: cty.StringVal(""),
},
"simple": {
input: cty.StringVal("foo/bar"),
want: cty.StringVal("foo_bar"),
},
"simple no slash": {
input: cty.StringVal("foobar"),
want: cty.StringVal("foobar"),
},
"dot": {
input: cty.StringVal("foo/bar."),
want: cty.StringVal("foo_bar_"),
},
"dotdot": {
input: cty.StringVal("foo/bar.."),
want: cty.StringVal("foo_bar__"),
},
"dotdotdot": {
input: cty.StringVal("foo/bar..."),
want: cty.StringVal("foo_bar___"),
},
"utf8": {
input: cty.StringVal("foo/🍕bar"),
want: cty.StringVal("foo__bar"),
},
"symbols": {
input: cty.StringVal("foo/bar!@(ba+z)"),
want: cty.StringVal("foo_bar___ba_z_"),
},
}
for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
got, err := sanitizeFunc().Call([]cty.Value{test.input})
require.NoError(t, err)
require.Equal(t, test.want, got)
})
}
}