Skip to content

Commit

Permalink
feat: render kustomize into jsonnet (grafana#422)
Browse files Browse the repository at this point in the history
* feat: render kustomize into jsonnet

* refactor: move ListAsMap

* docs: add TANKA_KUSTOMIZE_PATH to env-vars
  • Loading branch information
Duologic authored Nov 18, 2020
1 parent 53b5a41 commit 8e1e69e
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 141 deletions.
5 changes: 5 additions & 0 deletions docs/docs/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ menu: "References"

**Description**: Path to the `helm` executable
**Default**: `$PATH/helm`

### TANKA_KUSTOMIZE_PATH

**Description**: Path to the `kustomize` executable
**Default**: `$PATH/kustomize`
45 changes: 1 addition & 44 deletions pkg/helm/jsonnet.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package helm

import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"text/template"

"github.com/Masterminds/sprig/v3"
"github.com/google/go-jsonnet"
"github.com/google/go-jsonnet/ast"
"github.com/grafana/tanka/pkg/kubernetes/manifest"
Expand Down Expand Up @@ -71,7 +68,7 @@ func NativeFunc(h Helm) *jsonnet.NativeFunction {
}

// convert list to map
out, err := listAsMap(list, opts.NameFormat)
out, err := manifest.ListAsMap(list, opts.NameFormat)
if err != nil {
return nil, err
}
Expand All @@ -98,43 +95,3 @@ func parseOpts(data interface{}) (*JsonnetOpts, error) {

return &opts, nil
}

func listAsMap(list manifest.List, nameFormat string) (map[string]interface{}, error) {
if nameFormat == "" {
nameFormat = DefaultNameFormat
}

tmpl, err := template.New("").
Funcs(sprig.TxtFuncMap()).
Parse(nameFormat)
if err != nil {
return nil, fmt.Errorf("Parsing name format: %w", err)
}

out := make(map[string]interface{})
for _, m := range list {
var buf bytes.Buffer
if err := tmpl.Execute(&buf, m); err != nil {
return nil, err
}
name := buf.String()

if _, ok := out[name]; ok {
return nil, ErrorDuplicateName{name: name, format: nameFormat}
}
out[name] = map[string]interface{}(m)
}

return out, nil
}

// ErrorDuplicateName means two resources share the same name using the given
// nameFormat.
type ErrorDuplicateName struct {
name string
format string
}

func (e ErrorDuplicateName) Error() string {
return fmt.Sprintf("Two resources share the same name '%s'. Please adapt the name template '%s'. See https://tanka.dev/helm#two-resources-share-the-same-name", e.name, e.format)
}
97 changes: 0 additions & 97 deletions pkg/helm/jsonnet_test.go

This file was deleted.

2 changes: 2 additions & 0 deletions pkg/jsonnet/native/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
jsonnet "github.com/google/go-jsonnet"
"github.com/google/go-jsonnet/ast"
"github.com/grafana/tanka/pkg/helm"
"github.com/grafana/tanka/pkg/kustomize"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v3"
)
Expand All @@ -32,6 +33,7 @@ func Funcs() []*jsonnet.NativeFunction {
regexSubst(),

helm.NativeFunc(helm.ExecHelm{}),
kustomize.NativeFunc(kustomize.ExecKustomize{}),
}
}

Expand Down
11 changes: 11 additions & 0 deletions pkg/kubernetes/manifest/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,14 @@ func (s SampleString) Indent(n int) string {
lines := strings.Split(s.String(), "\n")
return indent + strings.Join(lines, "\n"+indent)
}

// ErrorDuplicateName means two resources share the same name using the given
// nameFormat.
type ErrorDuplicateName struct {
name string
format string
}

func (e ErrorDuplicateName) Error() string {
return fmt.Sprintf("Two resources share the same name '%s'. Please adapt the name template '%s'.", e.name, e.format)
}
34 changes: 34 additions & 0 deletions pkg/kubernetes/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"bytes"
"encoding/json"
"fmt"
"text/template"

"github.com/Masterminds/sprig/v3"
"github.com/pkg/errors"
"github.com/stretchr/objx"
yaml "gopkg.in/yaml.v2"
Expand Down Expand Up @@ -270,3 +272,35 @@ func m2o(m interface{}) objx.Map {
}
return nil
}

// DefaultNameFormat to use when no nameFormat is supplied
const DefaultNameFormat = `{{ print .kind "_" .metadata.name | snakecase }}`

func ListAsMap(list List, nameFormat string) (map[string]interface{}, error) {
if nameFormat == "" {
nameFormat = DefaultNameFormat
}

tmpl, err := template.New("").
Funcs(sprig.TxtFuncMap()).
Parse(nameFormat)
if err != nil {
return nil, fmt.Errorf("Parsing name format: %w", err)
}

out := make(map[string]interface{})
for _, m := range list {
var buf bytes.Buffer
if err := tmpl.Execute(&buf, m); err != nil {
return nil, err
}
name := buf.String()

if _, ok := out[name]; ok {
return nil, ErrorDuplicateName{name: name, format: nameFormat}
}
out[name] = map[string]interface{}(m)
}

return out, nil
}
89 changes: 89 additions & 0 deletions pkg/kubernetes/manifest/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,92 @@ spec:
t.Error(s)
}
}

func TestListAsMap(t *testing.T) {
cases := []struct {
name string
list List
result map[string]interface{}
nameFormat string
err error
}{
{
name: "simple",
nameFormat: "", // test it properly defaults to DefaultNameFormat
list: List{
configMap("foo"),
deployment("bar"),
},
result: map[string]interface{}{
"config_map_foo": configMap("foo"),
"deployment_bar": deployment("bar"),
},
},
{
name: "duplicate-default",
list: List{
secret("foo", map[string]interface{}{"id": 1}),
secret("foo", map[string]interface{}{"id": 2}),
},
err: ErrorDuplicateName{name: "secret_foo", format: DefaultNameFormat},
result: nil, // expect no result
},
{
name: "duplicate-custom",
nameFormat: `{{ print .metadata.name "_" .data.id }}`,
list: List{
secret("foo", map[string]interface{}{"id": 1}),
secret("foo", map[string]interface{}{"id": 2}),
},
result: map[string]interface{}{
"foo_1": secret("foo", map[string]interface{}{"id": 1}),
"foo_2": secret("foo", map[string]interface{}{"id": 2}),
},
err: nil, // expect no error
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
result, err := ListAsMap(c.list, c.nameFormat)
if err != c.err {
t.Fatalf("err mismatch: want '%s' but got '%s'", c.err, err)
}

if diff := cmp.Diff(c.result, result); diff != "" {
t.Fatal(diff)
}
})
}
}

func configMap(name string) map[string]interface{} {
return map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{"name": name},
"data": map[string]interface{}{},
}
}

func deployment(name string) map[string]interface{} {
return map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{"name": name},
"spec": map[string]interface{}{},
}
}

func secret(name string, data map[string]interface{}) map[string]interface{} {
if data == nil {
data = map[string]interface{}{}
}

return map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{"name": name},
"data": data,
}
}
40 changes: 40 additions & 0 deletions pkg/kustomize/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package kustomize

import (
"bytes"
"io"
"os"

"github.com/grafana/tanka/pkg/kubernetes/manifest"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v3"
)

// Build expands a Kustomize into a regular manifest.List using the `kustomize
// build` command
func (k ExecKustomize) Build(path string) (manifest.List, error) {
cmd := k.cmd("build", path)
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
return nil, errors.Wrap(err, "Expanding Kustomize")
}

var list manifest.List
d := yaml.NewDecoder(&buf)
for {
var m manifest.Manifest
if err := d.Decode(&m); err != nil {
if err == io.EOF {
break
}
return nil, errors.Wrap(err, "Parsing Kustomize output")
}

list = append(list, m)
}

return list, nil
}
Loading

0 comments on commit 8e1e69e

Please sign in to comment.