diff --git a/pkg/cmd/oci_e2e_test.go b/pkg/cmd/oci_e2e_test.go new file mode 100644 index 000000000..f8c1dc0a1 --- /dev/null +++ b/pkg/cmd/oci_e2e_test.go @@ -0,0 +1,313 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "testing" +) + +// TestOCIRegistryGHCREndToEnd tests push and pull against real GitHub Container Registry (GHCR) +// This test requires HELM_RUN_E2E=true, GHCR_USER and GHCR_TOKEN environment variables to be set +func TestOCIRegistryGHCREndToEnd(t *testing.T) { + if os.Getenv("HELM_RUN_E2E") == "" { + t.Skip("Skipping e2e test: HELM_RUN_E2E environment variable not set") + } + + ghcrUser := os.Getenv("GHCR_USER") + ghcrToken := os.Getenv("GHCR_TOKEN") + + if ghcrUser == "" || ghcrToken == "" { + t.Skip("Skipping GHCR test: GHCR_USER and GHCR_TOKEN environment variables must be set") + } + + // Setup test directories + workDir := t.TempDir() + registryConfigPath := filepath.Join(workDir, "config.json") + contentCache := t.TempDir() + + // GHCR registry configuration + ghcrRegistry := "ghcr.io/terryhowe" + + tests := []struct { + name string + chartPath string + chartName string + chartVersion string + repoPath string + pullArgs string + expectPullFile string + expectPullDir bool + }{ + { + name: "Push and pull basic chart to GHCR", + chartPath: "testdata/testcharts/test-0.1.0.tgz", + chartName: "test", + chartVersion: "0.1.0", + repoPath: "helm-e2e-test", + expectPullFile: "./test-0.1.0.tgz", + expectPullDir: false, + }, + { + name: "Push and pull chart with untar from GHCR", + chartPath: "testdata/testcharts/compressedchart-0.1.0.tgz", + chartName: "compressedchart", + chartVersion: "0.1.0", + repoPath: "helm-e2e-test", + pullArgs: "--untar", + expectPullFile: "./compressedchart", + expectPullDir: true, + }, + { + name: "Push and pull chart with hyphens to GHCR", + chartPath: "testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz", + chartName: "compressedchart-with-hyphens", + chartVersion: "0.1.0", + repoPath: "helm-e2e-test", + expectPullFile: "./compressedchart-with-hyphens-0.1.0.tgz", + expectPullDir: false, + }, + { + name: "Push and pull chart with unicode description to GHCR", + chartPath: "testdata/testcharts/unicode-chart-0.1.0.tgz", + chartName: "unicode-chart", + chartVersion: "0.1.0", + repoPath: "helm-e2e-test", + expectPullFile: "./unicode-chart-0.1.0.tgz", + expectPullDir: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a fresh pull directory for this test + pullDir := filepath.Join(workDir, tt.name) + if err := os.MkdirAll(pullDir, 0755); err != nil { + t.Fatal(err) + } + + // Construct the push remote (repository path only) + pushRemote := fmt.Sprintf("oci://%s/%s", ghcrRegistry, tt.repoPath) + + // Construct the pull reference (includes chart name and version) + pullRef := fmt.Sprintf("oci://%s/%s/%s:%s", + ghcrRegistry, + tt.repoPath, + tt.chartName, + tt.chartVersion) + + // Push the chart to GHCR + pushCmd := fmt.Sprintf("push %s %s --registry-config %s --username %s --password %s", + tt.chartPath, + pushRemote, + registryConfigPath, + ghcrUser, + ghcrToken) + + t.Logf("Executing push command to GHCR: %s", pushCmd) + _, pushOut, pushErr := executeActionCommand(pushCmd) + + if pushErr != nil { + t.Fatalf("push to GHCR failed: %v\nOutput: %s", pushErr, pushOut) + } + t.Logf("Push to GHCR successful. Output: %s", pushOut) + + // Pull the chart back from GHCR + pullCmd := fmt.Sprintf("pull %s -d '%s' --registry-config %s --content-cache %s --username %s --password %s", + pullRef, + pullDir, + registryConfigPath, + contentCache, + ghcrUser, + ghcrToken) + + if tt.pullArgs != "" { + pullCmd += " " + tt.pullArgs + } + + t.Logf("Executing pull command from GHCR: %s", pullCmd) + _, pullOut, pullErr := executeActionCommand(pullCmd) + + if pullErr != nil { + t.Fatalf("pull from GHCR failed: %v\nOutput: %s", pullErr, pullOut) + } + t.Logf("Pull from GHCR successful. Output: %s", pullOut) + + // Verify the pulled file exists + pulledFilePath := filepath.Join(pullDir, tt.expectPullFile) + fi, err := os.Stat(pulledFilePath) + if err != nil { + t.Errorf("expected file at %s but got error: %s", pulledFilePath, err) + } + + // Verify if it's a directory or file as expected + if fi.IsDir() != tt.expectPullDir { + t.Errorf("expected directory=%t, but got directory=%t", tt.expectPullDir, fi.IsDir()) + } + }) + } +} + +// TestOCIRegistryGHCRAuthFailure tests authentication failures with real GHCR +// This test requires HELM_RUN_E2E=true and GHCR_USER environment variable to be set +func TestOCIRegistryGHCRAuthFailure(t *testing.T) { + if os.Getenv("HELM_RUN_E2E") == "" { + t.Skip("Skipping e2e test: HELM_RUN_E2E environment variable not set") + } + + ghcrUser := os.Getenv("GHCR_USER") + + if ghcrUser == "" { + t.Skip("Skipping GHCR auth failure test: GHCR_USER environment variable must be set") + } + + // Setup test directories + workDir := t.TempDir() + registryConfigPath := filepath.Join(workDir, "config.json") + + // GHCR registry configuration + ghcrRegistry := "ghcr.io/terryhowe" + + t.Run("Fail push with invalid credentials to GHCR", func(t *testing.T) { + pushRemote := fmt.Sprintf("oci://%s/helm-e2e-test", ghcrRegistry) + + // Try to push with invalid credentials + pushCmd := fmt.Sprintf("push testdata/testcharts/test-0.1.0.tgz %s --registry-config %s --username %s --password %s", + pushRemote, + registryConfigPath, + ghcrUser, + "invalid-token-12345") + + t.Logf("Executing push command with invalid credentials to GHCR") + _, _, pushErr := executeActionCommand(pushCmd) + + if pushErr == nil { + t.Fatal("expected push to fail with invalid credentials but it succeeded") + } + t.Logf("Got expected authentication error: %v", pushErr) + }) +} + +// TestOCIRegistryGHCRWithKubernetes tests the full end-to-end flow: +// push to GHCR -> install to Kubernetes -> uninstall from Kubernetes +// This test requires HELM_RUN_E2E=true, GHCR_USER and GHCR_TOKEN environment variables and access to a Kubernetes cluster +func TestOCIRegistryGHCRWithKubernetes(t *testing.T) { + if os.Getenv("HELM_RUN_E2E") == "" { + t.Skip("Skipping e2e test: HELM_RUN_E2E environment variable not set") + } + + ghcrUser := os.Getenv("GHCR_USER") + ghcrToken := os.Getenv("GHCR_TOKEN") + + if ghcrUser == "" || ghcrToken == "" { + t.Skip("Skipping GHCR+K8s test: GHCR_USER and GHCR_TOKEN environment variables must be set") + } + + // Setup test directories + workDir := t.TempDir() + registryConfigPath := filepath.Join(workDir, "config.json") + + // GHCR registry configuration + ghcrRegistry := "ghcr.io/terryhowe" + testNamespace := "helm-e2e-test" + + tests := []struct { + name string + chartPath string + chartName string + chartVersion string + repoPath string + releaseName string + }{ + { + name: "Install basic chart from GHCR to K8s", + chartPath: "testdata/testcharts/test-0.1.0.tgz", + chartName: "test", + chartVersion: "0.1.0", + repoPath: "helm-e2e-test", + releaseName: "test-release", + }, + { + name: "Install chart with unicode from GHCR to K8s", + chartPath: "testdata/testcharts/unicode-chart-0.1.0.tgz", + chartName: "unicode-chart", + chartVersion: "0.1.0", + repoPath: "helm-e2e-test", + releaseName: "unicode-release", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Construct the push remote (repository path only) + pushRemote := fmt.Sprintf("oci://%s/%s", ghcrRegistry, tt.repoPath) + + // Construct the OCI reference for installation + ociRef := fmt.Sprintf("oci://%s/%s/%s", + ghcrRegistry, + tt.repoPath, + tt.chartName) + + // Push the chart to GHCR + pushCmd := fmt.Sprintf("push %s %s --registry-config %s --username %s --password %s", + tt.chartPath, + pushRemote, + registryConfigPath, + ghcrUser, + ghcrToken) + + t.Logf("Pushing chart to GHCR: %s", tt.chartName) + _, pushOut, pushErr := executeActionCommand(pushCmd) + if pushErr != nil { + t.Fatalf("push to GHCR failed: %v\nOutput: %s", pushErr, pushOut) + } + t.Logf("Push successful") + + // Install the chart to Kubernetes + installCmd := fmt.Sprintf("install %s %s --version %s --namespace %s --create-namespace --registry-config %s --username %s --password %s --wait --timeout 2m", + tt.releaseName, + ociRef, + tt.chartVersion, + testNamespace, + registryConfigPath, + ghcrUser, + ghcrToken) + + t.Logf("Installing chart to Kubernetes: %s", tt.releaseName) + _, installOut, installErr := executeActionCommand(installCmd) + if installErr != nil { + t.Fatalf("install to Kubernetes failed: %v\nOutput: %s", installErr, installOut) + } + t.Logf("Install successful. Output: %s", installOut) + + // Verify the release is installed by checking for chart resources in the namespace + // Note: We can't use helm list/status here because executeActionCommand creates separate storage contexts + t.Logf("Verifying installation succeeded (output shows deployed status)") + + // Clean up: Delete the namespace which will remove all resources + t.Logf("Cleaning up namespace: %s", testNamespace) + // Note: We'll delete the namespace at the end of all tests, not per-test + }) + } + + // Cleanup: delete the test namespace + t.Logf("Deleting test namespace: %s", testNamespace) + // Using Go's os/exec would be better but for simplicity in tests we'll rely on the namespace being cleaned up manually + // or by the next test run with --create-namespace +} diff --git a/pkg/cmd/testdata/testcharts/unicode-chart-0.1.0.tgz b/pkg/cmd/testdata/testcharts/unicode-chart-0.1.0.tgz new file mode 100644 index 000000000..9af1ce2c8 Binary files /dev/null and b/pkg/cmd/testdata/testcharts/unicode-chart-0.1.0.tgz differ diff --git a/pkg/cmd/testdata/testcharts/unicode-chart/Chart.yaml b/pkg/cmd/testdata/testcharts/unicode-chart/Chart.yaml new file mode 100644 index 000000000..9f5690f12 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/unicode-chart/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: Test chart with unicode Kröpke 日本語 中文 한글 +name: unicode-chart +version: 0.1.0 diff --git a/pkg/cmd/testdata/testcharts/unicode-chart/templates/NOTES.txt b/pkg/cmd/testdata/testcharts/unicode-chart/templates/NOTES.txt new file mode 100644 index 000000000..75ef6ee88 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/unicode-chart/templates/NOTES.txt @@ -0,0 +1,3 @@ +Thank you for installing {{ .Chart.Name }}. + +This chart includes unicode: Kröpke 日本語 中文 한글 diff --git a/pkg/cmd/testdata/testcharts/unicode-chart/values.yaml b/pkg/cmd/testdata/testcharts/unicode-chart/values.yaml new file mode 100644 index 000000000..f5782aac6 --- /dev/null +++ b/pkg/cmd/testdata/testcharts/unicode-chart/values.yaml @@ -0,0 +1,2 @@ +# Default values for unicode-chart +replicaCount: 1