Merge pull request #239 from helmfile/local-oci-integration-test
Add integration test for #238 with local docker registry as a OCI-based helm chart repo
This commit is contained in:
		
						commit
						93b1ac2b19
					
				|  | @ -0,0 +1,10 @@ | ||||||
|  | This directory contains a set of Go test source and testdata | ||||||
|  | to test the helmfile template's rendering result by calling `helmfile build` or `helmfile template` on test input | ||||||
|  | and comparing the output against the snapshot. | ||||||
|  | 
 | ||||||
|  | The `testdata` directory is composed of: | ||||||
|  | 
 | ||||||
|  | - `charts`: The Helm charts used from within test helmfile configs (`snapshpt/*/input.yaml`) as local charts and remote charts | ||||||
|  | - `snapshot/$NAME/input.yaml`: The input helmfile config for the test case of `$NAME` | ||||||
|  | - `snapshot/$NAME/output.yaml`: The expected output of the helmfile command | ||||||
|  | - `snapshot/$NAME/config.yaml`: The snapshot test configuration file. See the `Config` struct defined in `snapshot_test.go` for more information | ||||||
|  | @ -2,21 +2,34 @@ package helmfile | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/exec" | 	"os/exec" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"runtime" | 	"runtime" | ||||||
|  | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/require" | 	"github.com/stretchr/testify/require" | ||||||
|  | 	"gopkg.in/yaml.v3" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestHelmfileTemplateWithBuildCommand(t *testing.T) { | func TestHelmfileTemplateWithBuildCommand(t *testing.T) { | ||||||
|  | 	type Config struct { | ||||||
|  | 		LocalDockerRegistry struct { | ||||||
|  | 			Enabled bool `yaml:"enabled"` | ||||||
|  | 			Port    int  `yaml:"port"` | ||||||
|  | 		} `yaml:"localDockerRegistry"` | ||||||
|  | 		ChartifyTempDir string   `yaml:"chartifyTempDir"` | ||||||
|  | 		HelmfileArgs    []string `yaml:"helmfileArgs"` | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	_, filename, _, _ := runtime.Caller(0) | 	_, filename, _, _ := runtime.Caller(0) | ||||||
| 	projectRoot := filepath.Join(filepath.Dir(filename), "..", "..", "..", "..") | 	projectRoot := filepath.Join(filepath.Dir(filename), "..", "..", "..", "..") | ||||||
| 	helmfileBin := filepath.Join(projectRoot, "helmfile") | 	helmfileBin := filepath.Join(projectRoot, "helmfile") | ||||||
| 	testdataDir := "testdata/snapshot" | 	testdataDir := "testdata/snapshot" | ||||||
|  | 	chartsDir := "testdata/charts" | ||||||
| 
 | 
 | ||||||
| 	entries, err := os.ReadDir(testdataDir) | 	entries, err := os.ReadDir(testdataDir) | ||||||
| 	require.NoError(t, err) | 	require.NoError(t, err) | ||||||
|  | @ -28,23 +41,152 @@ func TestHelmfileTemplateWithBuildCommand(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 		name := e.Name() | 		name := e.Name() | ||||||
| 
 | 
 | ||||||
| 		t.Run(name, func(t *testing.T) { | 		wd, err := os.Getwd() | ||||||
| 			inputFile := filepath.Join(testdataDir, name, "input.yaml") | 		require.NoError(t, err) | ||||||
| 
 | 
 | ||||||
| 			want, err := os.ReadFile(filepath.Join(testdataDir, name, "output.yaml")) | 		// We read the config from `testdata/snapshot/$CASE_NAME/config.yaml`.
 | ||||||
| 			require.NoError(t, err) | 		// It's optional so the test won't fail even if the config file does not exist.
 | ||||||
|  | 
 | ||||||
|  | 		var config Config | ||||||
|  | 
 | ||||||
|  | 		configFile := filepath.Join(testdataDir, name, "config.yaml") | ||||||
|  | 		if configData, err := os.ReadFile(configFile); err == nil { | ||||||
|  | 			if err := yaml.Unmarshal(configData, &config); err != nil { | ||||||
|  | 				t.Fatalf("Unable to load %s: %v", configFile, err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// We run `helmfile build` by default.
 | ||||||
|  | 		// If you want to test `helmfile template`, set the following in the config.yaml:
 | ||||||
|  | 		//
 | ||||||
|  | 		// helmfileArgs:
 | ||||||
|  | 		// - template
 | ||||||
|  | 		helmfileArgs := config.HelmfileArgs | ||||||
|  | 		if len(helmfileArgs) == 0 { | ||||||
|  | 			helmfileArgs = append(helmfileArgs, "build") | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		t.Run(name, func(t *testing.T) { | ||||||
|  | 			// Use the specific chartify tempdir for easy debugging and the test reproducibility.
 | ||||||
|  | 			// We do snapshot testing in this test. The default chartify tempdir is a random directory created within the os temp dir.
 | ||||||
|  | 			// Without making it a static path, it's unnecessarily hard to snapshot test it, as the dir path embedded in the output changes
 | ||||||
|  | 			// on each test run.
 | ||||||
|  | 			chartifyTempDir := config.ChartifyTempDir | ||||||
|  | 			if chartifyTempDir == "" { | ||||||
|  | 				chartifyTempDir = "chartify_temp" | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			// We set the envvar probided by chartify, CHARTIFY_TEMPDIR, to make the tempdir static.
 | ||||||
|  | 			chartifyTempDir = filepath.Join(wd, chartifyTempDir) | ||||||
|  | 			t.Setenv("CHARTIFY_TEMPDIR", chartifyTempDir) | ||||||
|  | 			// Ensure there's no dangling and remaining tempdir from the previous run
 | ||||||
|  | 			if err := os.RemoveAll(chartifyTempDir); err != nil { | ||||||
|  | 				t.Fatalf("unable to remove chartify temp dir %q: %v", chartifyTempDir, err) | ||||||
|  | 			} | ||||||
|  | 			// Ensure it's removed on test completion
 | ||||||
|  | 			t.Cleanup(func() { | ||||||
|  | 				if err := os.RemoveAll(chartifyTempDir); err != nil { | ||||||
|  | 					t.Fatalf("unable to remove chartify temp dir %q: %v", chartifyTempDir, err) | ||||||
|  | 				} | ||||||
|  | 			}) | ||||||
|  | 
 | ||||||
|  | 			// If localDockerRegistry.enabled is set to `true`,
 | ||||||
|  | 			// run the docker registry v2 and push the test charts to the registry
 | ||||||
|  | 			// so that it can be accessed by helm and helmfile as a oci registry based chart repository.
 | ||||||
|  | 			if config.LocalDockerRegistry.Enabled { | ||||||
|  | 				containerName := "helmfile_docker_registry" | ||||||
|  | 
 | ||||||
|  | 				hostPort := config.LocalDockerRegistry.Port | ||||||
|  | 				if hostPort < 0 { | ||||||
|  | 					hostPort = 5000 | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				execDocker(t, "run", "-d", "-p", fmt.Sprintf("%d:5000", hostPort), "--restart=always", "--name", containerName, "registry:2") | ||||||
|  | 				t.Cleanup(func() { | ||||||
|  | 					execDocker(t, "stop", containerName) | ||||||
|  | 					execDocker(t, "rm", containerName) | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 				// We helm-package and helm-push every test chart saved in the ./testdata/charts directory
 | ||||||
|  | 				// to the local registry, so that they can be accessed by helmfile and helm invoked while testing.
 | ||||||
|  | 				charts, err := os.ReadDir(chartsDir) | ||||||
|  | 				require.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 				for _, c := range charts { | ||||||
|  | 					chartPath := filepath.Join(chartsDir, c.Name()) | ||||||
|  | 					if !c.IsDir() { | ||||||
|  | 						t.Fatalf("%s is not a directory", c) | ||||||
|  | 					} | ||||||
|  | 					tgzFile := execHelmPackage(t, chartPath) | ||||||
|  | 					_ = execHelm(t, "push", tgzFile, fmt.Sprintf("oci://localhost:%d/myrepo", hostPort)) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			inputFile := filepath.Join(testdataDir, name, "input.yaml") | ||||||
|  | 			outputFile := filepath.Join(testdataDir, name, "output.yaml") | ||||||
| 
 | 
 | ||||||
| 			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | 			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | ||||||
| 			defer cancel() | 			defer cancel() | ||||||
| 
 | 
 | ||||||
| 			cmd := exec.CommandContext(ctx, helmfileBin, "-f", inputFile, "build") | 			args := []string{"-f", inputFile} | ||||||
|  | 			args = append(args, helmfileArgs...) | ||||||
|  | 			cmd := exec.CommandContext(ctx, helmfileBin, args...) | ||||||
| 			got, err := cmd.CombinedOutput() | 			got, err := cmd.CombinedOutput() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				t.Logf("%s", string(got)) | 				t.Logf("Output from %v: %s", args, string(got)) | ||||||
| 			} | 			} | ||||||
| 			require.NoError(t, err) |  | ||||||
| 
 | 
 | ||||||
| 			require.Equal(t, string(want), string(got)) | 			gotStr := string(got) | ||||||
|  | 			gotStr = strings.ReplaceAll(gotStr, fmt.Sprintf("chart=%s", wd), "chart=$WD") | ||||||
|  | 
 | ||||||
|  | 			require.NoError(t, err, "Unable to run helmfile with args %v", args) | ||||||
|  | 
 | ||||||
|  | 			if stat, _ := os.Stat(outputFile); stat != nil { | ||||||
|  | 				want, err := os.ReadFile(outputFile) | ||||||
|  | 				require.NoError(t, err) | ||||||
|  | 				require.Equal(t, string(want), gotStr) | ||||||
|  | 			} else { | ||||||
|  | 				// To update the test golden image(output.yaml), just remove it and rerun this test.
 | ||||||
|  | 				// We automatically capture the output to `output.yaml` in the test case directory
 | ||||||
|  | 				// when the output.yaml doesn't exist.
 | ||||||
|  | 				require.NoError(t, os.WriteFile(outputFile, []byte(gotStr), 0664)) | ||||||
|  | 			} | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func execDocker(t *testing.T, args ...string) { | ||||||
|  | 	t.Helper() | ||||||
|  | 
 | ||||||
|  | 	docker := exec.Command("docker", args...) | ||||||
|  | 	out, err := docker.CombinedOutput() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Logf("Docker output: %s", string(out)) | ||||||
|  | 		t.Fatalf("Unable to run docker: %v", err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func execHelmPackage(t *testing.T, localChart string) string { | ||||||
|  | 	t.Helper() | ||||||
|  | 
 | ||||||
|  | 	out := execHelm(t, "package", localChart) | ||||||
|  | 	msg := strings.Split(out, " ") | ||||||
|  | 	tgzAbsPath := msg[len(msg)-1] | ||||||
|  | 	return strings.TrimSpace(tgzAbsPath) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func execHelm(t *testing.T, args ...string) string { | ||||||
|  | 	t.Helper() | ||||||
|  | 
 | ||||||
|  | 	cmd := []string{"helm"} | ||||||
|  | 	cmd = append(cmd, args...) | ||||||
|  | 	c := strings.Join(cmd, " ") | ||||||
|  | 	docker := exec.Command("helm", args...) | ||||||
|  | 	out, err := docker.CombinedOutput() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Logf("%s: %s", c, string(out)) | ||||||
|  | 		t.Fatalf("Unable to run %s: %v", c, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return string(out) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | *.tgz | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | # Patterns to ignore when building packages. | ||||||
|  | # This supports shell glob matching, relative path matching, and | ||||||
|  | # negation (prefixed with !). Only one pattern per line. | ||||||
|  | .DS_Store | ||||||
|  | # Common VCS dirs | ||||||
|  | .git/ | ||||||
|  | .gitignore | ||||||
|  | .bzr/ | ||||||
|  | .bzrignore | ||||||
|  | .hg/ | ||||||
|  | .hgignore | ||||||
|  | .svn/ | ||||||
|  | # Common backup files | ||||||
|  | *.swp | ||||||
|  | *.bak | ||||||
|  | *.tmp | ||||||
|  | *.orig | ||||||
|  | *~ | ||||||
|  | # Various IDEs | ||||||
|  | .project | ||||||
|  | .idea/ | ||||||
|  | *.tmproj | ||||||
|  | .vscode/ | ||||||
|  | @ -0,0 +1,6 @@ | ||||||
|  | apiVersion: v2 | ||||||
|  | name: raw | ||||||
|  | description: A Helm chart for Kubernetes | ||||||
|  | type: application | ||||||
|  | version: 0.1.0 | ||||||
|  | appVersion: "1.16.0" | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | You should be able to test pushing this chart to a local registry with: | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | $ helm package . | ||||||
|  | Successfully packaged chart and saved it to: /home/mumoshu/p/helmfile/test/e2e/template/helmfile/testdata/charts/raw/raw-0.1.0.tgz | ||||||
|  | 
 | ||||||
|  | $ helm push raw-0.1.0.tgz oci://localhost:5000/myrepo/raw | ||||||
|  | Pushed: localhost:5000/myrepo/raw/raw:0.1.0 | ||||||
|  | Digest: sha256:9b7c9633b519b024fdbec1db795bc2dd8b0009149135908a3aafc55280146ad9 | ||||||
|  | ``` | ||||||
|  | @ -0,0 +1,6 @@ | ||||||
|  | {{- range $i, $r := $.Values.templates }} | ||||||
|  | {{- if gt $i 0 }} | ||||||
|  | --- | ||||||
|  | {{- end }} | ||||||
|  | {{- (tpl $r $) }} | ||||||
|  | {{- end }} | ||||||
|  | @ -0,0 +1,48 @@ | ||||||
|  | templates: [] | ||||||
|  | 
 | ||||||
|  | ## | ||||||
|  | ## Example: Uncomment the below and run `helm template ./`: | ||||||
|  | ## | ||||||
|  | # | ||||||
|  | # templates: | ||||||
|  | # - | | ||||||
|  | #   apiVersion: v1 | ||||||
|  | #   kind: ConfigMap | ||||||
|  | #   metadata: | ||||||
|  | #     name: {{ .Release.Name }}-1 | ||||||
|  | #     namespace: {{ .Release.Namespace }} | ||||||
|  | #   data: | ||||||
|  | #     foo: {{ .Values.foo }} | ||||||
|  | # - | | ||||||
|  | #   apiVersion: v1 | ||||||
|  | #   kind: ConfigMap | ||||||
|  | #   metadata: | ||||||
|  | #     name: {{ .Release.Name }}-2 | ||||||
|  | #     namespace: {{ .Release.Namespace }} | ||||||
|  | #   data: | ||||||
|  | #     foo: {{ .Values.foo }} | ||||||
|  | # values: | ||||||
|  | #   foo: FOO | ||||||
|  | # | ||||||
|  | ## | ||||||
|  | ## Expected Output: | ||||||
|  | ## | ||||||
|  | # | ||||||
|  | # --- | ||||||
|  | # # Source: raw/templates/resources.yaml | ||||||
|  | # apiVersion: v1 | ||||||
|  | # kind: ConfigMap | ||||||
|  | # metadata: | ||||||
|  | #   name: release-name-1 | ||||||
|  | #   namespace: default | ||||||
|  | # data: | ||||||
|  | #   foo: | ||||||
|  | # --- | ||||||
|  | # # Source: raw/templates/resources.yaml | ||||||
|  | # apiVersion: v1 | ||||||
|  | # kind: ConfigMap | ||||||
|  | # metadata: | ||||||
|  | #   name: release-name-2 | ||||||
|  | #   namespace: default | ||||||
|  | # data: | ||||||
|  | #   foo: | ||||||
|  | @ -0,0 +1,6 @@ | ||||||
|  | localDockerRegistry: | ||||||
|  |   enabled: true | ||||||
|  |   port: 5000 | ||||||
|  | chartifyTempDir: temp1 | ||||||
|  | helmfileArgs: | ||||||
|  | - template | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | releases: | ||||||
|  | - name: foo | ||||||
|  |   chart: ../../charts/raw | ||||||
|  |   values: | ||||||
|  |   - templates: | ||||||
|  |     - | | ||||||
|  |       apiVersion: v1 | ||||||
|  |       kind: ConfigMap | ||||||
|  |       metadata: | ||||||
|  |         name: {{`{{ .Release.Name }}`}}-1 | ||||||
|  |         namespace: {{`{{ .Release.Namespace }}`}} | ||||||
|  |       data: | ||||||
|  |         foo: FOO | ||||||
|  |     dep: | ||||||
|  |       templates: | ||||||
|  |       - | | ||||||
|  |         apiVersion: v1 | ||||||
|  |         kind: ConfigMap | ||||||
|  |         metadata: | ||||||
|  |           name: {{`{{ .Release.Name }}`}}-2 | ||||||
|  |           namespace: {{`{{ .Release.Namespace }}`}} | ||||||
|  |         data: | ||||||
|  |           bar: BAR | ||||||
|  |   dependencies: | ||||||
|  |   - alias: dep | ||||||
|  |     chart: oci://localhost:5000/myrepo/raw | ||||||
|  |     version: 0.1.0 | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | Building dependency release=foo, chart=$WD/temp1/foo | ||||||
|  | Templating release=foo, chart=$WD/temp1/foo | ||||||
|  | --- | ||||||
|  | # Source: raw/templates/charts/dep/templates/resources.yaml | ||||||
|  | # Source: raw/charts/dep/templates/resources.yaml | ||||||
|  | apiVersion: v1 | ||||||
|  | kind: ConfigMap | ||||||
|  | metadata: | ||||||
|  |   name: foo-2 | ||||||
|  |   namespace: default | ||||||
|  | data: | ||||||
|  |   bar: BAR | ||||||
|  | --- | ||||||
|  | # Source: raw/templates/resources.yaml | ||||||
|  | # Source: raw/templates/resources.yaml | ||||||
|  | apiVersion: v1 | ||||||
|  | kind: ConfigMap | ||||||
|  | metadata: | ||||||
|  |   name: foo-1 | ||||||
|  |   namespace: default | ||||||
|  | data: | ||||||
|  |   foo: FOO | ||||||
|  | 
 | ||||||
		Loading…
	
		Reference in New Issue