Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
rewrite to just use helm template
  • Loading branch information
johnstcn committed Mar 13, 2023
commit b7a9dee9b35957f68fdceccd613fb3ff03858e00
127 changes: 85 additions & 42 deletions helm/tests/chart_test.go
Original file line number Diff line number Diff line change
@@ -1,48 +1,47 @@
package tests // nolint: testpackage

import (
"bytes"
"flag"
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
"helm.sh/helm/v3/pkg/chartutil"
"golang.org/x/xerrors"
)

// These tests render the chart with the given test case and compares the output
// to the corresponding golden file.
// These tests run `helm template` with the values file specified in each test
// and compare the output to the contents of the corresponding golden file.
// All values and golden files are located in the `testdata` directory.
// To update golden files, run `go test . -update`.

// UpdateGoldenFiles is a flag that can be set to update golden files.
var UpdateGoldenFiles = flag.Bool("update", false, "Update golden files")

var TestCases = []TestCase{
{
name: "default_values",
fn: func(v *Values) {
v.Coder.Image.Tag = "latest"
},
valuesFile: "default_values.yaml",
goldenFile: "default_values.golden",
expectedError: "",
},
{
name: "missing_values",
fn: func(v *Values) {},
renderErr: `You must specify the coder.image.tag value if you're installing the Helm chart directly from Git.`,
valuesFile: "missing_values.yaml",
goldenFile: "",
expectedError: `You must specify the coder.image.tag value if you're installing the Helm chart directly from Git.`,
},
{
name: "tls",
fn: func(v *Values) {
v.Coder.Image.Tag = "latest"
v.Coder.TLS.SecretNames = []string{"coder-tls"}
},
valuesFile: "tls.yaml",
goldenFile: "tls.golden",
expectedError: "",
},
}

type TestCase struct {
name string // Name of the test case. This corresponds to the golden file name.
fn func(*Values) // Function that mutates the values.
opts *chartutil.ReleaseOptions // Release options to pass to the renderer.
caps *chartutil.Capabilities // Capabilities to pass to the renderer.
renderErr string // Expected error from rendering the chart.
valuesFile string // Values file to be passed to `helm template`.
goldenFile string // Expected output from running `helm template`.
expectedError string // Expected error from running `helm template`.
}

func TestRenderChart(t *testing.T) {
Expand All @@ -51,26 +50,38 @@ func TestRenderChart(t *testing.T) {
t.Skip("Golden files are being updated. Skipping test.")
}

// Ensure that Helm is available in $PATH
helmPath := lookupHelm(t)
for _, tc := range TestCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require.NotEmpty(t, tc.valuesFile, "all test cases must specify a values file")
t.Run(tc.valuesFile, func(t *testing.T) {
t.Parallel()

w, err := LoadChart()
require.NoError(t, err)
require.NoError(t, w.Chart.Validate())
manifests, err := renderManifests(w.Chart, w.OriginalValues, tc.fn, tc.opts, tc.caps)
if tc.renderErr != "" {
require.Error(t, err, "render should have failed")
require.Contains(t, err.Error(), tc.renderErr, "render error should match")
require.Empty(t, manifests, "manifests should be empty")
// Ensure that the values file exists.
valuesFilePath := filepath.Join("testdata", tc.valuesFile)
if _, err := os.Stat(valuesFilePath); os.IsNotExist(err) {
t.Fatalf("values file %q does not exist", valuesFilePath)
}

// Run helm template with the values file.
templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesFilePath)
if tc.expectedError != "" {
require.Error(t, err, "helm template should have failed")
require.Contains(t, templateOutput, tc.expectedError, "helm template output should contain expected error")
} else {
require.NoError(t, err, "render should not have failed")
require.NotEmpty(t, manifests, "manifests should not be empty")
expected, err := readGoldenFile(tc.name)
require.NoError(t, err, "failed to load golden file")
actual := dumpManifests(manifests)
require.Equal(t, expected, actual)
require.NoError(t, err, "helm template should not have failed")
require.NotEmpty(t, templateOutput, "helm template output should not be empty")
goldenFilePath := filepath.Join("testdata", tc.goldenFile)
goldenBytes, err := os.ReadFile(goldenFilePath)
require.NoError(t, err, "failed to read golden file %q", goldenFilePath)

// Remove carriage returns to make tests pass on Windows.
goldenBytes = bytes.Replace(goldenBytes, []byte("\r"), []byte(""), -1)
expected := string(goldenBytes)

require.NoError(t, err, "failed to load golden file %q")
require.Equal(t, expected, templateOutput)
}
})
}
Expand All @@ -81,20 +92,52 @@ func TestUpdateGoldenFiles(t *testing.T) {
if !*UpdateGoldenFiles {
t.Skip("Run with -update to update golden files")
}

helmPath := lookupHelm(t)
for _, tc := range TestCases {
w, err := LoadChart()
require.NoError(t, err, "failed to load chart")
if tc.renderErr != "" {
t.Logf("skipping test case %q with render error", tc.name)
if tc.expectedError != "" {
t.Logf("skipping test case %q with render error", tc.valuesFile)
continue
}
manifests, err := renderManifests(w.Chart, w.OriginalValues, tc.fn, tc.opts, tc.caps)
require.NoError(t, err, "failed to render manifests")
require.NoError(t, writeGoldenFile(tc.name, manifests), "failed to write golden file")

valuesPath := filepath.Join("testdata", tc.valuesFile)
templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesPath)

require.NoError(t, err, "failed to run `helm template -f %q`", valuesPath)

goldenFilePath := filepath.Join("testdata", tc.goldenFile)
err = os.WriteFile(goldenFilePath, []byte(templateOutput), 0644) // nolint:gosec
require.NoError(t, err, "failed to write golden file %q", goldenFilePath)
}
t.Log("Golden files updated. Please review the changes and commit them.")
}

// runHelmTemplate runs helm template on the given chart with the given values and
// returns the raw output.
func runHelmTemplate(t testing.TB, helmPath, chartDir, valuesFilePath string) (string, error) {
// Ensure that valuesFilePath exists
if _, err := os.Stat(valuesFilePath); err != nil {
return "", xerrors.Errorf("values file %q does not exist: %w", valuesFilePath, err)
}

cmd := exec.Command(helmPath, "template", chartDir, "-f", valuesFilePath)
t.Logf("exec command: %v", cmd.Args)
out, err := cmd.CombinedOutput()
return string(out), err
}

// lookupHelm ensures that Helm is available in $PATH and returns the path to the
// Helm executable.
func lookupHelm(t testing.TB) string {
helmPath, err := exec.LookPath("helm")
if err != nil {
t.Fatalf("helm not found in $PATH: %v", err)
return ""
}
t.Logf("Using helm at %q", helmPath)
return helmPath
}

func TestMain(m *testing.M) {
flag.Parse()
os.Exit(m.Run())
Expand Down
139 changes: 60 additions & 79 deletions helm/tests/testdata/default_values.golden
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
# This file is generated by TestUpdateGoldenFiles. DO NOT EDIT.
--- # Source: coder/templates/NOTES.txt


Enjoy Coder! Please create an issue at https://github.com/coder/coder if you run
into any problems! :)

--- # Source: coder/templates/coder.yaml

---
# Source: coder/templates/coder.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
Expand All @@ -17,20 +9,73 @@ metadata:
labels:
helm.sh/chart: coder-0.1.0
app.kubernetes.io/name: coder
app.kubernetes.io/instance: coder
app.kubernetes.io/instance: release-name
app.kubernetes.io/part-of: coder
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm

---
# Source: coder/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: coder-workspace-perms
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["*"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["*"]
---
# Source: coder/templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: "coder"
subjects:
- kind: ServiceAccount
name: "coder"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: coder-workspace-perms
---
# Source: coder/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: coder
labels:
helm.sh/chart: coder-0.1.0
app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
app.kubernetes.io/part-of: coder
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
annotations:
{}
spec:
type: LoadBalancer
sessionAffinity: ClientIP
ports:
- name: "http"
port: 80
targetPort: "http"
protocol: TCP
externalTrafficPolicy: "Cluster"
selector:
app.kubernetes.io/name: coder
app.kubernetes.io/instance: release-name
---
# Source: coder/templates/coder.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: coder
labels:
helm.sh/chart: coder-0.1.0
app.kubernetes.io/name: coder
app.kubernetes.io/instance: coder
app.kubernetes.io/instance: release-name
app.kubernetes.io/part-of: coder
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
Expand All @@ -41,13 +86,13 @@ spec:
selector:
matchLabels:
app.kubernetes.io/name: coder
app.kubernetes.io/instance: coder
app.kubernetes.io/instance: release-name
template:
metadata:
labels:
helm.sh/chart: coder-0.1.0
app.kubernetes.io/name: coder
app.kubernetes.io/instance: coder
app.kubernetes.io/instance: release-name
app.kubernetes.io/part-of: coder
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
Expand Down Expand Up @@ -81,7 +126,7 @@ spec:
# Set the default access URL so a `helm apply` works by default.
# See: https://github.com/coder/coder/issues/5024
- name: CODER_ACCESS_URL
value: "http://coder.coder.svc.cluster.local"
value: "http://coder.default.svc.cluster.local"
# Used for inter-pod communication with high-availability.
- name: KUBE_POD_IP
valueFrom:
Expand Down Expand Up @@ -114,67 +159,3 @@ spec:
scheme: "HTTP"
volumeMounts: []
volumes: []

--- # Source: coder/templates/extra-templates.yaml


--- # Source: coder/templates/ingress.yaml


--- # Source: coder/templates/rbac.yaml

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: coder-workspace-perms
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["*"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["*"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: "coder"
subjects:
- kind: ServiceAccount
name: "coder"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: coder-workspace-perms

--- # Source: coder/templates/service.yaml

---
apiVersion: v1
kind: Service
metadata:
name: coder
labels:
helm.sh/chart: coder-0.1.0
app.kubernetes.io/name: coder
app.kubernetes.io/instance: coder
app.kubernetes.io/part-of: coder
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/managed-by: Helm
annotations:
{}
spec:
type: LoadBalancer
sessionAffinity: ClientIP
ports:
- name: "http"
port: 80
targetPort: "http"
protocol: TCP
externalTrafficPolicy: "Cluster"
selector:
app.kubernetes.io/name: coder
app.kubernetes.io/instance: coder

3 changes: 3 additions & 0 deletions helm/tests/testdata/default_values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
coder:
image:
tag: latest
Empty file.
Loading