Skip to content

Commit

Permalink
helm: Set includeCrds to true by default (#851)
Browse files Browse the repository at this point in the history
This is what `helm install` does. With Helm 3, we aren't installing CRDs
by default any more and we expect this to be what most Tanka + Helm
users want.

Closes: #829
  • Loading branch information
iainlane authored May 4, 2023
1 parent 4bcf64e commit fad88c8
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 12 deletions.
13 changes: 11 additions & 2 deletions docs/docs/helm.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,6 @@ The following options control how the command is invoked:
values: {
persistence: { enabled: true }
},
// Equivalent to: --include-crds; only relevant for Helm v3+.
includeCrds: true,
// Equivalent to: --api-versions v1 --api-versions apps/v1
apiVersions: ['v1', 'apps/v1']
// Equivalent to: --kube-version v1.20.0
Expand All @@ -90,6 +88,17 @@ The following options control how the command is invoked:
}
```

Tanka will install Custom Resource Definitions (CRDs) automatically, if the
Helm Chart requires them and ships them in `crds/`. This is equivalent to `helm
template --include-crds`. This can be disabled using `includeCrds: false`:

```jsonnet
{
grafana: helm.template("grafana", "./charts/grafana", {
includeCrds: false
})
}
```

## Vendoring Helm Charts

Expand Down
14 changes: 14 additions & 0 deletions pkg/helm/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ type Helm interface {

// Template returns the individual resources of a Helm Chart
Template(name, chart string, opts TemplateOpts) (manifest.List, error)

// ChartExists checks if a chart exists in the provided calledFromPath
ChartExists(chart string, opts *JsonnetOpts) (string, error)
}

// PullOpts are additional, non-required options for Helm.Pull
Expand Down Expand Up @@ -103,6 +106,17 @@ func (e ExecHelm) RepoUpdate(opts Opts) error {
return nil
}

func (e ExecHelm) ChartExists(chart string, opts *JsonnetOpts) (string, error) {
// resolve the Chart relative to the caller
callerDir := filepath.Dir(opts.CalledFrom)
chart = filepath.Join(callerDir, chart)
if _, err := os.Stat(chart); err != nil {
return "", fmt.Errorf("helmTemplate: Failed to find a chart at '%s': %s. See https://tanka.dev/helm#failed-to-find-chart", chart, err)
}

return chart, nil
}

// cmd returns a prepared exec.Cmd to use the `helm` binary
func (e ExecHelm) cmd(action string, args ...string) *exec.Cmd {
argv := []string{action}
Expand Down
19 changes: 12 additions & 7 deletions pkg/helm/jsonnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package helm
import (
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/google/go-jsonnet"
"github.com/google/go-jsonnet/ast"
Expand Down Expand Up @@ -54,10 +52,8 @@ func NativeFunc(h Helm) *jsonnet.NativeFunction {
return "", err
}

// resolve the Chart relative to the caller
callerDir := filepath.Dir(opts.CalledFrom)
chart := filepath.Join(callerDir, chartpath)
if _, err := os.Stat(chart); err != nil {
chart, err := h.ChartExists(chartpath, opts)
if err != nil {
return nil, fmt.Errorf("helmTemplate: Failed to find a chart at '%s': %s. See https://tanka.dev/helm#failed-to-find-chart", chart, err)
}

Expand All @@ -83,7 +79,16 @@ func parseOpts(data interface{}) (*JsonnetOpts, error) {
if err != nil {
return nil, err
}
var opts JsonnetOpts

// default IncludeCRDs to true, as this is the default in the `helm install`
// command. Needs to be specified here because the zero value of bool is
// false.
opts := JsonnetOpts{
TemplateOpts: TemplateOpts{
IncludeCRDs: true,
},
}

if err := json.Unmarshal(c, &opts); err != nil {
return nil, err
}
Expand Down
142 changes: 142 additions & 0 deletions pkg/helm/jsonnet_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package helm

import (
"testing"

"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/grafana/tanka/pkg/kubernetes/manifest"
)

const calledFrom = "/my/path/here"

type MockHelm struct {
mock.Mock
}

// fulfill the Helm interface
func (m *MockHelm) Pull(chart, version string, opts PullOpts) error {
args := m.Called(chart, version, opts)
return args.Error(0)
}

func (m *MockHelm) RepoUpdate(opts Opts) error {
args := m.Called(opts)
return args.Error(0)
}

func (m *MockHelm) Template(name, chart string, opts TemplateOpts) (manifest.List, error) {
args := m.Called(name, chart, opts)

// figure out what arguments `helm template` would be called with and save
// them
execHelm := &ExecHelm{}
cmdArgs := execHelm.templateCommandArgs(name, chart, opts)
m.TestData().Set("templateCommandArgs", cmdArgs)

return args.Get(0).(manifest.List), args.Error(1)
}

func (m *MockHelm) ChartExists(chart string, opts *JsonnetOpts) (string, error) {
args := m.Called(chart, opts)
return args.String(0), args.Error(1)
}

func callNativeFunction(t *testing.T, expectedHelmTemplateOptions TemplateOpts, inputOptionsFromJsonnet map[string]interface{}) []string {
t.Helper()

helmMock := &MockHelm{}

helmMock.On(
"ChartExists",
"exampleChartPath",
mock.AnythingOfType("*helm.JsonnetOpts")).
Return("/full/chart/path", nil).
Once()

// this verifies that the helmMock.Template() method is called with the
// correct arguments, i.e. includeCrds: true is set by default
helmMock.On("Template", "exampleChartName", "/full/chart/path", expectedHelmTemplateOptions).
Return(manifest.List{}, nil).
Once()

nf := NativeFunc(helmMock)
require.NotNil(t, nf)

// the mandatory parameters to helm.template() in Jsonnet
params := []string{
"exampleChartName",
"exampleChartPath",
}

// mandatory parameters + the k-v pairs from the Jsonnet input
paramsInterface := make([]interface{}, 3)
paramsInterface[0] = params[0]
paramsInterface[1] = params[1]
paramsInterface[2] = inputOptionsFromJsonnet

_, err := nf.Func(paramsInterface)

require.NoError(t, err)

helmMock.AssertExpectations(t)

return helmMock.TestData().Get("templateCommandArgs").StringSlice()
}

// TestDefaultCommandineFlagsIncludeCrds tests that the includeCrds flag is set
// to true by default
func TestDefaultCommandLineFlagsIncludeCrds(t *testing.T) {
kubeVersion := "1.18.0"

// we will check that the template function is called with these options,
// i.e. that includeCrds got set to true. This is not us passing an input,
// we are asserting here that the template function is called with these
// options.
expectedHelmTemplateOptions := TemplateOpts{
KubeVersion: kubeVersion,
IncludeCRDs: true,
}

// the options to helm.template(), which are a Jsonnet object, turned into a
// go map[string]interface{}. we do not set includeCrds here, so it should
// be true by default
inputOptionsFromJsonnet := make(map[string]interface{})
inputOptionsFromJsonnet["calledFrom"] = calledFrom
inputOptionsFromJsonnet["kubeVersion"] = kubeVersion

args := callNativeFunction(t, expectedHelmTemplateOptions, inputOptionsFromJsonnet)

// finally check that the actual command line arguments we will pass to
// `helm template` contain the --include-crds flag
require.Contains(t, args, "--include-crds")
}

// TestIncludeCrdsFalse tests that the includeCrds flag is can be set to false,
// and this makes it to the helm.Template() method call
func TestIncludeCrdsFalse(t *testing.T) {
kubeVersion := "1.18.0"

// we will check that the template function is called with these options,
// i.e. that includeCrds got set to false. This is not us passing an input,
// we are asserting here that the template function is called with these
// options.
expectedHelmTemplateOptions := TemplateOpts{
KubeVersion: kubeVersion,
IncludeCRDs: false,
}

// the options to helm.template(), which are a Jsonnet object, turned into a
// go map[string]interface{}. we explicitly set includeCrds to false here
inputOptionsFromJsonnet := make(map[string]interface{})
inputOptionsFromJsonnet["calledFrom"] = calledFrom
inputOptionsFromJsonnet["kubeVersion"] = kubeVersion
inputOptionsFromJsonnet["includeCrds"] = false

args := callNativeFunction(t, expectedHelmTemplateOptions, inputOptionsFromJsonnet)

// finally check that the actual command line arguments we will pass to
// `helm template` don't contain the --include-crds flag
require.NotContains(t, args, "--include-crds")
}
11 changes: 8 additions & 3 deletions pkg/helm/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@ import (
yaml "gopkg.in/yaml.v3"
)

// Template expands a Helm Chart into a regular manifest.List using the `helm
// template` command
func (e ExecHelm) Template(name, chart string, opts TemplateOpts) (manifest.List, error) {
func (e ExecHelm) templateCommandArgs(name, chart string, opts TemplateOpts) []string {
args := []string{name, chart,
"--values", "-", // values from stdin
}
args = append(args, opts.Flags()...)
return args
}

// Template expands a Helm Chart into a regular manifest.List using the `helm
// template` command
func (e ExecHelm) Template(name, chart string, opts TemplateOpts) (manifest.List, error) {
args := e.templateCommandArgs(name, chart, opts)

cmd := e.cmd("template", args...)
var buf bytes.Buffer
Expand Down

0 comments on commit fad88c8

Please sign in to comment.