diff --git a/go.mod b/go.mod index 4f2f1f9..61aba63 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ replace github.com/miracl/conflate v1.2.1 => github.com/runz0rd/conflate v1.2.2- require ( github.com/1Password/connect-sdk-go v1.5.3 + github.com/BurntSushi/toml v1.4.0 + github.com/Masterminds/sprig/v3 v3.3.0 github.com/alecthomas/chroma v0.10.0 github.com/miracl/conflate v1.2.1 github.com/pkg/errors v0.9.1 @@ -18,13 +20,16 @@ require ( gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.31.1 + sigs.k8s.io/yaml v1.4.0 ) require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect - github.com/BurntSushi/toml v1.4.0 // indirect + dario.cat/mergo v1.0.1 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/containerd/console v1.0.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.4.0 // indirect @@ -33,16 +38,22 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gookit/color v1.5.4 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.4 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect github.com/uber/jaeger-lib v2.4.1+incompatible // indirect @@ -52,10 +63,11 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/atomic v1.10.0 // indirect + golang.org/x/crypto v0.26.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/apimachinery v0.31.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/go.sum b/go.sum index 5f0c0fc..e35859b 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/1Password/connect-sdk-go v1.5.3 h1:KyjJ+kCKj6BwB2Y8tPM1Ixg5uIS6HsB0uWA8U38p/Uk= github.com/1Password/connect-sdk-go v1.5.3/go.mod h1:5rSymY4oIYtS4G3t0oMkGAXBeoYiukV3vkqlnEjIDJs= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -22,6 +24,12 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/ github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +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.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= @@ -34,6 +42,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= @@ -54,6 +64,8 @@ github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQ github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -77,6 +89,10 @@ github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 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= @@ -109,8 +125,12 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -149,6 +169,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk 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.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -185,22 +207,22 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.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-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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= diff --git a/internal/config/config.go b/internal/config/config.go index 020211a..83d5c62 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,6 +11,8 @@ type Config struct { // Version of the schema Version string `yaml:"version,omitempty"` // Global values to be injected into all squadron values + Vars map[string]any `yaml:"vars,omitempty"` + // Global values to be injected into all squadron values Global map[string]any `yaml:"global,omitempty"` // Global builds that can be referenced as dependencies Builds map[string]Build `yaml:"builds,omitempty"` diff --git a/internal/config/unit.go b/internal/config/unit.go index 311d706..1a4816c 100644 --- a/internal/config/unit.go +++ b/internal/config/unit.go @@ -26,7 +26,7 @@ type Unit struct { // ~ Public methods // ------------------------------------------------------------------------------------------------ -func (u *Unit) ValuesYAML(global map[string]any) ([]byte, error) { +func (u *Unit) ValuesYAML(global, vars map[string]any) ([]byte, error) { values := u.Values if values == nil { values = map[string]any{} @@ -36,6 +36,11 @@ func (u *Unit) ValuesYAML(global map[string]any) ([]byte, error) { values["global"] = global } } + if vars != nil { + if _, ok := values["vars"]; !ok { + values["vars"] = vars + } + } return yamlv2.Marshal(values) } @@ -69,9 +74,9 @@ func (u *Unit) Push(ctx context.Context, squadron, unit string, args []string) ( return "", nil } -func (u *Unit) Template(ctx context.Context, name, squadron, unit, namespace string, global map[string]any, helmArgs []string) ([]byte, error) { +func (u *Unit) Template(ctx context.Context, name, squadron, unit, namespace string, global, vars map[string]any, helmArgs []string) ([]byte, error) { var ret bytes.Buffer - valueBytes, err := u.ValuesYAML(global) + valueBytes, err := u.ValuesYAML(global, vars) if err != nil { return nil, err } diff --git a/internal/template/default.go b/internal/template/default.go index 4046cb2..59eda5c 100644 --- a/internal/template/default.go +++ b/internal/template/default.go @@ -1,12 +1,5 @@ package template -func defaultValue(value string, def any) any { - if value == "" { - return def - } - return value -} - func defaultIndexValue(v map[string]any, index string, def any) any { var ok bool if _, ok = v[index]; ok { diff --git a/internal/template/file.go b/internal/template/file.go index 2757554..479f23a 100644 --- a/internal/template/file.go +++ b/internal/template/file.go @@ -3,9 +3,14 @@ package template import ( "bytes" "context" + "encoding/json" "os" + "strings" + "github.com/BurntSushi/toml" "github.com/pkg/errors" + "sigs.k8s.io/yaml" + goyaml "sigs.k8s.io/yaml/goyaml.v3" ) func file(ctx context.Context, templateVars any, errorOnMissing bool) func(v string) (string, error) { @@ -27,3 +32,131 @@ func file(ctx context.Context, templateVars any, errorOnMissing bool) func(v str return string(bytes.TrimSpace(renderedBytes)), nil } } + +// toYAML takes an interface, marshals it to yaml, and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toYAML(v any) string { + data, err := yaml.Marshal(v) + if err != nil { + // Swallow errors inside of a template. + return "" + } + return strings.TrimSuffix(string(data), "\n") +} + +func toYAMLPretty(v any) string { + var data bytes.Buffer + encoder := goyaml.NewEncoder(&data) + encoder.SetIndent(2) + err := encoder.Encode(v) + + if err != nil { + // Swallow errors inside of a template. + return "" + } + return strings.TrimSuffix(data.String(), "\n") +} + +// fromYAML converts a YAML document into a map[string]any. +// +// This is not a general-purpose YAML parser, and will not parse all valid +// YAML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string into +// m["Error"] in the returned map. +func fromYAML(str string) map[string]any { + m := map[string]any{} + + if err := yaml.Unmarshal([]byte(str), &m); err != nil { + m["Error"] = err.Error() + } + return m +} + +// fromYAMLArray converts a YAML array into a []any. +// +// This is not a general-purpose YAML parser, and will not parse all valid +// YAML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string as +// the first and only item in the returned array. +func fromYAMLArray(str string) []any { + a := []any{} + + if err := yaml.Unmarshal([]byte(str), &a); err != nil { + a = []any{err.Error()} + } + return a +} + +// toTOML takes an interface, marshals it to toml, and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toTOML(v any) string { + b := bytes.NewBuffer(nil) + e := toml.NewEncoder(b) + err := e.Encode(v) + if err != nil { + return err.Error() + } + return b.String() +} + +// fromTOML converts a TOML document into a map[string]any. +// +// This is not a general-purpose TOML parser, and will not parse all valid +// TOML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string into +// m["Error"] in the returned map. +func fromTOML(str string) map[string]any { + m := make(map[string]any) + + if err := toml.Unmarshal([]byte(str), &m); err != nil { + m["Error"] = err.Error() + } + return m +} + +// toJSON takes an interface, marshals it to json, and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toJSON(v any) string { + data, err := json.Marshal(v) + if err != nil { + // Swallow errors inside of a template. + return "" + } + return string(data) +} + +// fromJSON converts a JSON document into a map[string]any. +// +// This is not a general-purpose JSON parser, and will not parse all valid +// JSON documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string into +// m["Error"] in the returned map. +func fromJSON(str string) map[string]any { + m := make(map[string]any) + + if err := json.Unmarshal([]byte(str), &m); err != nil { + m["Error"] = err.Error() + } + return m +} + +// fromJSONArray converts a JSON array into a []any. +// +// This is not a general-purpose JSON parser, and will not parse all valid +// JSON documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string as +// the first and only item in the returned array. +func fromJSONArray(str string) []any { + a := []any{} + + if err := json.Unmarshal([]byte(str), &a); err != nil { + a = []any{err.Error()} + } + return a +} diff --git a/internal/template/format.go b/internal/template/format.go index 804ad80..be09382 100644 --- a/internal/template/format.go +++ b/internal/template/format.go @@ -8,11 +8,3 @@ func indent(spaces int, v string) string { pad := strings.Repeat(" ", spaces) return strings.ReplaceAll(v, "\n", "\n"+pad) } - -func quote(v string) string { - return "'" + v + "'" -} - -func replace(old, new, v string) string { - return strings.ReplaceAll(v, old, new) -} diff --git a/internal/template/template.go b/internal/template/template.go index edf2b10..dc2c1f8 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -4,24 +4,38 @@ import ( "bytes" "context" "text/template" + + "github.com/Masterminds/sprig/v3" ) func ExecuteFileTemplate(ctx context.Context, text string, templateVars any, errorOnMissing bool) ([]byte, error) { - funcMap := template.FuncMap{ - "env": env, - "envDefault": envDefault, - "op": onePassword(ctx, templateVars, errorOnMissing), - "opDoc": onePasswordDocument(ctx, templateVars, errorOnMissing), - "base64": base64, - "b64enc": base64, - "default": defaultValue, - "defaultIndex": defaultIndexValue, - "indent": indent, - "replace": replace, - "file": file(ctx, templateVars, errorOnMissing), - "git": git(ctx), - "quote": quote, - } + funcMap := sprig.TxtFuncMap() + delete(funcMap, "env") + delete(funcMap, "expandenv") + + funcMap["env"] = env + funcMap["envDefault"] = envDefault + + // deprecated + funcMap["indent"] = indent + funcMap["base64"] = base64 + funcMap["defaultIndex"] = defaultIndexValue + + funcMap["op"] = onePassword(ctx, templateVars, errorOnMissing) + funcMap["git"] = git(ctx) + funcMap["opDoc"] = onePasswordDocument(ctx, templateVars, errorOnMissing) + funcMap["file"] = file(ctx, templateVars, errorOnMissing) + + funcMap["toToml"] = toTOML + funcMap["fromToml"] = fromTOML + funcMap["toYaml"] = toYAML + funcMap["toYamlPretty"] = toYAMLPretty + funcMap["fromYaml"] = fromYAML + funcMap["fromYamlArray"] = fromYAMLArray + funcMap["toJson"] = toJSON + funcMap["fromJson"] = fromJSON + funcMap["fromJsonArray"] = fromJSONArray + tpl, err := template.New("squadron").Delims("<% ", " %>").Funcs(funcMap).Parse(text) if err != nil { return nil, err diff --git a/squadron.go b/squadron.go index a0bc834..4fbcdac 100644 --- a/squadron.go +++ b/squadron.go @@ -157,6 +157,10 @@ func (sq *Squadron) RenderConfig(ctx context.Context) error { util.ToSnakeCaseKeys(value) tv.Add("Global", value) } + if value, ok := vars["vars"]; ok { + util.ToSnakeCaseKeys(value) + tv.Add("Vars", value) + } if value, ok := vars["squadron"]; ok { util.ToSnakeCaseKeys(value) tv.Add("Squadron", value) @@ -190,6 +194,10 @@ func (sq *Squadron) RenderConfig(ctx context.Context) error { util.ToSnakeCaseKeys(value) tv.Add("Global", value) } + if value, ok := vars["vars"]; ok { + util.ToSnakeCaseKeys(value) + tv.Add("Vars", value) + } if value, ok := vars["squadron"]; ok { util.ToSnakeCaseKeys(value) tv.Add("Squadron", value) @@ -330,7 +338,7 @@ func (sq *Squadron) Diff(ctx context.Context, helmArgs []string, parallel int) e if err != nil { return err } - valueBytes, err := v.ValuesYAML(sq.c.Global) + valueBytes, err := v.ValuesYAML(sq.c.Global, sq.c.Vars) if err != nil { return err } @@ -557,7 +565,7 @@ func (sq *Squadron) Up(ctx context.Context, helmArgs []string, username, version if err != nil { return err } - valueBytes, err := v.ValuesYAML(sq.c.Global) + valueBytes, err := v.ValuesYAML(sq.c.Global, sq.c.Vars) if err != nil { return err } @@ -625,7 +633,7 @@ func (sq *Squadron) Template(ctx context.Context, helmArgs []string, parallel in } pterm.Debug.Printfln("running helm template for chart: %s", name) - out, err := v.Template(gctx, name, key, k, namespace, sq.c.Global, helmArgs) + out, err := v.Template(gctx, name, key, k, namespace, sq.c.Global, sq.c.Vars, helmArgs) if err != nil { return err } diff --git a/squadron_test.go b/squadron_test.go index 68d987c..c0ed3b2 100644 --- a/squadron_test.go +++ b/squadron_test.go @@ -39,6 +39,10 @@ func TestConfigSimpleSnapshot(t *testing.T) { name: "global", files: []string{"squadron.yaml", "squadron.override.yaml"}, }, + { + name: "vars", + files: []string{"squadron.yaml", "squadron.override.yaml"}, + }, { name: "template", files: []string{"squadron.yaml"}, @@ -89,7 +93,7 @@ func config(t *testing.T, name string, files []string, squadronName string, unit { out, err := sq.Template(ctx, nil, 1) - require.NoError(t, err, "failed to render template") + require.NoError(t, err) testutils.Snapshot(t, path.Join("testdata", name, "snapshop-template.yaml"), out) } } diff --git a/testdata/override/snapshop-config-norender.yaml b/testdata/override/snapshop-config-norender.yaml index 80c2505..5a7ebe3 100644 --- a/testdata/override/snapshop-config-norender.yaml +++ b/testdata/override/snapshop-config-norender.yaml @@ -15,7 +15,7 @@ squadron: - baz=baz file: Dockerfile tag: nightly - image: docker.mycompany.com/mycomapny/frontend + image: storefinder/frontend values: image: repository: <% .Squadron.storefinder.frontend.builds.default.image %> diff --git a/testdata/override/snapshop-config.yaml b/testdata/override/snapshop-config.yaml index 4ff7918..3541ee5 100644 --- a/testdata/override/snapshop-config.yaml +++ b/testdata/override/snapshop-config.yaml @@ -15,8 +15,8 @@ squadron: - baz=baz file: Dockerfile tag: nightly - image: docker.mycompany.com/mycomapny/frontend + image: storefinder/frontend values: image: - repository: docker.mycompany.com/mycomapny/frontend + repository: storefinder/frontend tag: nightly diff --git a/testdata/override/snapshop-template.yaml b/testdata/override/snapshop-template.yaml index f8ebc47..0d288f5 100644 --- a/testdata/override/snapshop-template.yaml +++ b/testdata/override/snapshop-template.yaml @@ -43,7 +43,7 @@ spec: spec: containers: - name: storefinder-frontend - image: 'docker.mycompany.com/mycomapny/frontend:nightly' + image: 'storefinder/frontend:nightly' ports: - name: http protocol: TCP diff --git a/testdata/override/squadron.yaml b/testdata/override/squadron.yaml index c4979e9..673fc04 100644 --- a/testdata/override/squadron.yaml +++ b/testdata/override/squadron.yaml @@ -8,7 +8,7 @@ squadron: default: tag: latest file: Dockerfile - image: docker.mycompany.com/mycomapny/frontend + image: storefinder/frontend build_arg: - "foo=foo" - "bar=bar" @@ -22,7 +22,7 @@ squadron: default: tag: latest file: Dockerfile - image: docker.mycompany.com/mycomapny/backend + image: storefinder/backend build_arg: - "foo=foo" - "bar=bar" diff --git a/testdata/template/snapshop-config-norender.yaml b/testdata/template/snapshop-config-norender.yaml index 2542250..04c7867 100644 --- a/testdata/template/snapshop-config-norender.yaml +++ b/testdata/template/snapshop-config-norender.yaml @@ -1,5 +1,6 @@ version: "2.0" global: + enabled: true host: mycompany.com squadron: storefinder: @@ -11,7 +12,7 @@ squadron: builds: default: tag: latest - image: docker.mycompany.com/mycomapny/frontend-admin + image: storefinder/backend values: image: repository: <% .Squadron.storefinder.backend.builds.default.image %> @@ -24,10 +25,10 @@ squadron: builds: default: tag: latest - image: docker.mycompany.com/mycomapny/frontend + image: storefinder/frontend values: env: - BASE64: <% base64 "1234567890" %> + BASE64: <% "1234567890" | b64enc %> DEFAULT_INDEX_VALUE: <% defaultIndex .Global "notexists" "fallback" %> DEFAULT_VALUE: <% "" | default "fallback" %> ENV: <% env "SHELL" %> diff --git a/testdata/template/snapshop-config.yaml b/testdata/template/snapshop-config.yaml index f19196f..76a69a2 100644 --- a/testdata/template/snapshop-config.yaml +++ b/testdata/template/snapshop-config.yaml @@ -1,5 +1,6 @@ version: "2.0" global: + enabled: true host: mycompany.com squadron: storefinder: @@ -11,10 +12,10 @@ squadron: builds: default: tag: latest - image: docker.mycompany.com/mycomapny/frontend-admin + image: storefinder/backend values: image: - repository: docker.mycompany.com/mycomapny/frontend-admin + repository: storefinder/backend tag: latest frontend: chart: @@ -24,7 +25,7 @@ squadron: builds: default: tag: latest - image: docker.mycompany.com/mycomapny/frontend + image: storefinder/frontend values: env: BASE64: MTIzNDU2Nzg5MA== @@ -33,7 +34,7 @@ squadron: ENV: /bin/zsh GLOBAL: mycompany.com image: - repository: docker.mycompany.com/mycomapny/frontend + repository: storefinder/frontend tag: latest values: | foo: bar diff --git a/testdata/template/snapshop-template.yaml b/testdata/template/snapshop-template.yaml index d64469b..2904125 100644 --- a/testdata/template/snapshop-template.yaml +++ b/testdata/template/snapshop-template.yaml @@ -43,7 +43,7 @@ spec: spec: containers: - name: storefinder-backend - image: 'docker.mycompany.com/mycomapny/frontend-admin:latest' + image: 'storefinder/backend:latest' ports: - name: http protocol: TCP @@ -93,7 +93,7 @@ spec: spec: containers: - name: storefinder-frontend - image: 'docker.mycompany.com/mycomapny/frontend:latest' + image: 'storefinder/frontend:latest' ports: - name: http protocol: TCP diff --git a/testdata/template/squadron.yaml b/testdata/template/squadron.yaml index 6af8c63..24afd0d 100644 --- a/testdata/template/squadron.yaml +++ b/testdata/template/squadron.yaml @@ -2,6 +2,7 @@ version: "2.0" global: host: mycompany.com + enabled: true squadron: storefinder: @@ -10,7 +11,7 @@ squadron: builds: default: tag: latest - image: docker.mycompany.com/mycomapny/frontend + image: storefinder/frontend values: image: tag: <% .Squadron.storefinder.frontend.builds.default.tag %> @@ -18,7 +19,7 @@ squadron: env: ENV: <% env "SHELL" %> GLOBAL: <% .Global.host %> - BASE64: <% base64 "1234567890" %> + BASE64: <% "1234567890" | b64enc %> DEFAULT_VALUE: <% "" | default "fallback" %> DEFAULT_INDEX_VALUE: <% defaultIndex .Global "notexists" "fallback" %> # ONE_PASSWORD: <% op "ACCOUNT_NAME" "UUID" "FIELD" %> @@ -31,7 +32,7 @@ squadron: builds: default: tag: latest - image: docker.mycompany.com/mycomapny/frontend-admin + image: storefinder/backend values: image: tag: <% .Squadron.storefinder.backend.builds.default.tag %> diff --git a/testdata/vars/snapshop-config-norender.yaml b/testdata/vars/snapshop-config-norender.yaml new file mode 100644 index 0000000..8377b34 --- /dev/null +++ b/testdata/vars/snapshop-config-norender.yaml @@ -0,0 +1,26 @@ +version: "2.0" +vars: + bar: + - one + - two + baz: null + foo: two +squadron: + storefinder: + backend: + chart: + name: backend + repository: file://<% env "PROJECT_ROOT" %>/_examples/common/charts/backend + version: 0.0.1 + builds: + default: + tag: latest + image: storefinder/backend + values: + env: + bar: <% .Vars.bar %> + baz: <% .Vars.baz %> + foo: <% .Vars.foo %> + image: + repository: <% .Squadron.storefinder.backend.builds.default.image %> + tag: <% .Squadron.storefinder.backend.builds.default.tag %> diff --git a/testdata/vars/snapshop-config.yaml b/testdata/vars/snapshop-config.yaml new file mode 100644 index 0000000..1ad54e8 --- /dev/null +++ b/testdata/vars/snapshop-config.yaml @@ -0,0 +1,26 @@ +version: "2.0" +vars: + bar: + - one + - two + baz: null + foo: two +squadron: + storefinder: + backend: + chart: + name: backend + repository: file://./_examples/common/charts/backend + version: 0.0.1 + builds: + default: + tag: latest + image: storefinder/backend + values: + env: + bar: [one two] + baz: + foo: two + image: + repository: storefinder/backend + tag: latest diff --git a/testdata/vars/snapshop-template.yaml b/testdata/vars/snapshop-template.yaml new file mode 100644 index 0000000..92703fa --- /dev/null +++ b/testdata/vars/snapshop-template.yaml @@ -0,0 +1,50 @@ +--- +# Source: backend/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: storefinder-backend + labels: + app.kubernetes.io/name: storefinder-backend + app.kubernetes.io/component: backend + app.kubernetes.io/managed-by: Helm + helm.sh/chart: 'backend-0.0.1' + namespace: default +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: storefinder-backend + app.kubernetes.io/component: backend + ports: + - name: http + port: 80 +--- +# Source: backend/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: storefinder-backend + labels: + app.kubernetes.io/name: storefinder-backend + app.kubernetes.io/component: backend + app.kubernetes.io/managed-by: Helm + helm.sh/chart: 'backend-0.0.1' + namespace: default +spec: + selector: + matchLabels: + app.kubernetes.io/name: storefinder-backend + app.kubernetes.io/component: backend + template: + metadata: + labels: + app.kubernetes.io/name: storefinder-backend + app.kubernetes.io/component: backend + spec: + containers: + - name: storefinder-backend + image: 'storefinder/backend:latest' + ports: + - name: http + protocol: TCP + containerPort: 80 diff --git a/testdata/vars/squadron.override.yaml b/testdata/vars/squadron.override.yaml new file mode 100644 index 0000000..fa4bf5f --- /dev/null +++ b/testdata/vars/squadron.override.yaml @@ -0,0 +1,6 @@ +version: "2.0" + +vars: + foo: "two" + bar: ["two"] + baz: ~ diff --git a/testdata/vars/squadron.yaml b/testdata/vars/squadron.yaml new file mode 100644 index 0000000..21a13d8 --- /dev/null +++ b/testdata/vars/squadron.yaml @@ -0,0 +1,23 @@ +version: "2.0" + +vars: + foo: "one" + bar: ["one"] + baz: "one" + +squadron: + storefinder: + backend: + chart: <% env "PROJECT_ROOT" %>/_examples/common/charts/backend + builds: + default: + tag: latest + image: storefinder/backend + values: + image: + tag: <% .Squadron.storefinder.backend.builds.default.tag %> + repository: <% .Squadron.storefinder.backend.builds.default.image %> + env: + foo: <% .Vars.foo %> + bar: <% .Vars.bar %> + baz: <% .Vars.baz %>