Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,31 @@ jobs:
- name: Run integration tests
run: go test -tags=integration -v -timeout=8m ./...

helm-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "~1.26"
- name: Setup Helm
uses: azure/setup-helm@f0accbfd55e3332a28f721b8202b1016cecf90d5 # v5
with:
version: "v3.18.3"
- name: Run Helm chart tests
run: go test ./helm/tests/...
- name: Check for unstaged files
run: ./scripts/check_unstaged.sh

required:
runs-on: ubuntu-latest
needs:
- test
- lint
- integration-test
- helm-test
# Allow this job to run even if the needed jobs fail, are skipped or
# cancelled.
if: always()
Expand Down
198 changes: 198 additions & 0 deletions helm/tests/chart_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package tests // nolint: testpackage

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

"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
)

// 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 namespaces = []string{
"default",
"coder",
}

var testCases = []testCase{
{
name: "default_values",
expectedError: "",
},
{
name: "all_values",
expectedError: "",
},
}

type testCase struct {
name string // Name of the test case. This is used to control which values and golden file are used.
namespace string // Namespace is the name of the namespace the resources should be generated within
expectedError string // Expected error from running `helm template`.
}

func (tc testCase) valuesFilePath() string {
return filepath.Join("./testdata", tc.name+".yaml")
}

func (tc testCase) goldenFilePath() string {
if tc.namespace == "default" {
return filepath.Join("./testdata", tc.name+".golden")
}

return filepath.Join("./testdata", tc.name+"_"+tc.namespace+".golden")
}

func inCI() bool { return os.Getenv("CI") != "" }

func TestRenderChart(t *testing.T) {
t.Parallel()
if *updateGoldenFiles {
t.Skip("Golden files are being updated. Skipping test.")
}
if inCI() {
switch runtime.GOOS {
case "windows", "darwin":
t.Skip("Skipping tests on Windows and macOS in CI")
}
}

// Ensure that Helm is available in $PATH
helmPath := lookupHelm(t)
err := updateHelmDependencies(t, helmPath, "..")
require.NoError(t, err, "failed to build Helm dependencies")

for _, tc := range testCases {
for _, ns := range namespaces {
tc.namespace = ns

t.Run(tc.namespace+"/"+tc.name, func(t *testing.T) {
t.Parallel()

// Ensure that the values file exists.
valuesFilePath := tc.valuesFilePath()
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, tc.namespace)
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, "helm template should not have failed")
require.NotEmpty(t, templateOutput, "helm template output should not be empty")
goldenFilePath := tc.goldenFilePath()
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.ReplaceAll(goldenBytes, []byte("\r"), []byte(""))
expected := string(goldenBytes)

require.NoError(t, err, "failed to load golden file %q")
require.Equal(t, expected, templateOutput)
}
})
}
}
}

func TestUpdateGoldenFiles(t *testing.T) {
t.Parallel()
if !*updateGoldenFiles {
t.Skip("Run with -update to update golden files")
}

helmPath := lookupHelm(t)
err := updateHelmDependencies(t, helmPath, "..")
require.NoError(t, err, "failed to build Helm dependencies")

for _, tc := range testCases {
if tc.expectedError != "" {
t.Logf("skipping test case %q with render error", tc.name)
continue
}

for _, ns := range namespaces {
tc.namespace = ns

valuesPath := tc.valuesFilePath()
templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesPath, tc.namespace)
if err != nil {
t.Logf("error running `helm template -f %q`: %v", valuesPath, err)
t.Logf("output: %s", templateOutput)
}
require.NoError(t, err, "failed to run `helm template -f %q`", valuesPath)

goldenFilePath := tc.goldenFilePath()
err = os.WriteFile(goldenFilePath, []byte(templateOutput), 0o644) // 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.")
}

// updateHelmDependencies runs `helm dependency update .` on the given chartDir.
func updateHelmDependencies(t testing.TB, helmPath, chartDir string) error {
// Remove charts/ from chartDir if it exists.
err := os.RemoveAll(filepath.Join(chartDir, "charts"))
if err != nil {
return xerrors.Errorf("failed to remove charts/ directory: %w", err)
}

// Regenerate the chart dependencies.
cmd := exec.Command(helmPath, "dependency", "update", "--skip-refresh", ".")
cmd.Dir = chartDir
t.Logf("exec command: %v", cmd.Args)
out, err := cmd.CombinedOutput()
if err != nil {
return xerrors.Errorf("failed to run `helm dependency build`: %w\noutput: %s", err, out)
}

return nil
}

// 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, namespace 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, "--namespace", namespace)
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())
}
Loading