mirror of https://github.com/helm/helm
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
247 lines
8.3 KiB
247 lines
8.3 KiB
//go:build e2e
|
|
|
|
/*
|
|
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 e2e contains end-to-end tests that exercise a real helm binary
|
|
// against real external systems (an OCI registry, and optionally a Kubernetes
|
|
// cluster).
|
|
//
|
|
// These tests are excluded from the normal build by the "e2e" build tag and
|
|
// are never run by `make test`. Run them with `make test-e2e` after exporting
|
|
// the configuration described in the package README.
|
|
package e2e
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Environment variables used to configure the end-to-end tests.
|
|
const (
|
|
// envRegistry is the registry namespace charts are pushed to, without a
|
|
// scheme, e.g. "ghcr.io/example-org" or "localhost:5000/charts".
|
|
envRegistry = "HELM_E2E_REGISTRY"
|
|
// envUsername is the username used to authenticate to envRegistry.
|
|
envUsername = "HELM_E2E_USERNAME"
|
|
// envPassword is the password or token used to authenticate to envRegistry.
|
|
envPassword = "HELM_E2E_PASSWORD"
|
|
// envHelmBin points at a prebuilt helm binary. When unset, one is built
|
|
// from the working tree.
|
|
envHelmBin = "HELM_E2E_BIN"
|
|
// envPlainHTTP requests plain HTTP for registries without TLS, which is
|
|
// useful when testing against a local registry.
|
|
envPlainHTTP = "HELM_E2E_PLAIN_HTTP"
|
|
// envKubernetes opts in to the tests that install charts into a real
|
|
// Kubernetes cluster using the ambient kubeconfig.
|
|
envKubernetes = "HELM_E2E_KUBERNETES"
|
|
// envNamespace is the namespace the Kubernetes tests install into.
|
|
envNamespace = "HELM_E2E_NAMESPACE"
|
|
)
|
|
|
|
// harness carries the resolved configuration shared by the end-to-end tests.
|
|
type harness struct {
|
|
// helmBin is an absolute path to the helm binary under test.
|
|
helmBin string
|
|
// registry is the registry namespace charts are pushed to.
|
|
registry string
|
|
// username and password authenticate to the registry. They are only ever
|
|
// passed to helm over stdin and are never logged.
|
|
username string
|
|
password string
|
|
// plainHTTP is true when the registry should be reached over HTTP.
|
|
plainHTTP bool
|
|
// configPath is the registry credential file used for this run.
|
|
configPath string
|
|
// runID isolates the repositories written by a single test run so that
|
|
// concurrent or repeated runs do not collide.
|
|
runID string
|
|
}
|
|
|
|
// newHarness resolves the end-to-end configuration, failing the test when a
|
|
// required value is missing. Because the "e2e" build tag is an explicit opt-in,
|
|
// a missing setting is a configuration error rather than a reason to skip.
|
|
func newHarness(t *testing.T) *harness {
|
|
t.Helper()
|
|
|
|
h := &harness{
|
|
registry: requireEnv(t, envRegistry),
|
|
username: requireEnv(t, envUsername),
|
|
password: requireEnv(t, envPassword),
|
|
plainHTTP: os.Getenv(envPlainHTTP) != "",
|
|
helmBin: helmBinary(t),
|
|
runID: runID(t),
|
|
}
|
|
return h
|
|
}
|
|
|
|
// requireEnv returns the value of the named environment variable, failing the
|
|
// test with an actionable message when it is unset.
|
|
func requireEnv(t *testing.T, name string) string {
|
|
t.Helper()
|
|
v := os.Getenv(name)
|
|
if v == "" {
|
|
t.Fatalf("%s must be set to run the end-to-end tests; see test/e2e/README.md", name)
|
|
}
|
|
return v
|
|
}
|
|
|
|
// helmBinary returns the helm binary to exercise, building one from the
|
|
// working tree when HELM_E2E_BIN is not set.
|
|
func helmBinary(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
if bin := os.Getenv(envHelmBin); bin != "" {
|
|
abs, err := filepath.Abs(bin)
|
|
if err != nil {
|
|
t.Fatalf("resolving %s=%q: %v", envHelmBin, bin, err)
|
|
}
|
|
if _, err := os.Stat(abs); err != nil {
|
|
t.Fatalf("%s=%q is not usable: %v", envHelmBin, bin, err)
|
|
}
|
|
return abs
|
|
}
|
|
|
|
bin := filepath.Join(t.TempDir(), "helm")
|
|
build := exec.Command("go", "build", "-o", bin, "helm.sh/helm/v4/cmd/helm")
|
|
if out, err := build.CombinedOutput(); err != nil {
|
|
t.Fatalf("building helm: %v\n%s", err, out)
|
|
}
|
|
return bin
|
|
}
|
|
|
|
// runID returns a short random identifier unique to this test run.
|
|
func runID(t *testing.T) string {
|
|
t.Helper()
|
|
b := make([]byte, 4)
|
|
if _, err := rand.Read(b); err != nil {
|
|
t.Fatalf("generating run id: %v", err)
|
|
}
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
// registryHost returns the host portion of the configured registry, which is
|
|
// what `helm registry login` expects.
|
|
func (h *harness) registryHost() string {
|
|
host, _, _ := strings.Cut(h.registry, "/")
|
|
return host
|
|
}
|
|
|
|
// repo returns an isolated repository path for this run, so that a shared
|
|
// registry namespace can serve many runs without interference.
|
|
func (h *harness) repo(name string) string {
|
|
return fmt.Sprintf("%s/%s-%s", h.registry, name, h.runID)
|
|
}
|
|
|
|
// ref returns a full OCI reference for a chart within an isolated repository.
|
|
func (h *harness) ref(repo, chart, version string) string {
|
|
if version == "" {
|
|
return fmt.Sprintf("oci://%s/%s", repo, chart)
|
|
}
|
|
return fmt.Sprintf("oci://%s/%s:%s", repo, chart, version)
|
|
}
|
|
|
|
// plainHTTPCommands are the helm subcommands that accept --plain-http. The
|
|
// flag is only appended for these so that the harness can still run commands
|
|
// such as `status` and `uninstall` against a plain HTTP registry.
|
|
var plainHTTPCommands = map[string]bool{"push": true, "pull": true, "install": true, "upgrade": true}
|
|
|
|
// helm runs the helm binary with the given arguments and returns its combined
|
|
// output. Arguments are logged, so callers must never pass a credential.
|
|
func (h *harness) helm(t *testing.T, args ...string) (string, error) {
|
|
t.Helper()
|
|
if h.plainHTTP && len(args) > 0 && plainHTTPCommands[args[0]] {
|
|
args = append(args, "--plain-http")
|
|
}
|
|
t.Logf("helm %s", strings.Join(args, " "))
|
|
cmd := exec.Command(h.helmBin, args...)
|
|
cmd.Env = append(os.Environ(), "HELM_REGISTRY_CONFIG="+h.registryConfig(t))
|
|
out, err := cmd.CombinedOutput()
|
|
return string(out), err
|
|
}
|
|
|
|
// mustHelm runs helm and fails the test if the command does not succeed.
|
|
func (h *harness) mustHelm(t *testing.T, args ...string) string {
|
|
t.Helper()
|
|
out, err := h.helm(t, args...)
|
|
if err != nil {
|
|
t.Fatalf("helm %s failed: %v\n%s", strings.Join(args, " "), err, out)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// registryConfig returns the path to this run's registry credential file. The
|
|
// file lives under a per-test temporary directory so credentials never leak
|
|
// into the developer's real helm configuration.
|
|
func (h *harness) registryConfig(t *testing.T) string {
|
|
t.Helper()
|
|
if h.configPath == "" {
|
|
h.configPath = filepath.Join(t.TempDir(), "registry-config.json")
|
|
}
|
|
return h.configPath
|
|
}
|
|
|
|
// login authenticates to the configured registry. The password is written to
|
|
// helm's stdin rather than passed as a flag so it cannot appear in process
|
|
// listings or test output.
|
|
func (h *harness) login(t *testing.T, password string) (string, error) {
|
|
t.Helper()
|
|
args := []string{"registry", "login", h.registryHost(), "--username", h.username, "--password-stdin"}
|
|
if h.plainHTTP {
|
|
args = append(args, "--plain-http")
|
|
}
|
|
t.Logf("helm %s", strings.Join(args, " "))
|
|
cmd := exec.Command(h.helmBin, args...)
|
|
cmd.Env = append(os.Environ(), "HELM_REGISTRY_CONFIG="+h.registryConfig(t))
|
|
cmd.Stdin = strings.NewReader(password)
|
|
out, err := cmd.CombinedOutput()
|
|
return string(out), err
|
|
}
|
|
|
|
// mustLogin authenticates with the configured credential and arranges for a
|
|
// logout once the test completes.
|
|
func (h *harness) mustLogin(t *testing.T) {
|
|
t.Helper()
|
|
out, err := h.login(t, h.password)
|
|
if err != nil {
|
|
t.Fatalf("registry login to %s failed: %v\n%s", h.registryHost(), err, out)
|
|
}
|
|
t.Cleanup(func() {
|
|
if out, err := h.helm(t, "registry", "logout", h.registryHost()); err != nil {
|
|
t.Logf("registry logout failed (ignored): %v\n%s", err, out)
|
|
}
|
|
})
|
|
}
|
|
|
|
// chart returns the path to a chart archive in this package's testdata.
|
|
func chart(t *testing.T, name string) string {
|
|
t.Helper()
|
|
path, err := filepath.Abs(filepath.Join("testdata", "testcharts", name))
|
|
if err != nil {
|
|
t.Fatalf("resolving chart %q: %v", name, err)
|
|
}
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("chart %q is missing: %v", name, err)
|
|
}
|
|
return path
|
|
}
|