Skip to content

Commit 9b2abf0

Browse files
authored
chore(helm): add unit tests for helm chart (coder#6557)
This PR adds a minimum set of Helm tests for the Helm chart. It's heavily based on the approach in [1], but uses a golden-files-based approach instead. It also runs helm template directly instead of importing the entire Kubernetes API. Golden files can be updated by running go test ./helm/tests -update or by running make update-golden-files. [1] https://github.com/coder/enterprise-helm Fixes coder#6552
1 parent 179d9e0 commit 9b2abf0

13 files changed

+523
-7
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ site/playwright-report/*
3030

3131
# Make target for updating golden files.
3232
cli/testdata/.gen-golden
33+
helm/tests/testdata/.gen-golden
3334

3435
# Build
3536
/build/

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ site/playwright-report/*
3333

3434
# Make target for updating golden files.
3535
cli/testdata/.gen-golden
36+
helm/tests/testdata/.gen-golden
3637

3738
# Build
3839
/build/

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,13 +516,17 @@ coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS)
516516
./scripts/apidocgen/generate.sh
517517
yarn run --cwd=site format:write:only ../docs/api ../docs/manifest.json ../coderd/apidoc/swagger.json
518518

519-
update-golden-files: cli/testdata/.gen-golden
519+
update-golden-files: cli/testdata/.gen-golden helm/tests/testdata/.gen-golden
520520
.PHONY: update-golden-files
521521

522522
cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(GO_SRC_FILES)
523523
go test ./cli -run=TestCommandHelp -update
524524
touch "$@"
525525

526+
helm/tests/testdata/.gen-golden: $(wildcard helm/tests/testdata/*.golden) $(GO_SRC_FILES)
527+
go test ./helm/tests -run=TestUpdateGoldenFiles -update
528+
touch "$@"
529+
526530
# Generate a prettierrc for the site package that uses relative paths for
527531
# overrides. This allows us to share the same prettier config between the
528532
# site and the root of the repo.

flake.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
gopls
3030
gotestsum
3131
jq
32+
kubernetes-helm
3233
nfpm
3334
nodePackages.typescript
3435
nodePackages.typescript-language-server

helm/tests/chart_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package tests // nolint: testpackage
2+
3+
import (
4+
"bytes"
5+
"flag"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"runtime"
10+
"testing"
11+
12+
"github.com/stretchr/testify/require"
13+
"golang.org/x/xerrors"
14+
)
15+
16+
// These tests run `helm template` with the values file specified in each test
17+
// and compare the output to the contents of the corresponding golden file.
18+
// All values and golden files are located in the `testdata` directory.
19+
// To update golden files, run `go test . -update`.
20+
21+
// UpdateGoldenFiles is a flag that can be set to update golden files.
22+
var UpdateGoldenFiles = flag.Bool("update", false, "Update golden files")
23+
24+
var TestCases = []TestCase{
25+
{
26+
name: "default_values",
27+
expectedError: "",
28+
},
29+
{
30+
name: "missing_values",
31+
expectedError: `You must specify the coder.image.tag value if you're installing the Helm chart directly from Git.`,
32+
},
33+
{
34+
name: "tls",
35+
expectedError: "",
36+
},
37+
}
38+
39+
type TestCase struct {
40+
name string // Name of the test case. This is used to control which values and golden file are used.
41+
expectedError string // Expected error from running `helm template`.
42+
}
43+
44+
func (tc TestCase) valuesFilePath() string {
45+
return filepath.Join("./testdata", tc.name+".yaml")
46+
}
47+
48+
func (tc TestCase) goldenFilePath() string {
49+
return filepath.Join("./testdata", tc.name+".golden")
50+
}
51+
52+
func TestRenderChart(t *testing.T) {
53+
t.Parallel()
54+
if *UpdateGoldenFiles {
55+
t.Skip("Golden files are being updated. Skipping test.")
56+
}
57+
if _, runningInCI := os.LookupEnv("CI"); runningInCI {
58+
switch runtime.GOOS {
59+
case "windows", "darwin":
60+
t.Skip("Skipping tests on Windows and macOS in CI")
61+
}
62+
}
63+
64+
// Ensure that Helm is available in $PATH
65+
helmPath := lookupHelm(t)
66+
for _, tc := range TestCases {
67+
tc := tc
68+
t.Run(tc.name, func(t *testing.T) {
69+
t.Parallel()
70+
71+
// Ensure that the values file exists.
72+
valuesFilePath := tc.valuesFilePath()
73+
if _, err := os.Stat(valuesFilePath); os.IsNotExist(err) {
74+
t.Fatalf("values file %q does not exist", valuesFilePath)
75+
}
76+
77+
// Run helm template with the values file.
78+
templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesFilePath)
79+
if tc.expectedError != "" {
80+
require.Error(t, err, "helm template should have failed")
81+
require.Contains(t, templateOutput, tc.expectedError, "helm template output should contain expected error")
82+
} else {
83+
require.NoError(t, err, "helm template should not have failed")
84+
require.NotEmpty(t, templateOutput, "helm template output should not be empty")
85+
goldenFilePath := tc.goldenFilePath()
86+
goldenBytes, err := os.ReadFile(goldenFilePath)
87+
require.NoError(t, err, "failed to read golden file %q", goldenFilePath)
88+
89+
// Remove carriage returns to make tests pass on Windows.
90+
goldenBytes = bytes.Replace(goldenBytes, []byte("\r"), []byte(""), -1)
91+
expected := string(goldenBytes)
92+
93+
require.NoError(t, err, "failed to load golden file %q")
94+
require.Equal(t, expected, templateOutput)
95+
}
96+
})
97+
}
98+
}
99+
100+
func TestUpdateGoldenFiles(t *testing.T) {
101+
t.Parallel()
102+
if !*UpdateGoldenFiles {
103+
t.Skip("Run with -update to update golden files")
104+
}
105+
106+
helmPath := lookupHelm(t)
107+
for _, tc := range TestCases {
108+
if tc.expectedError != "" {
109+
t.Logf("skipping test case %q with render error", tc.name)
110+
continue
111+
}
112+
113+
valuesPath := tc.valuesFilePath()
114+
templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesPath)
115+
116+
require.NoError(t, err, "failed to run `helm template -f %q`", valuesPath)
117+
118+
goldenFilePath := tc.goldenFilePath()
119+
err = os.WriteFile(goldenFilePath, []byte(templateOutput), 0o644) // nolint:gosec
120+
require.NoError(t, err, "failed to write golden file %q", goldenFilePath)
121+
}
122+
t.Log("Golden files updated. Please review the changes and commit them.")
123+
}
124+
125+
// runHelmTemplate runs helm template on the given chart with the given values and
126+
// returns the raw output.
127+
func runHelmTemplate(t testing.TB, helmPath, chartDir, valuesFilePath string) (string, error) {
128+
// Ensure that valuesFilePath exists
129+
if _, err := os.Stat(valuesFilePath); err != nil {
130+
return "", xerrors.Errorf("values file %q does not exist: %w", valuesFilePath, err)
131+
}
132+
133+
cmd := exec.Command(helmPath, "template", chartDir, "-f", valuesFilePath, "--namespace", "default")
134+
t.Logf("exec command: %v", cmd.Args)
135+
out, err := cmd.CombinedOutput()
136+
return string(out), err
137+
}
138+
139+
// lookupHelm ensures that Helm is available in $PATH and returns the path to the
140+
// Helm executable.
141+
func lookupHelm(t testing.TB) string {
142+
helmPath, err := exec.LookPath("helm")
143+
if err != nil {
144+
t.Fatalf("helm not found in $PATH: %v", err)
145+
return ""
146+
}
147+
t.Logf("Using helm at %q", helmPath)
148+
return helmPath
149+
}
150+
151+
func TestMain(m *testing.M) {
152+
flag.Parse()
153+
os.Exit(m.Run())
154+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
---
2+
# Source: coder/templates/coder.yaml
3+
apiVersion: v1
4+
kind: ServiceAccount
5+
metadata:
6+
name: "coder"
7+
annotations:
8+
{}
9+
labels:
10+
helm.sh/chart: coder-0.1.0
11+
app.kubernetes.io/name: coder
12+
app.kubernetes.io/instance: release-name
13+
app.kubernetes.io/part-of: coder
14+
app.kubernetes.io/version: "0.1.0"
15+
app.kubernetes.io/managed-by: Helm
16+
---
17+
# Source: coder/templates/rbac.yaml
18+
apiVersion: rbac.authorization.k8s.io/v1
19+
kind: Role
20+
metadata:
21+
name: coder-workspace-perms
22+
rules:
23+
- apiGroups: [""]
24+
resources: ["pods"]
25+
verbs: ["*"]
26+
- apiGroups: [""]
27+
resources: ["persistentvolumeclaims"]
28+
verbs: ["*"]
29+
---
30+
# Source: coder/templates/rbac.yaml
31+
apiVersion: rbac.authorization.k8s.io/v1
32+
kind: RoleBinding
33+
metadata:
34+
name: "coder"
35+
subjects:
36+
- kind: ServiceAccount
37+
name: "coder"
38+
roleRef:
39+
apiGroup: rbac.authorization.k8s.io
40+
kind: Role
41+
name: coder-workspace-perms
42+
---
43+
# Source: coder/templates/service.yaml
44+
apiVersion: v1
45+
kind: Service
46+
metadata:
47+
name: coder
48+
labels:
49+
helm.sh/chart: coder-0.1.0
50+
app.kubernetes.io/name: coder
51+
app.kubernetes.io/instance: release-name
52+
app.kubernetes.io/part-of: coder
53+
app.kubernetes.io/version: "0.1.0"
54+
app.kubernetes.io/managed-by: Helm
55+
annotations:
56+
{}
57+
spec:
58+
type: LoadBalancer
59+
sessionAffinity: ClientIP
60+
ports:
61+
- name: "http"
62+
port: 80
63+
targetPort: "http"
64+
protocol: TCP
65+
externalTrafficPolicy: "Cluster"
66+
selector:
67+
app.kubernetes.io/name: coder
68+
app.kubernetes.io/instance: release-name
69+
---
70+
# Source: coder/templates/coder.yaml
71+
apiVersion: apps/v1
72+
kind: Deployment
73+
metadata:
74+
name: coder
75+
labels:
76+
helm.sh/chart: coder-0.1.0
77+
app.kubernetes.io/name: coder
78+
app.kubernetes.io/instance: release-name
79+
app.kubernetes.io/part-of: coder
80+
app.kubernetes.io/version: "0.1.0"
81+
app.kubernetes.io/managed-by: Helm
82+
annotations:
83+
{}
84+
spec:
85+
replicas: 1
86+
selector:
87+
matchLabels:
88+
app.kubernetes.io/name: coder
89+
app.kubernetes.io/instance: release-name
90+
template:
91+
metadata:
92+
labels:
93+
helm.sh/chart: coder-0.1.0
94+
app.kubernetes.io/name: coder
95+
app.kubernetes.io/instance: release-name
96+
app.kubernetes.io/part-of: coder
97+
app.kubernetes.io/version: "0.1.0"
98+
app.kubernetes.io/managed-by: Helm
99+
spec:
100+
serviceAccountName: "coder"
101+
restartPolicy: Always
102+
terminationGracePeriodSeconds: 60
103+
affinity:
104+
podAntiAffinity:
105+
preferredDuringSchedulingIgnoredDuringExecution:
106+
- podAffinityTerm:
107+
labelSelector:
108+
matchExpressions:
109+
- key: app.kubernetes.io/instance
110+
operator: In
111+
values:
112+
- coder
113+
topologyKey: kubernetes.io/hostname
114+
weight: 1
115+
containers:
116+
- name: coder
117+
image: "ghcr.io/coder/coder:latest"
118+
imagePullPolicy: IfNotPresent
119+
resources:
120+
{}
121+
env:
122+
- name: CODER_HTTP_ADDRESS
123+
value: "0.0.0.0:8080"
124+
- name: CODER_PROMETHEUS_ADDRESS
125+
value: "0.0.0.0:2112"
126+
# Set the default access URL so a `helm apply` works by default.
127+
# See: https://github.com/coder/coder/issues/5024
128+
- name: CODER_ACCESS_URL
129+
value: "http://coder.default.svc.cluster.local"
130+
# Used for inter-pod communication with high-availability.
131+
- name: KUBE_POD_IP
132+
valueFrom:
133+
fieldRef:
134+
fieldPath: status.podIP
135+
- name: CODER_DERP_SERVER_RELAY_URL
136+
value: "http://$(KUBE_POD_IP):8080"
137+
138+
ports:
139+
- name: "http"
140+
containerPort: 8080
141+
protocol: TCP
142+
securityContext:
143+
allowPrivilegeEscalation: false
144+
readOnlyRootFilesystem: null
145+
runAsGroup: 1000
146+
runAsNonRoot: true
147+
runAsUser: 1000
148+
seccompProfile:
149+
type: RuntimeDefault
150+
readinessProbe:
151+
httpGet:
152+
path: /api/v2/buildinfo
153+
port: "http"
154+
scheme: "HTTP"
155+
livenessProbe:
156+
httpGet:
157+
path: /api/v2/buildinfo
158+
port: "http"
159+
scheme: "HTTP"
160+
volumeMounts: []
161+
volumes: []
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
coder:
2+
image:
3+
tag: latest

helm/tests/testdata/missing_values.yaml

Whitespace-only changes.

0 commit comments

Comments
 (0)