From 2df1337300afc9965d3d400f9f5fad5161e91ef2 Mon Sep 17 00:00:00 2001 From: Chris Seto Date: Wed, 28 Feb 2024 16:53:07 -0500 Subject: [PATCH] pkg/gotohelm: implement go to helm transpiler This commit introduces the gotohelm and helmette packages. The combination of which implement a golang to helm template transpiler. Equivalence between the go functions and helm templates is asserted for all test cases. There is no harness for doing so outside of the test environment, consumers will need to perform this assertion themselves. See gotohelm/doc.go for details on the transpiler and helmette/doc.go for details on the "helm in go" environment. gotohelm/testdata contains examples of the transpiled templates. --- cmd/gotohelm/main.go | 67 ++ go.mod | 14 +- go.sum | 47 +- pkg/gotohelm/ast.go | 265 ++++++ pkg/gotohelm/doc.go | 32 + pkg/gotohelm/helmette/doc.go | 7 + pkg/gotohelm/helmette/helm.go | 40 + pkg/gotohelm/helmette/shims.go | 72 ++ pkg/gotohelm/helmette/sprig.go | 159 ++++ pkg/gotohelm/helmette/sprig_test.go | 36 + pkg/gotohelm/rewrite.go | 296 +++++++ pkg/gotohelm/shims.yaml | 32 + pkg/gotohelm/testdata/.gitignore | 1 + .../testdata/src/example/a/_shims.tpl | 34 + .../testdata/src/example/a/_shims.yaml | 35 + pkg/gotohelm/testdata/src/example/a/a.go | 9 + pkg/gotohelm/testdata/src/example/a/a.yaml | 9 + .../testdata/src/example/b/_shims.tpl | 34 + .../testdata/src/example/b/_shims.yaml | 35 + pkg/gotohelm/testdata/src/example/b/b.go | 13 + pkg/gotohelm/testdata/src/example/b/b.yaml | 16 + .../testdata/src/example/bootstrap/_shims.tpl | 34 + .../src/example/bootstrap/bootstrap.go | 43 + .../src/example/bootstrap/bootstrap.yaml | 58 ++ .../src/example/directives/_shims.tpl | 34 + .../src/example/directives/_shims.yaml | 35 + .../src/example/directives/directives.go | 5 + .../src/example/directives/directives.yaml | 9 + .../src/example/directives/ignored.go | 2 + .../src/example/directives/nameoverride.go | 6 + .../src/example/directives/overridden.yaml | 7 + .../src/example/flowcontrol/_shims.tpl | 34 + .../src/example/flowcontrol/_shims.yaml | 35 + .../src/example/flowcontrol/flowcontrol.go | 110 +++ .../src/example/flowcontrol/flowcontrol.yaml | 110 +++ pkg/gotohelm/testdata/src/example/go.mod | 30 + pkg/gotohelm/testdata/src/example/go.sum | 98 +++ .../testdata/src/example/inputs/_shims.tpl | 34 + .../testdata/src/example/inputs/_shims.yaml | 35 + .../testdata/src/example/inputs/inputs.go | 53 ++ .../testdata/src/example/inputs/inputs.yaml | 48 + .../testdata/src/example/k8s/_shims.tpl | 34 + .../testdata/src/example/k8s/_shims.yaml | 35 + pkg/gotohelm/testdata/src/example/k8s/k8s.go | 34 + .../testdata/src/example/k8s/k8s.yaml | 17 + pkg/gotohelm/testdata/src/example/main.go | 103 +++ .../src/example/mutability/_shims.tpl | 34 + .../src/example/mutability/mutability.go | 28 + .../src/example/mutability/mutability.yaml | 15 + .../testdata/src/example/sprig/_shims.tpl | 34 + .../testdata/src/example/sprig/sprig.go | 53 ++ .../testdata/src/example/sprig/sprig.yaml | 26 + .../testdata/src/example/typing/_shims.tpl | 34 + .../testdata/src/example/typing/_shims.yaml | 35 + .../testdata/src/example/typing/asserts.go | 41 + .../testdata/src/example/typing/asserts.yaml | 45 + .../testdata/src/example/typing/structs.go | 83 ++ .../testdata/src/example/typing/structs.yaml | 42 + pkg/gotohelm/transpiler.go | 823 ++++++++++++++++++ pkg/gotohelm/transpiler_test.go | 285 ++++++ 60 files changed, 3870 insertions(+), 4 deletions(-) create mode 100644 cmd/gotohelm/main.go create mode 100644 pkg/gotohelm/ast.go create mode 100644 pkg/gotohelm/doc.go create mode 100644 pkg/gotohelm/helmette/doc.go create mode 100644 pkg/gotohelm/helmette/helm.go create mode 100644 pkg/gotohelm/helmette/shims.go create mode 100644 pkg/gotohelm/helmette/sprig.go create mode 100644 pkg/gotohelm/helmette/sprig_test.go create mode 100644 pkg/gotohelm/rewrite.go create mode 100644 pkg/gotohelm/shims.yaml create mode 100644 pkg/gotohelm/testdata/.gitignore create mode 100644 pkg/gotohelm/testdata/src/example/a/_shims.tpl create mode 100644 pkg/gotohelm/testdata/src/example/a/_shims.yaml create mode 100644 pkg/gotohelm/testdata/src/example/a/a.go create mode 100644 pkg/gotohelm/testdata/src/example/a/a.yaml create mode 100644 pkg/gotohelm/testdata/src/example/b/_shims.tpl create mode 100644 pkg/gotohelm/testdata/src/example/b/_shims.yaml create mode 100644 pkg/gotohelm/testdata/src/example/b/b.go create mode 100644 pkg/gotohelm/testdata/src/example/b/b.yaml create mode 100644 pkg/gotohelm/testdata/src/example/bootstrap/_shims.tpl create mode 100644 pkg/gotohelm/testdata/src/example/bootstrap/bootstrap.go create mode 100644 pkg/gotohelm/testdata/src/example/bootstrap/bootstrap.yaml create mode 100644 pkg/gotohelm/testdata/src/example/directives/_shims.tpl create mode 100644 pkg/gotohelm/testdata/src/example/directives/_shims.yaml create mode 100644 pkg/gotohelm/testdata/src/example/directives/directives.go create mode 100644 pkg/gotohelm/testdata/src/example/directives/directives.yaml create mode 100644 pkg/gotohelm/testdata/src/example/directives/ignored.go create mode 100644 pkg/gotohelm/testdata/src/example/directives/nameoverride.go create mode 100644 pkg/gotohelm/testdata/src/example/directives/overridden.yaml create mode 100644 pkg/gotohelm/testdata/src/example/flowcontrol/_shims.tpl create mode 100644 pkg/gotohelm/testdata/src/example/flowcontrol/_shims.yaml create mode 100644 pkg/gotohelm/testdata/src/example/flowcontrol/flowcontrol.go create mode 100644 pkg/gotohelm/testdata/src/example/flowcontrol/flowcontrol.yaml create mode 100644 pkg/gotohelm/testdata/src/example/go.mod create mode 100644 pkg/gotohelm/testdata/src/example/go.sum create mode 100644 pkg/gotohelm/testdata/src/example/inputs/_shims.tpl create mode 100644 pkg/gotohelm/testdata/src/example/inputs/_shims.yaml create mode 100644 pkg/gotohelm/testdata/src/example/inputs/inputs.go create mode 100644 pkg/gotohelm/testdata/src/example/inputs/inputs.yaml create mode 100644 pkg/gotohelm/testdata/src/example/k8s/_shims.tpl create mode 100644 pkg/gotohelm/testdata/src/example/k8s/_shims.yaml create mode 100644 pkg/gotohelm/testdata/src/example/k8s/k8s.go create mode 100644 pkg/gotohelm/testdata/src/example/k8s/k8s.yaml create mode 100644 pkg/gotohelm/testdata/src/example/main.go create mode 100644 pkg/gotohelm/testdata/src/example/mutability/_shims.tpl create mode 100644 pkg/gotohelm/testdata/src/example/mutability/mutability.go create mode 100644 pkg/gotohelm/testdata/src/example/mutability/mutability.yaml create mode 100644 pkg/gotohelm/testdata/src/example/sprig/_shims.tpl create mode 100644 pkg/gotohelm/testdata/src/example/sprig/sprig.go create mode 100644 pkg/gotohelm/testdata/src/example/sprig/sprig.yaml create mode 100644 pkg/gotohelm/testdata/src/example/typing/_shims.tpl create mode 100644 pkg/gotohelm/testdata/src/example/typing/_shims.yaml create mode 100644 pkg/gotohelm/testdata/src/example/typing/asserts.go create mode 100644 pkg/gotohelm/testdata/src/example/typing/asserts.yaml create mode 100644 pkg/gotohelm/testdata/src/example/typing/structs.go create mode 100644 pkg/gotohelm/testdata/src/example/typing/structs.yaml create mode 100644 pkg/gotohelm/transpiler.go create mode 100644 pkg/gotohelm/transpiler_test.go diff --git a/cmd/gotohelm/main.go b/cmd/gotohelm/main.go new file mode 100644 index 0000000000..af59b14514 --- /dev/null +++ b/cmd/gotohelm/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path" + + "github.com/redpanda-data/helm-charts/pkg/gotohelm" + "golang.org/x/tools/go/packages" +) + +func main() { + out := flag.String("write", "-", "The directory to write the transpiled templates to or - to write them to standard out") + + flag.Parse() + + cwd, _ := os.Getwd() + + pkgs, err := gotohelm.LoadPackages(&packages.Config{ + Dir: cwd, + }, flag.Args()...) + if err != nil { + panic(err) + } + + for _, pkg := range pkgs { + chart, err := gotohelm.Transpile(pkg) + if err != nil { + fmt.Printf("Failed to transpile %q: %s\n", pkg.Name, err) + continue + } + + if *out == "-" { + writeToStdout(chart) + } else { + if err := writeToDir(chart, *out); err != nil { + panic(err) + } + } + + } +} + +func writeToStdout(chart *gotohelm.Chart) { + for _, f := range chart.Files { + fmt.Printf("%s\n", f.Name) + f.Write(os.Stdout) + fmt.Printf("\n\n") + } +} + +func writeToDir(chart *gotohelm.Chart, dir string) error { + for _, f := range chart.Files { + file, err := os.OpenFile(path.Join(dir, f.Name), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return err + } + + f.Write(file) + + if err := file.Close(); err != nil { + return err + } + } + return nil +} diff --git a/go.mod b/go.mod index 7654a7ae07..173014213b 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,16 @@ module github.com/redpanda-data/helm-charts go 1.21.5 require ( + github.com/Masterminds/sprig/v3 v3.2.3 github.com/cockroachdb/errors v1.11.1 github.com/evanphx/json-patch v4.12.0+incompatible github.com/gonvenience/ytbx v1.4.4 github.com/homeport/dyff v1.7.1 github.com/invopop/jsonschema v0.12.0 + github.com/mitchellh/mapstructure v1.5.0 github.com/stretchr/testify v1.8.4 github.com/wk8/go-ordered-map/v2 v2.1.8 + golang.org/x/net v0.21.0 golang.org/x/tools v0.17.0 k8s.io/api v0.29.2 k8s.io/apimachinery v0.29.2 @@ -20,6 +23,8 @@ require ( require ( github.com/BurntSushi/toml v1.3.2 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect @@ -43,7 +48,8 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.11 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kr/pretty v0.3.1 // indirect @@ -52,8 +58,10 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/hashstructure v1.1.0 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -63,12 +71,14 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/spf13/cast v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/texttheater/golang-levenshtein v1.0.1 // indirect github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 // indirect + golang.org/x/crypto v0.19.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.21.0 // indirect golang.org/x/oauth2 v0.12.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.17.0 // indirect diff --git a/go.sum b/go.sum index 3ba8e6a7c2..49c9406f55 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= @@ -70,6 +76,7 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -79,8 +86,10 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/homeport/dyff v1.7.1 h1:B3KJUtnU53H2UryxGcfYKQPrde8VjjbwlHZbczH3giQ= github.com/homeport/dyff v1.7.1/go.mod h1:iLe5b3ymc9xmHZNuJlNVKERE8L2isQMBLxFiTXcwZY0= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -105,10 +114,16 @@ github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 h1:BXxTozrOU8zg github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3/go.mod h1:x1uk6vxTiVuNt6S5R2UYgdhpj3oKojXvOXauHZ7dEnI= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -140,13 +155,19 @@ github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c h1:fPpdjeP github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -160,6 +181,7 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= @@ -167,10 +189,15 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -178,6 +205,9 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= @@ -185,19 +215,30 @@ golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= @@ -206,6 +247,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -228,6 +270,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/gotohelm/ast.go b/pkg/gotohelm/ast.go new file mode 100644 index 0000000000..dc0f36a883 --- /dev/null +++ b/pkg/gotohelm/ast.go @@ -0,0 +1,265 @@ +package gotohelm + +import ( + "fmt" + "io" +) + +type Node interface { + Write(io.Writer) +} + +type Selector struct { + Expr Node + Field string +} + +func (s *Selector) Write(w io.Writer) { + s.Expr.Write(w) + fmt.Fprintf(w, ".%s", s.Field) +} + +type Nil struct{} + +func (*Nil) Write(w io.Writer) { + w.Write([]byte(`(fromJson "null")`)) +} + +type Statement struct { + NoCapture bool + Expr Node +} + +func (s *Statement) Write(w io.Writer) { + fmt.Fprintf(w, "{{- ") + if !s.NoCapture { + fmt.Fprintf(w, "$_ := ") + } + s.Expr.Write(w) + fmt.Fprintf(w, " -}}\n") +} + +type Binary struct { + LHS Node + Op string + RHS Node +} + +func (b *Binary) Write(w io.Writer) { + b.LHS.Write(w) + fmt.Fprintf(w, " %s ", b.Op) + b.RHS.Write(w) +} + +type Ident struct { + Name string +} + +func (i *Ident) Write(w io.Writer) { + fmt.Fprintf(w, "$%s", i.Name) +} + +type BuiltInCall struct { + FuncName string + Arguments []Node +} + +func (c *BuiltInCall) Write(w io.Writer) { + fmt.Fprintf(w, "(%s ", c.FuncName) + for i, arg := range c.Arguments { + if i > 0 { + fmt.Fprintf(w, " ") + } + arg.Write(w) + } + fmt.Fprintf(w, ")") +} + +type Call struct { + FuncName string + Arguments []Node +} + +func (c *Call) Write(w io.Writer) { + args := &DictLiteral{ + KeysValues: []*KeyValue{ + { + Key: `"a"`, + Value: &BuiltInCall{ + FuncName: "list", + Arguments: c.Arguments, + }, + }, + }, + } + + fmt.Fprintf(w, `(get (fromJson (include %q `, c.FuncName) + args.Write(w) + fmt.Fprintf(w, `)) %q)`, "r") +} + +type Assignment struct { + LHS Node + New bool + RHS Node +} + +func (a *Assignment) Write(w io.Writer) { + fmt.Fprintf(w, "{{- ") + a.LHS.Write(w) + fmt.Fprintf(w, " ") + if a.New { + fmt.Fprintf(w, ":") + } + fmt.Fprintf(w, "= ") + a.RHS.Write(w) + fmt.Fprintf(w, " -}}\n") +} + +type DictLiteral struct { + KeysValues []*KeyValue +} + +func (d *DictLiteral) Write(w io.Writer) { + fmt.Fprintf(w, "(dict ") + for _, p := range d.KeysValues { + p.Write(w) + fmt.Fprintf(w, " ") + } + fmt.Fprintf(w, ")") +} + +type KeyValue struct { + Key string + Value Node +} + +func (p *KeyValue) Write(w io.Writer) { + fmt.Fprintf(w, "%s ", p.Key) + p.Value.Write(w) +} + +type File struct { + Source string + Name string + Header string + Funcs []*Func +} + +func (f *File) Write(w io.Writer) { + fmt.Fprintf(w, "{{- /* Generated from %q */ -}}\n\n", f.Source) + w.Write([]byte(f.Header)) + for _, s := range f.Funcs { + s.Write(w) + w.Write([]byte{'\n'}) + } +} + +type Func struct { + Namespace string + Name string + Params []Node + Statements []Node +} + +func (f *Func) Write(w io.Writer) { + fmt.Fprintf(w, "{{- define %q -}}\n", f.Namespace+"."+f.Name) + for i := range f.Params { + fmt.Fprintf(w, "{{- ") + f.Params[i].Write(w) + fmt.Fprintf(w, " := (index .a %d) -}}\n", i) + } + fmt.Fprintf(w, "{{- range $_ := (list 1) -}}\n") + for _, s := range f.Statements { + s.Write(w) + } + fmt.Fprintf(w, "{{- end -}}\n") + fmt.Fprintf(w, "{{- end -}}\n") +} + +type Return struct { + Expr Node +} + +func (r *Return) Write(w io.Writer) { + fmt.Fprintf(w, "{{- (dict %q ", "r") + r.Expr.Write(w) + fmt.Fprintf(w, ") | toJson -}}\n") + fmt.Fprintf(w, "{{- break -}}\n") +} + +type Literal struct { + Value string +} + +func (l *Literal) Write(w io.Writer) { + fmt.Fprintf(w, "%s", l.Value) +} + +type Block struct { + Statements []Node +} + +func (b *Block) Write(w io.Writer) { + for _, s := range b.Statements { + s.Write(w) + } +} + +type Range struct { + Key Node + Value Node + Over Node + Body Node +} + +func (r *Range) Write(w io.Writer) { + fmt.Fprintf(w, "{{- range ") + if r.Key != nil { + r.Key.Write(w) + } else { + w.Write([]byte("$_")) + } + fmt.Fprintf(w, ", ") + if r.Value != nil { + r.Value.Write(w) + } else { + w.Write([]byte("$_")) + } + fmt.Fprintf(w, " := ") + r.Over.Write(w) + fmt.Fprintf(w, " -}}\n") + r.Body.Write(w) + fmt.Fprintf(w, "{{- end -}}\n") +} + +type IfStmt struct { + Init Node + Cond Node + Body Node + Else Node +} + +func (i *IfStmt) Write(w io.Writer) { + if i.Init != nil { + i.Init.Write(w) + } + + fmt.Fprintf(w, "{{- if ") + i.Cond.Write(w) + fmt.Fprintf(w, " -}}\n") + + if i.Body != nil { + i.Body.Write(w) + } + + if i.Else != nil { + fmt.Fprintf(w, "{{- else -}}") + if _, ok := i.Else.(*IfStmt); !ok { + fmt.Fprintf(w, "\n") + } + i.Else.Write(w) + } + + fmt.Fprintf(w, "{{- end -}}\n") +} diff --git a/pkg/gotohelm/doc.go b/pkg/gotohelm/doc.go new file mode 100644 index 0000000000..238fe7f275 --- /dev/null +++ b/pkg/gotohelm/doc.go @@ -0,0 +1,32 @@ +// package gotohelm implements a source to source compiler (transpiler) from go +// to helm templates. +// +// gotohelm relies on the go compiler to type check code and on go test to +// assert the correctness thereof. Doing so allows the transpiling process to +// generally "trust" the code that's being transpiled. After the initial +// parsing and type checking, a collection of AST rewrites are performed to +// convert various bits of go syntax into (mostly) equivalent but more easily +// transpilable syntax. +// +// gotohelm takes the approach of bootstrapping a rudimentary LISP-y +// programming language within helm templates using the available builtins. +// +// # Functions +// Functions are "implemented" by abusing the `include` builtin and are turned +// into `define` blocks. Return values are wrapped in a dictionary and +// marshalled to JSON. (Almost like Internet Explorer circa 2011). +// Function calls are then a pipline of `(include NAME ARGS...) | fromJson | get RETURNKEY` +// +// # Interop +// Transpiled go functions can be invoked within existing templates using the +// following syntax: `((include NAME (dict "a" (list ARGS...))) | fromJson | get "r")` +// +// # Limitations +// - There is no "trap door" to fallback to raw templates +// - Switch statements, in all forms, are not currently supported +// - Code must deal with the "lowest common denominator" of .Values in the form +// of map[string]any. Values coalescing has not yet been implemented. +// - Type assertions don't work. +// - Many helpers and bits of syntax are missing. +// - Most forms of incompatibility are handled with panics and fmt.Sprintf. +package gotohelm diff --git a/pkg/gotohelm/helmette/doc.go b/pkg/gotohelm/helmette/doc.go new file mode 100644 index 0000000000..77d8db5818 --- /dev/null +++ b/pkg/gotohelm/helmette/doc.go @@ -0,0 +1,7 @@ +// package helmette implements golang analogs for the constructs present within +// the helm template rendering environment. +// See also: +// - https://helm.sh/docs/chart_template_guide/function_list/ +// - http://masterminds.github.io/sprig/ +// - https://pkg.go.dev/text/template +package helmette diff --git a/pkg/gotohelm/helmette/helm.go b/pkg/gotohelm/helmette/helm.go new file mode 100644 index 0000000000..0c91d31098 --- /dev/null +++ b/pkg/gotohelm/helmette/helm.go @@ -0,0 +1,40 @@ +package helmette + +// Dot is a representation of the "global" context or `.` in the execution +// of a helm template. +// See also: https://github.com/helm/helm/blob/3764b483b385a12e7d3765bff38eced840362049/pkg/chartutil/values.go#L137-L166 +type Dot struct { + Values Values + Release Release + Chart Chart + // Capabilities +} + +type Release struct { + Name string + Namespace string + Service string + IsUpgrade bool + IsInstall bool + // Revision +} + +type Chart struct { + Name string + Version string + AppVersion string +} + +type Values map[string]any + +func (v Values) AsMap() map[string]any { + if v == nil { + return map[string]any{} + } + return v +} + +// https://helm.sh/docs/howto/charts_tips_and_tricks/#using-the-tpl-function +func Tpl(tpl string, context any) string { + panic("not yet implemented in Go") +} diff --git a/pkg/gotohelm/helmette/shims.go b/pkg/gotohelm/helmette/shims.go new file mode 100644 index 0000000000..25b2b5e75f --- /dev/null +++ b/pkg/gotohelm/helmette/shims.go @@ -0,0 +1,72 @@ +package helmette + +import ( + "time" + + "github.com/mitchellh/mapstructure" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TypeTest is an equivalent of `val, ok := x.(type)` that is exercised as a +// function call rather than a special form of syntax. +// See also: "_shims.typetest". +func TypeTest[T any](val any) (T, bool) { + asT, ok := val.(T) + return asT, ok +} + +// TypeAssertion is an equivalent of `x.(type)` that is exercised as a function +// call rather than a special form of syntax. +// See also: "_shims.typeassertion". +func TypeAssertion[T any](val any) T { + return val.(T) +} + +// DictTest is an equivalent of `val, ok := map[key]` that is exercised as a +// function call rather than a special form of syntax. +// See also: "_shims.dicttest". +// func DictTest[K comparable, V any](m map[K]V, key K) TestResult[V] { +func DictTest[K comparable, V any](m map[K]V, key K) (V, bool) { + val, ok := m[key] + return val, ok +} + +func MustDuration(duration string) *metav1.Duration { + d, err := time.ParseDuration(duration) + if err != nil { + panic(err) + } + return &metav1.Duration{Duration: d} +} + +type Tuple2[T1, T2 any] struct { + T1 T1 + T2 T2 +} + +func Compact2[T1, T2 any](t1, t2 any) Tuple2[T1, T2] { + return Tuple2[T1, T2]{} +} + +// Unwrap "unwraps" .Values into a golang struct. +// DANGER: Unwrap performs no defaulting or validation. At the helm level, this +// is transpiled into .Values.AsMap. +// Callers are responsible for verifying that T is appropriately validated by +// the charts values.json.schema. +func Unwrap[T any](from Values) T { + // TODO might be beneficial to have the helm side of this merge values with + // a zero value of the struct? + var out T + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + TagName: "json", + Result: &out, + }) + if err != nil { + panic(err) + } + + if err := decoder.Decode(from.AsMap()); err != nil { + panic(err) + } + return out +} diff --git a/pkg/gotohelm/helmette/sprig.go b/pkg/gotohelm/helmette/sprig.go new file mode 100644 index 0000000000..734f0f7c5d --- /dev/null +++ b/pkg/gotohelm/helmette/sprig.go @@ -0,0 +1,159 @@ +package helmette + +import ( + "encoding/json" + "fmt" + "maps" + "reflect" + "regexp" + "sort" + "strings" + "text/template" +) + +var ( + // TrimPrefix is the go equivalent of sprig's `trimPrefix` + TrimPrefix = strings.TrimPrefix + + // SortAlpha is the go equivalent of sprig's `sortAlpha` + SortAlpha = sort.Strings + + // SortAlpha is the go equivalent of text/templates's `printf` + Printf = fmt.Sprintf +) + +// KindOf is the go equivalent of sprig's `kindOf`. +func KindOf(v any) string { + return reflect.TypeOf(v).Kind().String() +} + +// KindIs is the go equivalent of sprig's `kindIs`. +func KindIs(kind string, v any) bool { + return KindOf(v) == kind +} + +// Keys is the go equivalent of sprig's `keys`. +func Keys[K comparable, V any](m map[K]V) []K { + return nil +} + +// Merge is a go equivalent of sprig's `merge`. +func Merge[K comparable, V any](dst, src map[K]V) map[K]V { + maps.Copy(dst, src) + return dst +} + +// Dig is a go equivalent of sprig's `dig`. +func Dig(m map[string]any, fallback any, path ...string) any { + val := any(m) + + for _, key := range path { + var ok bool + val, ok = val.(map[string]any) + if !ok { + return fallback + } + + val, ok = val.(map[string]any)[key] + if !ok { + return fallback + } + } + + return val +} + +// Trunc is a go equivalent of sprig's `trunc`. +func Trunc(length int, in string) string { + if len(in) < length { + return in + } + return in[:length] +} + +// Default is a go equivalent of sprig's `default`. +func Default(default_, value any) any { + if Empty(value) { + return default_ + } + return value +} + +// RegexMatch is the go equivalent of sprig's `regexMatch`. +func RegexMatch(pattern, s string) bool { + return regexp.MustCompile(pattern).MatchString(s) +} + +// MustRegexMatch is the go equivalent of sprig's `mustRegexMatch`. +func MustRegexMatch(pattern, s string) { + if !RegexMatch(pattern, s) { + panic("did not match") + } +} + +// Coalesce is the go equivalent of sprig's `coalesce`. +func Coalesce(values ...any) any { + for _, v := range values { + if !Empty(v) { + return v + } + } + return nil +} + +// Empty is the go equivalent of sprig's `empty`. +func Empty(value any) bool { + truthy, ok := template.IsTrue(value) + if !truthy || !ok { + return true + } + return false +} + +// Required is the go equivalent of sprig's `required`. +func Required(msg string, value any) { + if Empty(value) { + Fail(msg) + } +} + +// Fail is the go equivalent of sprig's `fail`. +func Fail(msg string) { + panic(msg) +} + +// ToJSON is the go equivalent of sprig's `toJson`. +func ToJSON(value any) string { + marshalled, err := json.Marshal(value) + if err != nil { + return "" + } + return string(marshalled) +} + +// MustToJSON is the go equivalent of sprig's `mustToJson`. +func MustToJSON(value any) string { + marshalled, err := json.Marshal(value) + if err != nil { + panic(err) + } + return string(marshalled) +} + +// FromJSON is the go equivalent of sprig's `fromJson`. +func FromJSON(data string) any { + var out any + if err := json.Unmarshal([]byte(data), &out); err != nil { + return "" + } + return out +} + +// MustFromJSON is the go equivalent of sprig's `mustFromJson`. +func MustFromJSON(data string) any { + var out any + if err := json.Unmarshal([]byte(data), &out); err != nil { + panic(err) + } + return out +} diff --git a/pkg/gotohelm/helmette/sprig_test.go b/pkg/gotohelm/helmette/sprig_test.go new file mode 100644 index 0000000000..e758f6f47a --- /dev/null +++ b/pkg/gotohelm/helmette/sprig_test.go @@ -0,0 +1,36 @@ +package helmette_test + +import ( + "testing" + + "github.com/redpanda-data/helm-charts/pkg/gotohelm/helmette" + "github.com/stretchr/testify/require" +) + +func TestDig(t *testing.T) { + values := map[string]any{ + "k1": "v1", + "k2": map[string]any{ + "k3": "v3", + "k4": map[string]any{ + "k5": "v5", + }, + }, + } + + require.Equal(t, "v1", helmette.Dig(values, "fallback", "k1")) + require.Equal(t, "v3", helmette.Dig(values, "fallback", "k2", "k3")) + require.Equal(t, "v5", helmette.Dig(values, "fallback", "k2", "k4", "k5")) +} + +func TestDefault(t *testing.T) { + require.Equal(t, 10, helmette.Default(10, 0)) + require.Equal(t, 20, helmette.Default(10, 20)) + + require.Equal(t, "bar", helmette.Default("bar", "")) + require.Equal(t, "foo", helmette.Default("bar", "foo")) + + require.Equal(t, map[string]any{"default": true}, helmette.Default(map[string]any{"default": true}, nil)) + require.Equal(t, map[string]any{"default": true}, helmette.Default(map[string]any{"default": true}, map[string]any{})) + require.Equal(t, map[string]any{"default": false}, helmette.Default(map[string]any{"default": true}, map[string]any{"default": false})) +} diff --git a/pkg/gotohelm/rewrite.go b/pkg/gotohelm/rewrite.go new file mode 100644 index 0000000000..e8c9d1240b --- /dev/null +++ b/pkg/gotohelm/rewrite.go @@ -0,0 +1,296 @@ +package gotohelm + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/token" + "go/types" + + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/packages" +) + +type astRewrite func(*packages.Package, *ast.File) (_ *ast.File, changed bool) + +const ( + shimsPkg = "helmette" + shimsPkgPath = "github.com/redpanda-data/helm-charts/pkg/gotohelm/helmette" +) + +// NB: Order is very important here. +var rewrites = []astRewrite{ + hoistIfs, + rewriteMultiValueSyntaxToHelpers, + rewriteMultiValueReturns, +} + +// LoadPackages is a wrapper around [packages.Load] that performs a handful of +// AST rewrites followed by a second invocation of [packages.Load] to +// appropriately populate the AST. +// AST rewriting is done to keep the transpilation process to be as simple as +// possible. Any unsuported or non-trivially supported expressions/statements +// will be rewritten to supported equivalents instead. +// If need be, the rewritten files can also be dumped to disk and have assertions made +func LoadPackages(cfg *packages.Config, patterns ...string) ([]*packages.Package, error) { + // Ensure we're getting all the values we need (which is pretty much everything...) + cfg.Mode |= packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | + packages.NeedTypes | packages.NeedTypesSizes | packages.NeedSyntax | packages.NeedTypesInfo | + packages.NeedDeps | packages.NeedModule + + pkgs, err := packages.Load(cfg, patterns...) + if err != nil { + return pkgs, err + } + + if cfg.Overlay == nil { + cfg.Overlay = map[string][]byte{} + } + + for _, pkg := range pkgs { + for _, parsed := range pkg.Syntax { + filename := pkg.Fset.File(parsed.Pos()).Name() + + var changed bool + for _, rewrite := range rewrites { + var didChange bool + parsed, didChange = rewrite(pkg, parsed) + changed = changed || didChange + } + + if !changed { + continue + } + + var buf bytes.Buffer + if err := format.Node(&buf, pkg.Fset, parsed); err != nil { + return nil, err + } + + cfg.Overlay[filename] = buf.Bytes() + } + } + + return packages.Load(cfg, patterns...) +} + +// rewriteMultiValueReturns rewrites instances of multi-value returns into an +// equivalent set of statements that utilizes a tuple followed by unpacking it. +// +// x, y := f(a) +// +// mvr := Tuple2(f(a)) +// x := mvr.First +// y := mvr.Second +func rewriteMultiValueReturns(pkg *packages.Package, f *ast.File) (*ast.File, bool) { + fset := pkg.Fset + info := pkg.TypesInfo + + var count int + f = astutil.Apply(f, func(c *astutil.Cursor) bool { + assignment, ok := c.Node().(*ast.AssignStmt) + if !ok || assignment.Tok == token.ASSIGN { + return true + } + if len(assignment.Lhs) < 2 || len(assignment.Rhs) != 1 { + return true + } + + count++ + mvr := ast.NewIdent(fmt.Sprintf("tmp_tuple_%d", count)) + + // TODO might be nicer to call c.InsertAfter in reverse order because + // unpacking ends up looking "backwards". + var typeArgs []ast.Expr + for i, v := range assignment.Lhs { + qualifier := func(p *types.Package) string { + // TODO this doesn't work with import aliases :/ + if p == pkg.Types { + return "" + } + return p.Name() + } + + typeArgs = append(typeArgs, &ast.Ident{Name: types.TypeString(info.TypeOf(v), qualifier)}) + + // Skip over any blackhole assignments. + if ident, ok := v.(*ast.Ident); ok && ident.Name == "_" { + continue + } + + c.InsertAfter(&ast.AssignStmt{ + Lhs: []ast.Expr{v}, + Tok: assignment.Tok, + Rhs: []ast.Expr{ + &ast.SelectorExpr{ + X: mvr, + Sel: ast.NewIdent(fmt.Sprintf("T%d", i+1)), + }, + }, + }) + } + + c.Replace(&ast.AssignStmt{ + Lhs: []ast.Expr{mvr}, + Tok: assignment.Tok, + Rhs: []ast.Expr{ + &ast.CallExpr{ + Fun: &ast.IndexListExpr{ + X: &ast.SelectorExpr{ + X: ast.NewIdent(shimsPkg), + Sel: ast.NewIdent("Compact2"), + }, + Indices: typeArgs, + }, + Args: assignment.Rhs, + }, + }, + }) + + return true + }, nil).(*ast.File) + + if count > 0 { + _ = astutil.AddImport(fset, f, shimsPkgPath) + } + + return f, count > 0 +} + +// rewriteMultiValueSyntaxToHelpers rewrites instances of multi-value return +// syntax, such as dictionary tests and type tests into equivalent function +// invocations. +// +// t, ok := x.(type) +// +// t, ok := TypeAssertion[type](x) +func rewriteMultiValueSyntaxToHelpers(pkg *packages.Package, f *ast.File) (*ast.File, bool) { + count := 0 + fset := pkg.Fset + + f = astutil.Apply(f, func(c *astutil.Cursor) bool { + assignment, ok := c.Parent().(*ast.AssignStmt) + if !ok { + return true + } + + if len(assignment.Lhs) != 2 || len(assignment.Rhs) != 1 { + return true + } + + if assignment.Rhs[0] != c.Node() { + return true + } + + switch node := c.Node().(type) { + case *ast.IndexExpr: + count++ + // x, ok := y.[key] -> x, ok := DictTest(y, key) + c.Replace(&ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: ast.NewIdent(shimsPkg), + Sel: ast.NewIdent("DictTest"), + }, + Args: []ast.Expr{node.X, node.Index}, + }) + + case *ast.TypeAssertExpr: + count++ + // x, ok := y.(type) -> x, ok := TypeTest[type](y) + c.Replace(&ast.CallExpr{ + Fun: &ast.IndexExpr{ + X: &ast.SelectorExpr{ + X: ast.NewIdent(shimsPkg), + Sel: ast.NewIdent("TypeTest"), + }, + Index: node.Type, + }, + Args: []ast.Expr{node.X}, + }) + } + + return true + }, nil).(*ast.File) + + if count > 0 { + _ = astutil.AddImport(fset, f, shimsPkgPath) + } + + return f, count > 0 +} + +// hoistIfs "hoists" all assignments within an if else chain to be above said +// chain. It munges the variable names to ensure that variable shadowing +// doesn't become an issues. +// NOTE: All assignments within if-else chains MUST expect to be called as if +// hoisting nullifies the capabilities of short-circuiting. +// +// if x, ok := m[k1]; ok { +// } y, ok := m[k2]; ok { +// } +// +// Will get rewritten to: +// +// x, ok_1 := m[k1] +// y, ok_2 := m[k2] +// +// if ok_1 { +// } else if ok_2 { +// } +func hoistIfs(pkg *packages.Package, f *ast.File) (*ast.File, bool) { + count := 0 + info := pkg.TypesInfo + renames := map[*ast.Object]*ast.Ident{} + + return astutil.Apply(f, func(c *astutil.Cursor) bool { + node, ok := c.Node().(*ast.IfStmt) + if !ok || node.Init == nil { + return true + } + + for _, v := range node.Init.(*ast.AssignStmt).Lhs { + old := v.(*ast.Ident) + if old.Name == "_" { + continue + } + + count++ + new := ast.NewIdent(fmt.Sprintf("%s_%d", old.Name, count)) + new.Obj = old.Obj + + renames[old.Obj] = new + + info.Defs[new] = info.Defs[old] + info.Instances[new] = info.Instances[old] + } + + return true + }, func(c *astutil.Cursor) bool { + switch node := c.Node().(type) { + case *ast.Ident: + if rename, ok := renames[node.Obj]; ok { + c.Replace(rename) + } + + case *ast.IfStmt: + // Don't process if-else statements as c.InsertBefore will panic. + // Instead, we loop through the first if and hoist all child + // assignments. + if _, ok := c.Parent().(*ast.IfStmt); ok { + return true + } + + for n := node; n != nil; { + if n.Init != nil { + c.InsertBefore(n.Init) + n.Init = nil + } + + n, _ = n.Else.(*ast.IfStmt) + } + } + + return true + }).(*ast.File), count > 0 +} diff --git a/pkg/gotohelm/shims.yaml b/pkg/gotohelm/shims.yaml new file mode 100644 index 0000000000..6b8eed3be1 --- /dev/null +++ b/pkg/gotohelm/shims.yaml @@ -0,0 +1,32 @@ +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/.gitignore b/pkg/gotohelm/testdata/.gitignore new file mode 100644 index 0000000000..01d0a08458 --- /dev/null +++ b/pkg/gotohelm/testdata/.gitignore @@ -0,0 +1 @@ +pkg/ diff --git a/pkg/gotohelm/testdata/src/example/a/_shims.tpl b/pkg/gotohelm/testdata/src/example/a/_shims.tpl new file mode 100644 index 0000000000..130e24f6e1 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/a/_shims.tpl @@ -0,0 +1,34 @@ +{{- /* Generated from "" */ -}} + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/a/_shims.yaml b/pkg/gotohelm/testdata/src/example/a/_shims.yaml new file mode 100644 index 0000000000..ec285f3fa7 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/a/_shims.yaml @@ -0,0 +1,35 @@ +{{- /* Generated from "" */ -}} + + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/a/a.go b/pkg/gotohelm/testdata/src/example/a/a.go new file mode 100644 index 0000000000..77666ac0dd --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/a/a.go @@ -0,0 +1,9 @@ +package a + +func ConfigMap() map[string]any { + return map[string]any{ + "apiVersion": "v1", + "kind": "ConfigMap", + "name": "foo", + } +} diff --git a/pkg/gotohelm/testdata/src/example/a/a.yaml b/pkg/gotohelm/testdata/src/example/a/a.yaml new file mode 100644 index 0000000000..a56e04b8ad --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/a/a.yaml @@ -0,0 +1,9 @@ +{{- /* Generated from "a.go" */ -}} + +{{- define "a.ConfigMap" -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" (dict "apiVersion" "v1" "kind" "ConfigMap" "name" "foo" )) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + diff --git a/pkg/gotohelm/testdata/src/example/b/_shims.tpl b/pkg/gotohelm/testdata/src/example/b/_shims.tpl new file mode 100644 index 0000000000..130e24f6e1 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/b/_shims.tpl @@ -0,0 +1,34 @@ +{{- /* Generated from "" */ -}} + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/b/_shims.yaml b/pkg/gotohelm/testdata/src/example/b/_shims.yaml new file mode 100644 index 0000000000..ec285f3fa7 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/b/_shims.yaml @@ -0,0 +1,35 @@ +{{- /* Generated from "" */ -}} + + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/b/b.go b/pkg/gotohelm/testdata/src/example/b/b.go new file mode 100644 index 0000000000..61d8806ba2 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/b/b.go @@ -0,0 +1,13 @@ +package b + +func Constant() string { + return "foo" +} + +func ConfigMap() map[string]any { + return map[string]any{ + "apiVersion": "v1", + "kind": "ConfigMap", + "name": Constant(), + } +} diff --git a/pkg/gotohelm/testdata/src/example/b/b.yaml b/pkg/gotohelm/testdata/src/example/b/b.yaml new file mode 100644 index 0000000000..79a627af5f --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/b/b.yaml @@ -0,0 +1,16 @@ +{{- /* Generated from "b.go" */ -}} + +{{- define "b.Constant" -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" "foo") | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "b.ConfigMap" -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" (dict "apiVersion" "v1" "kind" "ConfigMap" "name" (get (fromJson (include "b.Constant" (dict "a" (list ) ))) "r") )) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + diff --git a/pkg/gotohelm/testdata/src/example/bootstrap/_shims.tpl b/pkg/gotohelm/testdata/src/example/bootstrap/_shims.tpl new file mode 100644 index 0000000000..130e24f6e1 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/bootstrap/_shims.tpl @@ -0,0 +1,34 @@ +{{- /* Generated from "" */ -}} + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/bootstrap/bootstrap.go b/pkg/gotohelm/testdata/src/example/bootstrap/bootstrap.go new file mode 100644 index 0000000000..992ca1168b --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/bootstrap/bootstrap.go @@ -0,0 +1,43 @@ +package bootstrap + +import ( + "fmt" + + "github.com/redpanda-data/helm-charts/pkg/gotohelm/helmette" +) + +type TypeSpec struct { + ExpectedType string + DefaultValue any +} + +func hydrate(in any) any { + return in +} + +func mustget(d map[string]any, key string) any { + value, ok := d[key] + if !ok { + panic(fmt.Sprintf("missing key %q", key)) + } + return value +} + +func zeroof(kind string) any { + if kind == "int" { + return 0 + } else if kind == "string" { + return "" + } else if kind == "slice" { + return []any{} // TODO is this technically correct? + } else { + panic(fmt.Sprintf("unhandled kind %q", kind)) + } +} + +func typetest(kind string, value any) []any { + if helmette.KindOf(value) == kind { + return []any{value, true} + } + return []any{zeroof(kind), false} +} diff --git a/pkg/gotohelm/testdata/src/example/bootstrap/bootstrap.yaml b/pkg/gotohelm/testdata/src/example/bootstrap/bootstrap.yaml new file mode 100644 index 0000000000..dd88860843 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/bootstrap/bootstrap.yaml @@ -0,0 +1,58 @@ +{{- /* Generated from "bootstrap.go" */ -}} + +{{- define "bootstrap.hydrate" -}} +{{- $in := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" $in) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "bootstrap.mustget" -}} +{{- $d := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- range $_ := (list 1) -}} +{{- $tmp_tuple_1 := (get (fromJson (include "_shims.compact" (dict "a" (list (get (fromJson (include "_shims.dicttest" (dict "a" (list $d $key) ))) "r")) ))) "r") -}} +{{- $ok := $tmp_tuple_1.T2 -}} +{{- $value := $tmp_tuple_1.T1 -}} +{{- if (not $ok) -}} +{{- $_ := (fail (printf "missing key %q" $key)) -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "bootstrap.zeroof" -}} +{{- $kind := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- if (eq $kind "int") -}} +{{- (dict "r" 0) | toJson -}} +{{- break -}} +{{- else -}}{{- if (eq $kind "string") -}} +{{- (dict "r" "") | toJson -}} +{{- break -}} +{{- else -}}{{- if (eq $kind "slice") -}} +{{- (dict "r" (list )) | toJson -}} +{{- break -}} +{{- else -}} +{{- $_ := (fail (printf "unhandled kind %q" $kind)) -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{- define "bootstrap.typetest" -}} +{{- $kind := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- range $_ := (list 1) -}} +{{- if (eq (kindOf $value) $kind) -}} +{{- (dict "r" (list $value true)) | toJson -}} +{{- break -}} +{{- end -}} +{{- (dict "r" (list (get (fromJson (include "bootstrap.zeroof" (dict "a" (list $kind) ))) "r") false)) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + diff --git a/pkg/gotohelm/testdata/src/example/directives/_shims.tpl b/pkg/gotohelm/testdata/src/example/directives/_shims.tpl new file mode 100644 index 0000000000..130e24f6e1 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/directives/_shims.tpl @@ -0,0 +1,34 @@ +{{- /* Generated from "" */ -}} + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/directives/_shims.yaml b/pkg/gotohelm/testdata/src/example/directives/_shims.yaml new file mode 100644 index 0000000000..ec285f3fa7 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/directives/_shims.yaml @@ -0,0 +1,35 @@ +{{- /* Generated from "" */ -}} + + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/directives/directives.go b/pkg/gotohelm/testdata/src/example/directives/directives.go new file mode 100644 index 0000000000..b9572d3940 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/directives/directives.go @@ -0,0 +1,5 @@ +package directives + +func Directives() bool { + return true +} diff --git a/pkg/gotohelm/testdata/src/example/directives/directives.yaml b/pkg/gotohelm/testdata/src/example/directives/directives.yaml new file mode 100644 index 0000000000..c2f2df6865 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/directives/directives.yaml @@ -0,0 +1,9 @@ +{{- /* Generated from "directives.go" */ -}} + +{{- define "directives.Directives" -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" true) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + diff --git a/pkg/gotohelm/testdata/src/example/directives/ignored.go b/pkg/gotohelm/testdata/src/example/directives/ignored.go new file mode 100644 index 0000000000..ce04bbdc50 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/directives/ignored.go @@ -0,0 +1,2 @@ +//+gotohelm:ignore=true +package directives diff --git a/pkg/gotohelm/testdata/src/example/directives/nameoverride.go b/pkg/gotohelm/testdata/src/example/directives/nameoverride.go new file mode 100644 index 0000000000..01199a5e7b --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/directives/nameoverride.go @@ -0,0 +1,6 @@ +// +gotohelm:filename=overridden.yaml +package directives + +// +gotohelm:name=does-something +func Noop() { +} diff --git a/pkg/gotohelm/testdata/src/example/directives/overridden.yaml b/pkg/gotohelm/testdata/src/example/directives/overridden.yaml new file mode 100644 index 0000000000..d5bb7820d7 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/directives/overridden.yaml @@ -0,0 +1,7 @@ +{{- /* Generated from "nameoverride.go" */ -}} + +{{- define "directives.does-something" -}} +{{- range $_ := (list 1) -}} +{{- end -}} +{{- end -}} + diff --git a/pkg/gotohelm/testdata/src/example/flowcontrol/_shims.tpl b/pkg/gotohelm/testdata/src/example/flowcontrol/_shims.tpl new file mode 100644 index 0000000000..130e24f6e1 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/flowcontrol/_shims.tpl @@ -0,0 +1,34 @@ +{{- /* Generated from "" */ -}} + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/flowcontrol/_shims.yaml b/pkg/gotohelm/testdata/src/example/flowcontrol/_shims.yaml new file mode 100644 index 0000000000..ec285f3fa7 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/flowcontrol/_shims.yaml @@ -0,0 +1,35 @@ +{{- /* Generated from "" */ -}} + + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/flowcontrol/flowcontrol.go b/pkg/gotohelm/testdata/src/example/flowcontrol/flowcontrol.go new file mode 100644 index 0000000000..3f686d77e8 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/flowcontrol/flowcontrol.go @@ -0,0 +1,110 @@ +package flowcontrol + +import ( + "github.com/redpanda-data/helm-charts/pkg/gotohelm/helmette" +) + +func FlowControl(dot *helmette.Dot) map[string]any { + return map[string]any{ + "earlyReturn": earlyReturn(dot), + "ifElse": ifElse(dot), + "sliceRanges": sliceRanges(dot), + "mapRanges": mapRanges(dot), + "intBinaryExprs": intBinaryExprs(), + } +} + +func earlyReturn(dot *helmette.Dot) string { + // This is trickily written on purpose. + if b, ok := dot.Values["boolean"]; ok && b.(bool) { + return "Early Returns work!" + } + return "Should have returned early" +} + +func ifElse(dot *helmette.Dot) string { + oneToFour, ok := dot.Values["oneToFour"] + if !ok { + return "oneToFour not specified!" + } + + if int(oneToFour.(float64)) == 1 { + return "It's 1" + } else if int(oneToFour.(float64)) == 2 { + return "It's 2" + } else if int(oneToFour.(float64)) == 3 { + return "It's 3" + } else { + return "It's 4" + } + return "unreachable" +} + +func sliceRanges(dot *helmette.Dot) []any { + intsAny, ok := dot.Values["ints"] + if !ok { + intsAny = []any{} + } + + ints := intsAny.([]any) + + sumOfIndexes := 0 + for i := range ints { + sumOfIndexes = sumOfIndexes + i + } + + continuesWork := true + for range ints { + continue + continuesWork = false + } + + breaksWork := true + for range ints { + break + breaksWork = false + } + + return []any{ + sumOfIndexes, + continuesWork, + breaksWork, + } +} + +func mapRanges(dot *helmette.Dot) []any { + m := map[string]int{"1": 1, "2": 2, "3": 3} + + // NOTE: Ranges of maps are not technically equivalent. In go, they are + // non-deterministic but range nodes with templates are deterministic. + for k := range m { + _ = k + } + + sum := 0 + for _, v := range m { + sum = sum + v + } + + return []any{sum} +} + +func intBinaryExprs() []int { + x := 1 + y := 2 + z := 3 + + // Not currently supported. + // z += x + // z -= y + // z *= y + // z /= y + + return []int{ + z, + x - y, + x + y, + x / y, + x * y, + } +} diff --git a/pkg/gotohelm/testdata/src/example/flowcontrol/flowcontrol.yaml b/pkg/gotohelm/testdata/src/example/flowcontrol/flowcontrol.yaml new file mode 100644 index 0000000000..c04a1a8886 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/flowcontrol/flowcontrol.yaml @@ -0,0 +1,110 @@ +{{- /* Generated from "flowcontrol.go" */ -}} + +{{- define "flowcontrol.FlowControl" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" (dict "earlyReturn" (get (fromJson (include "flowcontrol.earlyReturn" (dict "a" (list $dot) ))) "r") "ifElse" (get (fromJson (include "flowcontrol.ifElse" (dict "a" (list $dot) ))) "r") "sliceRanges" (get (fromJson (include "flowcontrol.sliceRanges" (dict "a" (list $dot) ))) "r") "mapRanges" (get (fromJson (include "flowcontrol.mapRanges" (dict "a" (list $dot) ))) "r") "intBinaryExprs" (get (fromJson (include "flowcontrol.intBinaryExprs" (dict "a" (list ) ))) "r") )) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "flowcontrol.earlyReturn" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- $tmp_tuple_1 := (get (fromJson (include "_shims.compact" (dict "a" (list (get (fromJson (include "_shims.dicttest" (dict "a" (list $dot.Values "boolean") ))) "r")) ))) "r") -}} +{{- $ok_2 := $tmp_tuple_1.T2 -}} +{{- $b_1 := $tmp_tuple_1.T1 -}} +{{- if (and $ok_2 $b_1) -}} +{{- (dict "r" "Early Returns work!") | toJson -}} +{{- break -}} +{{- end -}} +{{- (dict "r" "Should have returned early") | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "flowcontrol.ifElse" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- $tmp_tuple_2 := (get (fromJson (include "_shims.compact" (dict "a" (list (get (fromJson (include "_shims.dicttest" (dict "a" (list $dot.Values "oneToFour") ))) "r")) ))) "r") -}} +{{- $ok := $tmp_tuple_2.T2 -}} +{{- $oneToFour := $tmp_tuple_2.T1 -}} +{{- if (not $ok) -}} +{{- (dict "r" "oneToFour not specified!") | toJson -}} +{{- break -}} +{{- end -}} +{{- if (eq (int $oneToFour) 1) -}} +{{- (dict "r" "It's 1") | toJson -}} +{{- break -}} +{{- else -}}{{- if (eq (int $oneToFour) 2) -}} +{{- (dict "r" "It's 2") | toJson -}} +{{- break -}} +{{- else -}}{{- if (eq (int $oneToFour) 3) -}} +{{- (dict "r" "It's 3") | toJson -}} +{{- break -}} +{{- else -}} +{{- (dict "r" "It's 4") | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- (dict "r" "unreachable") | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "flowcontrol.sliceRanges" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- $tmp_tuple_3 := (get (fromJson (include "_shims.compact" (dict "a" (list (get (fromJson (include "_shims.dicttest" (dict "a" (list $dot.Values "ints") ))) "r")) ))) "r") -}} +{{- $ok := $tmp_tuple_3.T2 -}} +{{- $intsAny := $tmp_tuple_3.T1 -}} +{{- if (not $ok) -}} +{{- $intsAny = (list ) -}} +{{- end -}} +{{- $ints := $intsAny -}} +{{- $sumOfIndexes := 0 -}} +{{- range $i, $_ := $ints -}} +{{- $sumOfIndexes = (add $sumOfIndexes $i) -}} +{{- end -}} +{{- $continuesWork := true -}} +{{- range $_, $_ := $ints -}} +{{- continue -}} +{{- $continuesWork = false -}} +{{- end -}} +{{- $breaksWork := true -}} +{{- range $_, $_ := $ints -}} +{{- break -}} +{{- $breaksWork = false -}} +{{- end -}} +{{- (dict "r" (list $sumOfIndexes $continuesWork $breaksWork)) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "flowcontrol.mapRanges" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- $m := (dict "1" 1 "2" 2 "3" 3 ) -}} +{{- range $k, $_ := $m -}} +{{- $_ = $k -}} +{{- end -}} +{{- $sum := 0 -}} +{{- range $_, $v := $m -}} +{{- $sum = (add $sum $v) -}} +{{- end -}} +{{- (dict "r" (list $sum)) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "flowcontrol.intBinaryExprs" -}} +{{- range $_ := (list 1) -}} +{{- $x := 1 -}} +{{- $y := 2 -}} +{{- $z := 3 -}} +{{- (dict "r" (list $z (sub $x $y) (add $x $y) (div $x $y) (mul $x $y))) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + diff --git a/pkg/gotohelm/testdata/src/example/go.mod b/pkg/gotohelm/testdata/src/example/go.mod new file mode 100644 index 0000000000..1003a1f072 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/go.mod @@ -0,0 +1,30 @@ +module example.com/example + +go 1.21.5 + +require ( + github.com/redpanda-data/helm-charts v0.0.0-00010101000000-000000000000 + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 + k8s.io/api v0.29.2 + k8s.io/apimachinery v0.29.2 +) + +require ( + github.com/go-logr/logr v1.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect +) + +replace github.com/redpanda-data/helm-charts => ../../../../../ diff --git a/pkg/gotohelm/testdata/src/example/go.sum b/pkg/gotohelm/testdata/src/example/go.sum new file mode 100644 index 0000000000..ed103ed57f --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/go.sum @@ -0,0 +1,98 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= +k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0= +k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= +k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= +k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/pkg/gotohelm/testdata/src/example/inputs/_shims.tpl b/pkg/gotohelm/testdata/src/example/inputs/_shims.tpl new file mode 100644 index 0000000000..130e24f6e1 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/inputs/_shims.tpl @@ -0,0 +1,34 @@ +{{- /* Generated from "" */ -}} + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/inputs/_shims.yaml b/pkg/gotohelm/testdata/src/example/inputs/_shims.yaml new file mode 100644 index 0000000000..ec285f3fa7 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/inputs/_shims.yaml @@ -0,0 +1,35 @@ +{{- /* Generated from "" */ -}} + + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/inputs/inputs.go b/pkg/gotohelm/testdata/src/example/inputs/inputs.go new file mode 100644 index 0000000000..b622b432a5 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/inputs/inputs.go @@ -0,0 +1,53 @@ +package inputs + +import ( + "slices" + + "github.com/redpanda-data/helm-charts/pkg/gotohelm/helmette" + "golang.org/x/exp/maps" +) + +type Nested struct { + Quux any `json:"quux,omitempty"` +} + +type Values struct { + Foo any `json:"foo,omitempty"` + Bar string `json:"bar,omitempty"` + Nested Nested `json:"nested,omitempty"` +} + +func Inputs(dot *helmette.Dot) map[string]any { + return map[string]any{ + "unwrap": unwrap(dot), + "echo": echo(dot), + "digCompat": digCompat(dot), + "keys": keys(dot), + } +} + +func unwrap(dot *helmette.Dot) Nested { + return helmette.Unwrap[Values](dot.Values).Nested +} + +func echo(globals *helmette.Dot) map[string]any { + return globals.Values +} + +func digCompat(dot *helmette.Dot) string { + return helmette.Dig(dot.Values.AsMap(), "hello", "doesn't", "exist").(string) +} + +func keys(globals *helmette.Dot) []string { + // Get the keys in all possible ways but only return the stable ones. + + keys := []string{} + for key := range globals.Values { + keys = append(keys, key) + } + + keys = maps.Keys(globals.Values) + slices.Sort(keys) + + return keys +} diff --git a/pkg/gotohelm/testdata/src/example/inputs/inputs.yaml b/pkg/gotohelm/testdata/src/example/inputs/inputs.yaml new file mode 100644 index 0000000000..4917122d5f --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/inputs/inputs.yaml @@ -0,0 +1,48 @@ +{{- /* Generated from "inputs.go" */ -}} + +{{- define "inputs.Inputs" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" (dict "unwrap" (get (fromJson (include "inputs.unwrap" (dict "a" (list $dot) ))) "r") "echo" (get (fromJson (include "inputs.echo" (dict "a" (list $dot) ))) "r") "digCompat" (get (fromJson (include "inputs.digCompat" (dict "a" (list $dot) ))) "r") "keys" (get (fromJson (include "inputs.keys" (dict "a" (list $dot) ))) "r") )) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "inputs.unwrap" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" $dot.Values.AsMap.nested) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "inputs.echo" -}} +{{- $globals := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" $globals.Values) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "inputs.digCompat" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" (dig "doesn't" "exist" "hello" $dot.Values.AsMap)) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "inputs.keys" -}} +{{- $globals := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- $keys := (list ) -}} +{{- range $key, $_ := $globals.Values -}} +{{- $keys = (mustAppend $keys $key) -}} +{{- end -}} +{{- $keys = (keys $globals.Values) -}} +{{- $_ := (sortAlpha $keys) -}} +{{- (dict "r" $keys) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + diff --git a/pkg/gotohelm/testdata/src/example/k8s/_shims.tpl b/pkg/gotohelm/testdata/src/example/k8s/_shims.tpl new file mode 100644 index 0000000000..130e24f6e1 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/k8s/_shims.tpl @@ -0,0 +1,34 @@ +{{- /* Generated from "" */ -}} + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/k8s/_shims.yaml b/pkg/gotohelm/testdata/src/example/k8s/_shims.yaml new file mode 100644 index 0000000000..ec285f3fa7 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/k8s/_shims.yaml @@ -0,0 +1,35 @@ +{{- /* Generated from "" */ -}} + + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/k8s/k8s.go b/pkg/gotohelm/testdata/src/example/k8s/k8s.go new file mode 100644 index 0000000000..970151874d --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/k8s/k8s.go @@ -0,0 +1,34 @@ +package k8s + +import ( + corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func Pod() corev1.Pod { + return corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "spacename", + Name: "eman", + }, + } +} + +func PDB() policyv1.PodDisruptionBudget { + minAvail := intstr.FromInt32(3) + return policyv1.PodDisruptionBudget{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "PodDisruptionBudget", + }, + Spec: policyv1.PodDisruptionBudgetSpec{ + MinAvailable: &minAvail, + }, + } +} diff --git a/pkg/gotohelm/testdata/src/example/k8s/k8s.yaml b/pkg/gotohelm/testdata/src/example/k8s/k8s.yaml new file mode 100644 index 0000000000..754f9380e6 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/k8s/k8s.yaml @@ -0,0 +1,17 @@ +{{- /* Generated from "k8s.go" */ -}} + +{{- define "k8s.Pod" -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" (mustMergeOverwrite (mustMergeOverwrite (dict ) (dict "metadata" (dict "creationTimestamp" (fromJson "null") ) "spec" (dict "containers" (fromJson "null") ) "status" (dict ) )) (mustMergeOverwrite (dict ) (dict "apiVersion" "v1" "kind" "Pod" )) (dict "metadata" (mustMergeOverwrite (dict "creationTimestamp" (fromJson "null") ) (dict "namespace" "spacename" "name" "eman" )) ))) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "k8s.PDB" -}} +{{- range $_ := (list 1) -}} +{{- $minAvail := 3 -}} +{{- (dict "r" (mustMergeOverwrite (mustMergeOverwrite (dict ) (dict "metadata" (dict "creationTimestamp" (fromJson "null") ) "spec" (dict ) "status" (dict "disruptionsAllowed" 0 "currentHealthy" 0 "desiredHealthy" 0 "expectedPods" 0 ) )) (mustMergeOverwrite (dict ) (dict "apiVersion" "v1" "kind" "PodDisruptionBudget" )) (dict "spec" (mustMergeOverwrite (dict ) (dict "minAvailable" $minAvail )) ))) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + diff --git a/pkg/gotohelm/testdata/src/example/main.go b/pkg/gotohelm/testdata/src/example/main.go new file mode 100644 index 0000000000..4cd70dade5 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "example.com/example/a" + "example.com/example/b" + "example.com/example/directives" + "example.com/example/flowcontrol" + "example.com/example/inputs" + "example.com/example/k8s" + "example.com/example/mutability" + "example.com/example/sprig" + "example.com/example/typing" + "github.com/redpanda-data/helm-charts/pkg/gotohelm/helmette" +) + +func main() { + enc := json.NewEncoder(os.Stdout) + dec := json.NewDecoder(os.Stdin) + + for { + var dot helmette.Dot + if err := dec.Decode(&dot); err != nil { + if err == io.EOF { + break + } + panic(err) + } + + out, err := runChart(&dot) + + if out == nil { + out = map[string]any{} + } + + if err := enc.Encode(map[string]any{ + "result": out, + "err": err, + }); err != nil { + panic(err) + } + } +} + +func runChart(dot *helmette.Dot) (_ map[string]any, err any) { + defer func() { err = recover() }() + + switch dot.Chart.Name { + case "sprig": + return map[string]any{ + "Sprig": sprig.Sprig(), + }, nil + + case "a": + return map[string]any{ + "ConfigMap": a.ConfigMap(), + }, nil + + case "b": + return map[string]any{ + "Constant": b.Constant(), + "ConfigMap": b.ConfigMap(), + }, nil + + case "typing": + return map[string]any{ + "Typing": typing.Typing(dot), + }, nil + + case "directives": + return map[string]any{ + "Directives": directives.Directives(), + }, nil + + case "mutability": + return map[string]any{ + "Mutability": mutability.Mutability(), + }, nil + + case "k8s": + return map[string]any{ + "Pod": k8s.Pod(), + "PDB": k8s.PDB(), + }, nil + + case "flowcontrol": + return map[string]any{ + "FlowControl": flowcontrol.FlowControl(dot), + }, nil + + case "inputs": + return map[string]any{ + "Inputs": inputs.Inputs(dot), + }, nil + + default: + panic(fmt.Sprintf("unknown package %q", dot.Chart.Name)) + } +} diff --git a/pkg/gotohelm/testdata/src/example/mutability/_shims.tpl b/pkg/gotohelm/testdata/src/example/mutability/_shims.tpl new file mode 100644 index 0000000000..130e24f6e1 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/mutability/_shims.tpl @@ -0,0 +1,34 @@ +{{- /* Generated from "" */ -}} + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/mutability/mutability.go b/pkg/gotohelm/testdata/src/example/mutability/mutability.go new file mode 100644 index 0000000000..4394124988 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/mutability/mutability.go @@ -0,0 +1,28 @@ +package mutability + +import "github.com/redpanda-data/helm-charts/pkg/gotohelm/helmette" + +type Values struct { + Name string + Labels map[string]string + SubService *SubService +} + +type SubService struct { + Name string + Labels map[string]string +} + +func Mutability() map[string]any { + var v Values + v.Labels = map[string]string{} + v.SubService = &SubService{} + v.SubService.Labels = map[string]string{} + + v.SubService.Name = "Hello!" + v.SubService.Labels["hello"] = "world" + + return map[string]any{ + "values": helmette.MustFromJSON(helmette.MustToJSON(v)), + } +} diff --git a/pkg/gotohelm/testdata/src/example/mutability/mutability.yaml b/pkg/gotohelm/testdata/src/example/mutability/mutability.yaml new file mode 100644 index 0000000000..198bec04a7 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/mutability/mutability.yaml @@ -0,0 +1,15 @@ +{{- /* Generated from "mutability.go" */ -}} + +{{- define "mutability.Mutability" -}} +{{- range $_ := (list 1) -}} +{{- $v := (dict "Name" "" "Labels" (fromJson "null") "SubService" (fromJson "null") ) -}} +{{- $_ := (set $v "Labels" (dict )) -}} +{{- $_ := (set $v "SubService" (mustMergeOverwrite (dict "Name" "" "Labels" (fromJson "null") ) (dict ))) -}} +{{- $_ := (set $v.SubService "Labels" (dict )) -}} +{{- $_ := (set $v.SubService "Name" "Hello!") -}} +{{- $_ := (set $v.SubService.Labels "hello" "world") -}} +{{- (dict "r" (dict "values" (mustFromJson (mustToJson $v)) )) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + diff --git a/pkg/gotohelm/testdata/src/example/sprig/_shims.tpl b/pkg/gotohelm/testdata/src/example/sprig/_shims.tpl new file mode 100644 index 0000000000..130e24f6e1 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/sprig/_shims.tpl @@ -0,0 +1,34 @@ +{{- /* Generated from "" */ -}} + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/sprig/sprig.go b/pkg/gotohelm/testdata/src/example/sprig/sprig.go new file mode 100644 index 0000000000..a169773ec1 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/sprig/sprig.go @@ -0,0 +1,53 @@ +package sprig + +import ( + "github.com/redpanda-data/helm-charts/pkg/gotohelm/helmette" +) + +type AStruct struct { + Value int +} + +// Sprig runs a variety of values through various sprig functions. Assertions +// are no performed within this code, we're merely testing that the functions +// in helmette return the same values as the transpiled versions. +func Sprig() map[string]any { + return map[string]any{ + "empty": empty(), + "default": default_(), + } +} + +func default_() []any { + defaultStr := "DEFAULT" + defaultInt := 1234 + defaultStrSlice := []string{defaultStr} + + return []any{ + helmette.Default("", defaultStr), + helmette.Default("value", defaultStr), + helmette.Default(nil, defaultStrSlice), + helmette.Default([]string{}, defaultStrSlice), + helmette.Default(0, defaultInt), + helmette.Default(1, defaultInt), + } +} + +func empty() []bool { + return []bool{ + helmette.Empty(nil), + helmette.Empty([]string{}), + helmette.Empty([]string{""}), + helmette.Empty(map[string]any{}), + helmette.Empty(map[string]any{"key": nil}), + helmette.Empty(1), + helmette.Empty(0), + helmette.Empty(false), + helmette.Empty(true), + helmette.Empty(""), + helmette.Empty("hello"), + helmette.Empty(AStruct{}), + helmette.Empty(AStruct{Value: 0}), + helmette.Empty(AStruct{Value: 1}), + } +} diff --git a/pkg/gotohelm/testdata/src/example/sprig/sprig.yaml b/pkg/gotohelm/testdata/src/example/sprig/sprig.yaml new file mode 100644 index 0000000000..58b1cbf7f6 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/sprig/sprig.yaml @@ -0,0 +1,26 @@ +{{- /* Generated from "sprig.go" */ -}} + +{{- define "sprig.Sprig" -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" (dict "empty" (get (fromJson (include "sprig.empty" (dict "a" (list ) ))) "r") "default" (get (fromJson (include "sprig.default_" (dict "a" (list ) ))) "r") )) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "sprig.default_" -}} +{{- range $_ := (list 1) -}} +{{- $defaultStr := "DEFAULT" -}} +{{- $defaultInt := 1234 -}} +{{- $defaultStrSlice := (list $defaultStr) -}} +{{- (dict "r" (list (default "" $defaultStr) (default "value" $defaultStr) (default nil $defaultStrSlice) (default (list ) $defaultStrSlice) (default 0 $defaultInt) (default 1 $defaultInt))) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "sprig.empty" -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" (list (empty nil) (empty (list )) (empty (list "")) (empty (dict )) (empty (dict "key" nil )) (empty 1) (empty 0) (empty false) (empty true) (empty "") (empty "hello") (empty (mustMergeOverwrite (dict "Value" 0 ) (dict ))) (empty (mustMergeOverwrite (dict "Value" 0 ) (dict "Value" 0 ))) (empty (mustMergeOverwrite (dict "Value" 0 ) (dict "Value" 1 ))))) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + diff --git a/pkg/gotohelm/testdata/src/example/typing/_shims.tpl b/pkg/gotohelm/testdata/src/example/typing/_shims.tpl new file mode 100644 index 0000000000..130e24f6e1 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/typing/_shims.tpl @@ -0,0 +1,34 @@ +{{- /* Generated from "" */ -}} + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/typing/_shims.yaml b/pkg/gotohelm/testdata/src/example/typing/_shims.yaml new file mode 100644 index 0000000000..ec285f3fa7 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/typing/_shims.yaml @@ -0,0 +1,35 @@ +{{- /* Generated from "" */ -}} + + +{{- define "_shims.typetest" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- dict "r" (list $value (typeIs $type $value)) | toJson -}} +{{- end -}} + +{{- define "_shims.dicttest" -}} +{{- $dict := (index .a 0) -}} +{{- $key := (index .a 1) -}} +{{- if (hasKey $dict $key) -}} +{{- (dict "r" (list (index $dict $key) true)) | toJson -}} +{{- else -}} +{{- (dict "r" (list "" false)) | toJson -}} +{{- end -}} +{{- end -}} + +{{- define "_shims.typeassertion" -}} +{{- $type := (index .a 0) -}} +{{- $value := (index .a 1) -}} +{{- if (not (typeIs $type $value)) -}} +{{- (fail "TODO MAKE THIS A NICE MESSAGE") -}} +{{- end -}} +{{- (dict "r" $value) | toJson -}} +{{- end -}} + +{{- define "_shims.compact" -}} +{{- $out := (dict) -}} +{{- range $i, $e := (index .a 0) }} +{{- $_ := (set $out (printf "T%d" (add1 $i)) $e) -}} +{{- end -}} +{{- (dict "r" $out) | toJson -}} +{{- end -}} diff --git a/pkg/gotohelm/testdata/src/example/typing/asserts.go b/pkg/gotohelm/testdata/src/example/typing/asserts.go new file mode 100644 index 0000000000..a320c294a1 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/typing/asserts.go @@ -0,0 +1,41 @@ +package typing + +import ( + "github.com/redpanda-data/helm-charts/pkg/gotohelm/helmette" +) + +func typeTesting(dot *helmette.Dot) string { + t := dot.Values["t"] + + if _, ok := t.(string); ok { + return "it's a string!" + } else if _, ok := t.(int); ok { + return "it's an int!" + } else if _, ok := t.(float64); ok { + return "it's a float!" + } + + return "it's something else!" +} + +func typeAssertions(dot *helmette.Dot) string { + return "Not yet supported" + // _ = dot.Values["no-such-key"].(int) + // return "Didn't panic!" +} + +func typeSwitching(dot *helmette.Dot) string { + return "Not yet supported" + // switch dot.Values["t"].(type) { + // case int: + // return "it's an int!" + // case string: + // return "it's a string!" + // case float64: + // return "it's a float64!" + // case bool: + // return "it's a bool!" + // default: + // return "it's something else" + // } +} diff --git a/pkg/gotohelm/testdata/src/example/typing/asserts.yaml b/pkg/gotohelm/testdata/src/example/typing/asserts.yaml new file mode 100644 index 0000000000..a77e774a18 --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/typing/asserts.yaml @@ -0,0 +1,45 @@ +{{- /* Generated from "asserts.go" */ -}} + +{{- define "typing.typeTesting" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- $t := (index $dot.Values "t") -}} +{{- $tmp_tuple_1 := (get (fromJson (include "_shims.compact" (dict "a" (list (get (fromJson (include "_shims.typetest" (dict "a" (list "string" $t) ))) "r")) ))) "r") -}} +{{- $ok_1 := $tmp_tuple_1.T2 -}} +{{- $tmp_tuple_2 := (get (fromJson (include "_shims.compact" (dict "a" (list (get (fromJson (include "_shims.typetest" (dict "a" (list "int" $t) ))) "r")) ))) "r") -}} +{{- $ok_2 := $tmp_tuple_2.T2 -}} +{{- $tmp_tuple_3 := (get (fromJson (include "_shims.compact" (dict "a" (list (get (fromJson (include "_shims.typetest" (dict "a" (list "float64" $t) ))) "r")) ))) "r") -}} +{{- $ok_3 := $tmp_tuple_3.T2 -}} +{{- if $ok_1 -}} +{{- (dict "r" "it's a string!") | toJson -}} +{{- break -}} +{{- else -}}{{- if $ok_2 -}} +{{- (dict "r" "it's an int!") | toJson -}} +{{- break -}} +{{- else -}}{{- if $ok_3 -}} +{{- (dict "r" "it's a float!") | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} +{{- end -}} +{{- (dict "r" "it's something else!") | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "typing.typeAssertions" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" "Not yet supported") | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "typing.typeSwitching" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" "Not yet supported") | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + diff --git a/pkg/gotohelm/testdata/src/example/typing/structs.go b/pkg/gotohelm/testdata/src/example/typing/structs.go new file mode 100644 index 0000000000..8ab15d05ec --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/typing/structs.go @@ -0,0 +1,83 @@ +package typing + +import "github.com/redpanda-data/helm-charts/pkg/gotohelm/helmette" + +type Object struct { + Key string + WithTag int `json:"with_tag"` +} + +type WithEmbed struct { + Object + Exclude string `json:"-"` + Omit *string `json:"Omit,omitempty"` + Nilable *int +} + +type JSONKeys struct { + Value string `json:"val,omitempty"` + Children []*JSONKeys `json:"childs,omitempty"` +} + +func Typing(dot *helmette.Dot) map[string]any { + return map[string]any{ + "zeros": zeros(), + // "settingFields": settingFields(), + "compileMe": compileMe(), + "typeTesting": typeTesting(dot), + "typeAssertions": typeSwitching(dot), + "typeSwitching": typeSwitching(dot), + "nestedFieldAccess": nestedFieldAccess(), + } +} + +func zeros() []any { + var number *int + var str *string + var stru *Object + + return []any{ + Object{}, + WithEmbed{}, + number, + str, + stru, + } +} + +func nestedFieldAccess() string { + x := JSONKeys{ + Children: []*JSONKeys{ + { + Children: []*JSONKeys{ + {Value: "Hello!"}, + }, + }, + }, + } + + return x.Children[0].Children[0].Value +} + +// func settingFields() string { +// var out WithEmbed +// +// out.Object = Object{Key: "foo"} +// out.Object.Key = "bar" +// return out.Object.Key +// } + +func compileMe() Object { + return Object{ + Key: "foo", + } +} + +func alsoMe() WithEmbed { + return WithEmbed{ + Object: Object{ + Key: "Foo", + }, + Exclude: "Exclude", + } +} diff --git a/pkg/gotohelm/testdata/src/example/typing/structs.yaml b/pkg/gotohelm/testdata/src/example/typing/structs.yaml new file mode 100644 index 0000000000..b93722549b --- /dev/null +++ b/pkg/gotohelm/testdata/src/example/typing/structs.yaml @@ -0,0 +1,42 @@ +{{- /* Generated from "structs.go" */ -}} + +{{- define "typing.Typing" -}} +{{- $dot := (index .a 0) -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" (dict "zeros" (get (fromJson (include "typing.zeros" (dict "a" (list ) ))) "r") "compileMe" (get (fromJson (include "typing.compileMe" (dict "a" (list ) ))) "r") "typeTesting" (get (fromJson (include "typing.typeTesting" (dict "a" (list $dot) ))) "r") "typeAssertions" (get (fromJson (include "typing.typeSwitching" (dict "a" (list $dot) ))) "r") "typeSwitching" (get (fromJson (include "typing.typeSwitching" (dict "a" (list $dot) ))) "r") "nestedFieldAccess" (get (fromJson (include "typing.nestedFieldAccess" (dict "a" (list ) ))) "r") )) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "typing.zeros" -}} +{{- range $_ := (list 1) -}} +{{- $number := (fromJson "null") -}} +{{- $str := (fromJson "null") -}} +{{- $stru := (fromJson "null") -}} +{{- (dict "r" (list (mustMergeOverwrite (dict "Key" "" "with_tag" 0 ) (dict )) (mustMergeOverwrite (mustMergeOverwrite (dict "Key" "" "with_tag" 0 ) (dict "Nilable" (fromJson "null") )) (dict )) $number $str $stru)) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "typing.nestedFieldAccess" -}} +{{- range $_ := (list 1) -}} +{{- $x := (mustMergeOverwrite (dict ) (dict "childs" (list (mustMergeOverwrite (dict ) (dict "childs" (list (mustMergeOverwrite (dict ) (dict "val" "Hello!" ))) ))) )) -}} +{{- (dict "r" (index (index $x.childs 0).childs 0).val) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "typing.compileMe" -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" (mustMergeOverwrite (dict "Key" "" "with_tag" 0 ) (dict "Key" "foo" ))) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + +{{- define "typing.alsoMe" -}} +{{- range $_ := (list 1) -}} +{{- (dict "r" (mustMergeOverwrite (mustMergeOverwrite (dict "Key" "" "with_tag" 0 ) (dict "Nilable" (fromJson "null") )) (mustMergeOverwrite (dict "Key" "" "with_tag" 0 ) (dict "Key" "Foo" )) (dict ))) | toJson -}} +{{- break -}} +{{- end -}} +{{- end -}} + diff --git a/pkg/gotohelm/transpiler.go b/pkg/gotohelm/transpiler.go new file mode 100644 index 0000000000..e13846c01b --- /dev/null +++ b/pkg/gotohelm/transpiler.go @@ -0,0 +1,823 @@ +package gotohelm + +import ( + "bytes" + _ "embed" + "fmt" + "go/ast" + "go/format" + "go/token" + "go/types" + "path/filepath" + "regexp" + "strconv" + "strings" + + "golang.org/x/tools/go/packages" + "golang.org/x/tools/go/types/typeutil" +) + +var directiveRE = regexp.MustCompile(`\+gotohelm:([\w\.-]+)=([\w\.-]+)`) + +// TODO need to ensure dict test returns the correct zero value... +// TODO _shims.compact is a little bit hacky. It might malfunction if a slice +// is one of the return values it's called with. +// +//go:embed shims.yaml +var shimsYAML string + +type Unsupported struct { + Node ast.Node + Msg string + Fset *token.FileSet +} + +func (u *Unsupported) Error() string { + var b bytes.Buffer + fmt.Fprintf(&b, "unsupported ast.Node: %T\n", u.Node) + fmt.Fprintf(&b, "%s\n", u.Msg) + if err := format.Node(&b, u.Fset, u.Node); err != nil { + panic(err) // Oh the irony + } + return b.String() +} + +type Chart struct { + Files []*File +} + +func Transpile(pkg *packages.Package) (_ *Chart, err error) { + defer func() { + switch v := recover().(type) { + case nil: + case *Unsupported: + err = v + default: + panic(v) + } + }() + + // Ensure there are no errors in the package before we transpile it. + for _, err := range pkg.TypeErrors { + return nil, err + } + + for _, err := range pkg.Errors { + return nil, err + } + + t := &Transpiler{ + Package: pkg.Name, + Fset: pkg.Fset, + TypesInfo: pkg.TypesInfo, + Files: pkg.Syntax, + } + + return t.Transpile(), nil +} + +type Transpiler struct { + Package string + Fset *token.FileSet + Files []*ast.File + TypesInfo *types.Info +} + +func (t *Transpiler) Transpile() *Chart { + var chart Chart + for _, f := range t.Files { + path := t.Fset.File(f.Pos()).Name() + name := filepath.Base(path) + source := filepath.Base(path) + name = source[:len(source)-3] + ".yaml" + + isTestFile := strings.HasSuffix(name, "_test.go") + if isTestFile || name == "main.go" { + continue + } + + fileDirectives := parseDirectives(f.Doc.Text()) + if _, ok := fileDirectives["filename"]; ok { + name = fileDirectives["filename"] + } + + if _, ok := fileDirectives["ignore"]; ok { + continue + } + + var funcs []*Func + for _, d := range f.Decls { + fn, ok := d.(*ast.FuncDecl) + if !ok { + continue + } + + var params []Node + for _, param := range fn.Type.Params.List { + for _, name := range param.Names { + params = append(params, t.transpileExpr(name)) + } + } + + var statements []Node + for _, stmt := range fn.Body.List { + statements = append(statements, t.transpileStatement(stmt)) + } + + funcDirectives := parseDirectives(fn.Doc.Text()) + name := funcDirectives["name"] + if name == "" { + name = fn.Name.String() + } + + // TODO add a source field here? Ideally with a line number. + funcs = append(funcs, &Func{ + Name: name, + Namespace: t.Package, + Params: params, + Statements: statements, + }) + } + + chart.Files = append(chart.Files, &File{ + Name: name, + Source: source, + Funcs: funcs, + }) + } + // TODO Do this better + // Write out some basic shim functions to help us better match go's + // behavior. + chart.Files = append(chart.Files, &File{ + Source: "", + Name: "_shims.tpl", + Header: shimsYAML, + }) + return &chart +} + +func (t *Transpiler) transpileStatement(stmt ast.Stmt) Node { + switch stmt := stmt.(type) { + case nil: + return nil + + case *ast.DeclStmt: + switch d := stmt.Decl.(type) { + case *ast.GenDecl: + if len(d.Specs) > 1 { + // TODO could just return multiple statements. + panic(&Unsupported{ + Node: d, + Fset: t.Fset, + Msg: "declarations may only contain 1 spec", + }) + } + spec := d.Specs[0].(*ast.ValueSpec) + + if len(spec.Names) > 1 || len(spec.Values) > 1 { + panic(&Unsupported{ + Node: d, + Fset: t.Fset, + Msg: "specs may only contain 1 value", + }) + } + + rhs := t.zeroOf(t.TypesInfo.TypeOf(spec.Names[0])) + if len(spec.Values) > 0 { + rhs = t.transpileExpr(spec.Values[0]) + } + + return &Assignment{ + LHS: t.transpileExpr(spec.Names[0]), + New: true, + RHS: rhs, + } + + default: + panic(fmt.Sprintf("unsupported declaration: %#v", d)) + } + + case *ast.BranchStmt: + switch stmt.Tok { + case token.BREAK: + return &Statement{NoCapture: true, Expr: &Literal{Value: "break"}} + + case token.CONTINUE: + return &Statement{NoCapture: true, Expr: &Literal{Value: "continue"}} + } + + case *ast.ReturnStmt: + if len(stmt.Results) != 1 { + panic(&Unsupported{ + Node: stmt, + Fset: t.Fset, + Msg: "returns must return exactly 1 value", + }) + } + + return &Return{ + Expr: t.transpileExpr(stmt.Results[0]), + } + + case *ast.AssignStmt: + if len(stmt.Lhs) != 1 || len(stmt.Rhs) != 1 { + break + } + + // +=, /=, *=, etc show up as assignments. They're not supported in + // templates. We'll need to either rewrite the expression here or add + // another AST rewrite. + switch stmt.Tok { + case token.ASSIGN, token.DEFINE: + default: + panic(&Unsupported{ + Node: stmt, + Fset: t.Fset, + Msg: "Unsupported assignment token", + }) + } + + // TODO could simplify this by performing a type switch on the + // transpiled result of lhs. + if _, ok := stmt.Lhs[0].(*ast.SelectorExpr); ok { + selector := t.transpileExpr(stmt.Lhs[0]).(*Selector) + + return &Statement{ + Expr: &BuiltInCall{ + FuncName: "set", + Arguments: []Node{ + selector.Expr, + &Literal{Value: strconv.Quote(selector.Field)}, + t.transpileExpr(stmt.Rhs[0]), + }, + }, + } + } + + // TODO could simplify this by implementing an IndexExpr node and then + // performing a type switch on the transpiled result of lhs. + if idx, ok := stmt.Lhs[0].(*ast.IndexExpr); ok { + return &Statement{ + Expr: &BuiltInCall{ + FuncName: "set", + Arguments: []Node{ + t.transpileExpr(idx.X), + t.transpileExpr(idx.Index), + t.transpileExpr(stmt.Rhs[0]), + }, + }, + } + } + + rhs := t.transpileExpr(stmt.Rhs[0]) + lhs := t.transpileExpr(stmt.Lhs[0]) + + return &Assignment{RHS: rhs, LHS: lhs, New: stmt.Tok.String() == ":="} + + case *ast.RangeStmt: + return &Range{ + Key: t.transpileExpr(stmt.Key), + Value: t.transpileExpr(stmt.Value), + Over: t.transpileExpr(stmt.X), + Body: t.transpileStatement(stmt.Body), + } + + case *ast.ExprStmt: + return &Statement{ + Expr: t.transpileExpr(stmt.X), + } + + case *ast.BlockStmt: + var out []Node + for _, s := range stmt.List { + out = append(out, t.transpileStatement(s)) + } + return &Block{Statements: out} + + case *ast.IfStmt: + return &IfStmt{ + Init: t.transpileStatement(stmt.Init), + Cond: t.transpileExpr(stmt.Cond), + Body: t.transpileStatement(stmt.Body), + Else: t.transpileStatement(stmt.Else), + } + } + + panic(&Unsupported{ + Node: stmt, + Fset: t.Fset, + Msg: "unhandled ast.Stmt", + }) +} + +func (t *Transpiler) transpileExpr(n ast.Expr) Node { + switch n := n.(type) { + case nil: + return nil + + case *ast.BasicLit: + return &Literal{Value: n.Value} + + case *ast.StarExpr: + // TODO this should be wrapped in something like "Assert not nil" + return t.transpileExpr(n.X) + + case *ast.CompositeLit: + typ := t.typeOf(n) + + // TODO: Need to handle implementors of json.Marshaler. + // TODO: Need to filter out zero value fields that are explicitly + // provided. + + if p, ok := typ.(*types.Pointer); ok { + typ = p.Elem() + } + + switch typ := typ.Underlying().(type) { + case *types.Slice: + var elts []Node + for _, el := range n.Elts { + elts = append(elts, t.transpileExpr(el)) + } + return &BuiltInCall{ + FuncName: "list", + Arguments: elts, + } + + case *types.Map: + if !types.AssignableTo(typ.Key(), types.Typ[types.String]) { + panic(fmt.Sprintf("map keys must be string. Got %#v", typ.Key())) + } + + var d DictLiteral + for _, el := range n.Elts { + d.KeysValues = append(d.KeysValues, &KeyValue{ + Key: el.(*ast.KeyValueExpr).Key.(*ast.BasicLit).Value, + Value: t.transpileExpr(el.(*ast.KeyValueExpr).Value), + }) + } + return &d + + case *types.Struct: + zero := t.zeroOf(typ) + fields := getFields(typ) + fieldByName := map[string]*structField{} + for _, f := range fields { + f := f + fieldByName[f.Field.Name()] = &f + } + + var embedded []Node + var d DictLiteral + for _, el := range n.Elts { + key := el.(*ast.KeyValueExpr).Key.(*ast.Ident).Name + value := el.(*ast.KeyValueExpr).Value + + field := fieldByName[key] + if field.Omit { + continue + } + + if field.Embedded { + embedded = append(embedded, t.transpileExpr(value)) + continue + } + + d.KeysValues = append(d.KeysValues, &KeyValue{ + Key: strconv.Quote(field.JSONName), + Value: t.transpileExpr(value), + }) + } + + args := []Node{zero} + args = append(args, embedded...) + args = append(args, &d) + + return &BuiltInCall{ + FuncName: "mustMergeOverwrite", + Arguments: args, + } + + default: + panic(fmt.Sprintf("unsupported composite literal %#v", typ)) + } + + case *ast.CallExpr: + return t.transpileCallExpr(n) + + case *ast.Ident: + // Unclear how often this check is correct. true, false, and _ won't + // have an Obj. AST rewriting can also result in .Obj being nil. + if n.Obj == nil { + if n.Name == "_" { + return &Ident{Name: n.Name} + } + return &Literal{Value: n.Name} + } + + return &Ident{Name: n.Name} + + case *ast.SelectorExpr: + if s, ok := unwrapStruct(t.typeOf(n.X)); ok { + for _, f := range getFields(s) { + if f.Field.Name() == n.Sel.Name { + return &Selector{ + Expr: t.transpileExpr(n.X), + Field: f.JSONName, + } + } + } + } + + // TODO when would this ever get hit? + return &Selector{ + Expr: t.transpileExpr(n.X), + Field: n.Sel.Name, + } + + case *ast.BinaryExpr: + untyped := [3]string{"_", n.Op.String(), "_"} + typed := [3]string{t.typeOf(n.X).String(), n.Op.String(), t.typeOf(n.Y).String()} + + // Poor man's pattern matching :[ + mapping := map[[3]string]string{ + {"_", token.EQL.String(), "_"}: "eq", + {"_", token.NEQ.String(), "_"}: "ne", + {"_", token.LAND.String(), "_"}: "and", + {"_", token.LOR.String(), "_"}: "or", + {"_", token.GTR.String(), "_"}: "gt", + {"_", token.LSS.String(), "_"}: "lt", + {"_", token.GEQ.String(), "_"}: "gte", + {"_", token.LEQ.String(), "_"}: "lte", + {"int", token.ADD.String(), "int"}: "add", + {"int", token.SUB.String(), "int"}: "sub", + {"int", token.MUL.String(), "int"}: "mul", + {"int", token.QUO.String(), "int"}: "div", + {"float32", token.QUO.String(), "float32"}: "divf", + {"float64", token.QUO.String(), "float64"}: "divf", + } + + // Typed versions take precedence. + if funcName, ok := mapping[typed]; ok { + return &BuiltInCall{ + FuncName: funcName, + Arguments: []Node{t.transpileExpr(n.X), t.transpileExpr(n.Y)}, + } + } + + // Fallback to "wild cards" (_). + if funcName, ok := mapping[untyped]; ok { + return &BuiltInCall{ + FuncName: funcName, + Arguments: []Node{t.transpileExpr(n.X), t.transpileExpr(n.Y)}, + } + } + + // TODO re-add suport for rewriting str + str into printf "%s%s". For + // now its easier to just require writers to use printf + // No support for easy string concatenation in helm/sprig/templates soooo. Printf. + // if t.isString(n.Y) && t.isString(n.X) { + // return &BuiltInCall{ + // FuncName: "printf", + // Arguments: []Node{ + // &Literal{Value: `"%s%s"`}, + // t.transpileExpr(n.X), + // t.transpileExpr(n.Y), + // }, + // } + // } + + case *ast.UnaryExpr: + switch n.Op { + case token.NOT: + return &BuiltInCall{ + FuncName: "not", + Arguments: []Node{t.transpileExpr(n.X)}, + } + case token.AND: + // Can't take addresses in templates so just return the variable. + return t.transpileExpr(n.X) + } + + case *ast.IndexExpr: + return &BuiltInCall{ + FuncName: "index", + Arguments: []Node{ + t.transpileExpr(n.X), + t.transpileExpr(n.Index), + }, + } + + case *ast.TypeAssertExpr: + // return &BuiltInCall{ + // FuncName: "_shims.typeassertion", + // Arguments: []Node{ + // t.transpileExpr(n.Type), + // t.transpileExpr(n.X), + // }, + // } + + // TODO figure out how to support type switches. For now, hope for the + // best and expect something to break if the type happens to be + // incorrect. + // Could potentially inject some "bootstrap" functions that would make this easier. + // IE + return t.transpileExpr(n.X) + } + + var b bytes.Buffer + if err := format.Node(&b, t.Fset, n); err != nil { + panic(err) + } + panic(fmt.Sprintf("unhandled Expr %T\n%s", n, b.String())) +} + +func (t *Transpiler) transpileCallExpr(n *ast.CallExpr) Node { + var args []Node + for _, arg := range n.Args { + args = append(args, t.transpileExpr(arg)) + } + + callee := typeutil.Callee(t.TypesInfo, n) + + switch { + // go builtins + case callee == nil, callee.Pkg() == nil: + switch n.Fun.(*ast.Ident).Name { + case "append": + // TODO: mustAppend isn't variadic. Need to either nest mustAppend calls or use concat. + if len(args) > 2 { + panic(&Unsupported{ + Node: n, + Fset: t.Fset, + Msg: "appending multiple values at once is not currently supported", + }) + } + return &BuiltInCall{FuncName: "mustAppend", Arguments: args} + case "int", "int32": + return &BuiltInCall{FuncName: "int", Arguments: args} + case "panic": + return &BuiltInCall{FuncName: "fail", Arguments: args} + case "string": + return &BuiltInCall{FuncName: "toString", Arguments: args} + default: + panic(fmt.Sprintf("unsupport golang builtin %q", n.Fun.(*ast.Ident).Name)) + } + + // Method call. + case callee.Type().(*types.Signature).Recv() != nil: + if len(args) != 0 { + panic(&Unsupported{Fset: t.Fset, Node: n, Msg: "method calls with arguments are not implemented"}) + } + // Method calls come in as a "top level" CallExpr where .Fun is the + // selector up to that call. IE all of `Foo.Bar.Baz()` will be "within" + // the CallExpr. CallExpr.Fun will contain Foo.Bar.Baz. In the case of + // zero argument methods, text/template will automatically call them. + return t.transpileExpr(n.Fun) + + // Call to function within the same package. A-Okay. It's + // transpiled. + case callee.Pkg().Name() == t.Package: + return &Call{FuncName: fmt.Sprintf("%s.%s", t.Package, callee.Name()), Arguments: args} + } + + // Mapping of go functions to sprig/helm/template functions where arguments + // are also the same. + funcMapping := map[string]string{ + "fmt.Sprintf": "printf", + "helmette.Default": "default", + "helmette.Empty": "empty", + "helmette.FromJSON": "fromJson", + "helmette.Keys": "keys", + "helmette.KindIs": "kindIs", + "helmette.KindOf": "kindOf", + "helmette.Merge": "merge", + "helmette.MustFromJSON": "mustFromJson", + "helmette.MustToJSON": "mustToJson", + "helmette.RegexMatch": "regexMatch", + "helmette.ToJSON": "toJson", + "helmette.Tpl": "tpl", + "helmette.Trunc": "trunc", + "maps.Keys": "keys", + "math.Floor": "floor", + } + + // Call to any other function. + // This check's a bit... buggy + name := callee.Pkg().Name() + "." + callee.Name() + + if tplFuncName, ok := funcMapping[name]; ok { + return &BuiltInCall{FuncName: tplFuncName, Arguments: args} + } + + // Mappings that are not 1:1 and require some argument fiddling to make + // them match up as expected. + switch name { + case "slices.Sort": + // TODO: This only works for strings :[ + return &BuiltInCall{FuncName: "sortAlpha", Arguments: args} + case "strings.TrimSuffix": + return &BuiltInCall{FuncName: "trimSuffix", Arguments: []Node{args[1], args[0]}} + case "strings.ReplaceAll": + return &BuiltInCall{FuncName: "replace", Arguments: []Node{args[1], args[2], args[0]}} + case "intstr.FromInt32": + return args[0] + case "helmette.MustDuration": + return args[0] + case "helmette.Dig": + return &BuiltInCall{FuncName: "dig", Arguments: append(args[2:], args[1], args[0])} + case "helmette.Unwrap": + return &Selector{Expr: args[0], Field: "AsMap"} + case "helmette.Compact2": + return &Call{FuncName: "_shims.compact", Arguments: args} + case "helmette.DictTest": + // TODO need to figure out how to get the generic argument here. + // TODO revalidate arguments + // TODO add in zerof + return &Call{FuncName: "_shims.dicttest", Arguments: args} + case "helmette.TypeTest": + // TODO there's got to be a better way to get the type params.... + args = append([]Node{ + &Literal{ + Value: fmt.Sprintf("%q", n.Fun.(*ast.IndexExpr).Index.(*ast.Ident).Name), + }, + }, args...) + return &Call{FuncName: "_shims.typetest", Arguments: args} + case "helmette.TypeAssertion": + // TODO need to figure out how to get the generic argument here. + // TODO revalidate arguments + // TODO there's got to be a better way to get the type params.... + args = append([]Node{ + &Literal{ + Value: fmt.Sprintf("%q", n.Fun.(*ast.IndexExpr).Index.(*ast.Ident).Name), + }, + }, args...) + return &Call{FuncName: "_shims.typeassertion", Arguments: args} + default: + panic(fmt.Sprintf("unsupported function %s", name)) + } +} + +func (t *Transpiler) isString(e ast.Expr) bool { + return types.AssignableTo(t.TypesInfo.TypeOf(e), types.Typ[types.String]) +} + +func (t *Transpiler) isBasic(e ast.Expr, typ types.BasicKind) bool { + if b, ok := t.typeOf(e).(*types.Basic); ok && b.Kind() == typ { + return true + } + return false +} + +func (t *Transpiler) typeOf(expr ast.Expr) types.Type { + return t.TypesInfo.TypeOf(expr) +} + +func (t *Transpiler) zeroOf(typ types.Type) Node { + // TODO need to detect and reject or special case implementors of + // json.Marshaler. Getting a handle to a that interface is... difficult. + + // Special cases. + switch typ.String() { + case "k8s.io/apimachinery/pkg/apis/meta/v1.Time": + return &Nil{} + } + + switch typ := typ.Underlying().(type) { + case *types.Basic: + switch typ.Info() { + case types.IsString: + return &Literal{Value: `""`} + case types.IsInteger, types.IsUnsigned | types.IsInteger: + return &Literal{Value: "0"} + case types.IsBoolean: + return &Literal{Value: "false"} + default: + panic(fmt.Sprintf("unsupported Basic type: %#v", typ)) + } + + case *types.Pointer, *types.Map, *types.Interface, *types.Slice: + return &Nil{} + + case *types.Struct: + var embedded []Node + var out DictLiteral + + // Skip fields that json Marshalling would itself skip. + for _, field := range getFields(typ) { + if field.Omit || (field.OmitEmpty && omitemptyRespected(field.Field.Type())) { + continue + } + + if field.Embedded { + embedded = append(embedded, t.zeroOf(field.Field.Type())) + continue + } + + out.KeysValues = append(out.KeysValues, &KeyValue{ + Key: strconv.Quote(field.JSONName), + Value: t.zeroOf(field.Field.Type()), + }) + } + if len(embedded) < 1 { + return &out + } + return &BuiltInCall{ + FuncName: "mustMergeOverwrite", + Arguments: append(embedded, &out), + } + + default: + panic(fmt.Sprintf("unsupported type: %#v", typ)) + } +} + +func unwrapStruct(typ types.Type) (*types.Struct, bool) { + if p, ok := typ.Underlying().(*types.Pointer); ok { + typ = p.Elem() + } + + if s, ok := typ.Underlying().(*types.Struct); ok { + return s, true + } + + return nil, false +} + +func omitemptyRespected(typ types.Type) bool { + switch typ.(type) { + case *types.Basic, *types.Pointer, *types.Slice, *types.Map: + return true + case *types.Named: + return omitemptyRespected(typ.Underlying()) + default: + return false + } +} + +type jsonTag struct { + Name string + Inline bool + OmitEmpty bool +} + +func parseTag(tag string) jsonTag { + match := regexp.MustCompile(`json:"([^"]+)"`).FindStringSubmatch(tag) + if match == nil { + return jsonTag{} + } + + idx := strings.Index(match[1], ",") + if idx == -1 { + idx = len(match[1]) + } + + return jsonTag{ + Name: match[1][:idx], + Inline: strings.Contains(match[1], "inline"), + OmitEmpty: strings.Contains(match[1], "omitempty"), + } +} + +type structField struct { + Field *types.Var + Embedded bool + Omit bool + OmitEmpty bool + JSONName string +} + +func getFields(s *types.Struct) []structField { + var fields []structField + for i := 0; i < s.NumFields(); i++ { + field := s.Field(i) + tag := parseTag(s.Tag(i)) + + omit := tag.Name == "-" || !field.Exported() + embedded := tag.Name == "" && field.Embedded() + + if tag.Name == "" && tag.Name != "-" { + tag.Name = field.Name() + } + + fields = append(fields, structField{ + Field: field, + Omit: omit, + OmitEmpty: tag.OmitEmpty, + JSONName: tag.Name, + Embedded: embedded, + }) + + } + return fields +} + +func parseDirectives(in string) map[string]string { + match := directiveRE.FindAllStringSubmatch(in, -1) + + out := map[string]string{} + for _, m := range match { + out[m[1]] = m[2] + } + return out +} diff --git a/pkg/gotohelm/transpiler_test.go b/pkg/gotohelm/transpiler_test.go new file mode 100644 index 0000000000..309c486561 --- /dev/null +++ b/pkg/gotohelm/transpiler_test.go @@ -0,0 +1,285 @@ +package gotohelm + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "text/template" + "unicode" + + "github.com/Masterminds/sprig/v3" + "github.com/redpanda-data/helm-charts/pkg/gotohelm/helmette" + "github.com/redpanda-data/helm-charts/pkg/testutil" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + "golang.org/x/tools/go/packages" +) + +type TestSpec struct { + Unsupported bool + Values []map[string]any +} + +var testSpecs = map[string]TestSpec{ + "a": {}, + "b": {}, + "k8s": {}, + "syntax": {}, + "sprig": {}, + "directives": {}, + "mutability": {}, + "inputs": { + Values: []map[string]any{ + {"foo": 1, "bar": "baz", "nested": map[string]any{"quux": true}}, + {"foo": []any{}, "bar": "baz", "nested": map[string]any{"quux": "hello"}}, + {"foo": []any{}, "bar": "baz", "nested": map[string]any{"quux": 1}}, + {"foo": []any{}, "bar": "baz", "nested": map[string]any{"quux": []string{"1", "2"}}}, + }, + }, + "flowcontrol": { + Values: []map[string]any{ + {"ints": []int{}, "boolean": true, "oneToFour": 1}, + {"ints": []int{}, "boolean": false, "oneToFour": 2}, + {"ints": []int{1, 2, 3}, "boolean": false, "oneToFour": 3}, + {"ints": []int{1, 2, 3}, "boolean": false, "oneToFour": 4}, + }, + }, + "typing": { + Values: []map[string]any{ + {"t": int(1)}, + {"t": float64(1)}, + {"t": true}, + {"t": "a string"}, + }, + }, +} + +func TestTranspile(t *testing.T) { + td, err := filepath.Abs("testdata") + require.NoError(t, err) + + pkgs, err := LoadPackages(&packages.Config{ + Dir: td + "/src/example", + Tests: true, + Env: append( + os.Environ(), + "GOPATH="+td, + "GO111MODULE=on", + ), + }, "./...") + require.NoError(t, err) + + // Ensure there are no compile errors before proceeding. + for _, pkg := range pkgs { + for _, err := range pkg.Errors { + require.NoErrorf(t, err, "failed to compile %q", pkg.Name) + } + } + + ctx := testutil.Context(t) + runner := NewGoRunner(td) + + go func() { + require.NoError(t, runner.Run(ctx)) + }() + + for _, pkg := range pkgs { + pkg := pkg + t.Run(pkg.Name, func(t *testing.T) { + spec, ok := testSpecs[pkg.Name] + if !ok { + t.Skipf("no test spec for %q", pkg.Name) + } + + if spec.Unsupported { + t.Skipf("%q is not currently supported", pkg.Name) + } + + chart, err := Transpile(pkg) + require.NoError(t, err) + + for _, f := range chart.Files { + var actual bytes.Buffer + f.Write(&actual) + + output := filepath.Join(td, "src", "example", pkg.Name, f.Name) + testutil.AssertGolden(t, testutil.Text, output, actual.Bytes()) + } + + // Ensure syntactic validity of generated values. + var tpl *template.Template + funcs := sprig.FuncMap() + funcs["include"] = func(template string, args ...any) (string, error) { + if len(args) > 1 { + return "", fmt.Errorf("include accepts either 0 or 1 arguments. got: %d", len(args)) + } + + args = append(args, nil) + + var b bytes.Buffer + if err := tpl.ExecuteTemplate(&b, template, args[0]); err != nil { + return "", err + } + t.Logf("%q(%#v) -> %s", template, args[0], b.String()) + return b.String(), nil + } + tpl, err = template.New(pkg.Name).Funcs(funcs).ParseGlob(filepath.Join(td, "src", "example", pkg.Name, "*.yaml")) + require.NoError(t, err) + + // If .Values isn't explicitly specified, default to an empty object. + if spec.Values == nil { + spec.Values = append(spec.Values, map[string]any{}) + } + + for i, values := range spec.Values { + values := values + + t.Run(fmt.Sprintf("Values%d", i), func(t *testing.T) { + t.Logf("using values: %#v", values) + + dot := helmette.Dot{ + Values: values, + Chart: helmette.Chart{ + Name: pkg.Name, + Version: "1.2.3", + }, + Release: helmette.Release{ + Name: "release-name", + Namespace: "release-namespace", + }, + } + + // MUST round trip values through JSON marshalling to + // ensure that types between go/helm/templates are the same. + // Numbers should always be integers :[ (TODO: Can Yaml + // technically encode the difference between ints and + // floats?) + dotJSON, err := json.Marshal(dot) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(dotJSON, &dot)) + + actualJSON := map[string]any{} + for _, tpl := range tpl.Templates() { + spl := strings.Split(tpl.Name(), ".") + if len(spl) != 2 || !unicode.IsUpper(rune(spl[1][0])) { + continue + } + + var b bytes.Buffer + require.NoError(t, tpl.Execute(&b, map[string]any{"a": []any{dot}})) + + var x map[string]any + require.NoError(t, json.Unmarshal(b.Bytes(), &x)) + actualJSON[spl[1]] = x["r"] // HACK + } + + gocodeJSON, err := runner.Render(ctx, &dot) + require.NoError(t, err) + + goPretty, err := json.MarshalIndent(gocodeJSON, "", "\t") + require.NoError(t, err) + + tplPretty, err := json.MarshalIndent(actualJSON, "", "\t") + require.NoError(t, err) + + t.Logf("go code output:\n%s", goPretty) + t.Logf("template output:\n%s", tplPretty) + + require.Equal(t, gocodeJSON, actualJSON, "Divergence between Go code and generated template") + }) + } + }) + } +} + +type GoRunner struct { + inputCh chan *helmette.Dot + outputCh chan map[string]any + cmd *exec.Cmd +} + +func NewGoRunner(root string) *GoRunner { + cmd := exec.Command("go", "run", "main.go") + cmd.Dir = filepath.Join(root, "src", "example") + cmd.Env = append( + os.Environ(), + "GOPATH="+root, + "GO111MODULE=on", + ) + + return &GoRunner{ + cmd: cmd, + inputCh: make(chan *helmette.Dot), + outputCh: make(chan map[string]any), + } +} + +func (g *GoRunner) Render(ctx context.Context, dot *helmette.Dot) (map[string]any, error) { + select { + case g.inputCh <- dot: + case <-ctx.Done(): + return nil, ctx.Err() + } + + select { + case res := <-g.outputCh: + var err error + if e, ok := res["err"]; ok && e != nil { + return nil, fmt.Errorf("%#v", e) + } + return res["result"].(map[string]any), err + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (g *GoRunner) Run(ctx context.Context) error { + defer close(g.inputCh) + defer close(g.outputCh) + + stdin, err := g.cmd.StdinPipe() + if err != nil { + return err + } + + stdout, err := g.cmd.StdoutPipe() + if err != nil { + return err + } + + if err := g.cmd.Start(); err != nil { + return err + } + + enc := json.NewEncoder(stdin) + dec := json.NewDecoder(stdout) + for { + var in *helmette.Dot + + select { + case in = <-g.inputCh: + case <-ctx.Done(): + return ctx.Err() + } + + if err := enc.Encode(in); err != nil { + return err + } + + var out map[string]any + if err := dec.Decode(&out); err != nil { + return err + } + + select { + case g.outputCh <- out: + case <-ctx.Done(): + return ctx.Err() + } + } +}