Ctrl K
ComunidadGuías…

internal/helm/upgrade_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package helm

import (
	"context"
	"errors"
	"io"
	"io/fs"
	"os"
	"strings"
	"testing"

	corev1 "k8s.io/api/core/v1"
	k8sfake "k8s.io/client-go/kubernetes/fake"

	"github.com/sophotechlabs/spinoza/internal/api"
)

type valuesReadingRunner struct {
	stubRunner

	valuesPath string
	valuesSeen string
	valuesMode fs.FileMode
}

type valuesFileStub struct {
	name     string
	written  int
	writeErr error
	closeErr error
	closed   bool
}

func (f *valuesFileStub) Name() string {
	return f.name
}

func (f *valuesFileStub) WriteString(string) (int, error) {
	return f.written, f.writeErr
}

func (f *valuesFileStub) Close() error {
	f.closed = true
	return f.closeErr
}

func (v *valuesReadingRunner) Run(ctx context.Context, args, env []string) (string, error) {
	for i, arg := range args {
		if arg != "--values" {
			continue
		}
		v.valuesPath = args[i+1]
		body, err := os.ReadFile(args[i+1])
		if err != nil {
			return "", err
		}
		v.valuesSeen = string(body)
		info, statErr := os.Stat(args[i+1])
		if statErr != nil {
			return "", statErr
		}
		v.valuesMode = info.Mode()
	}
	return v.stubRunner.Run(ctx, args, env)
}

func upgradeSpec() UpgradeRequest {
	return UpgradeRequest{
		Namespace: "demo",
		Name:      "podinfo",
		Chart:     "podinfo",
		Version:   "6.10.0",
		RepoURL:   "https://charts.example.com",
		Values:    "replicaCount: 2\n",
	}
}

func TestUpgradeRunsHelmAgainstTheChosenRepo(t *testing.T) {
	runner := &valuesReadingRunner{}
	service := acting(t, runner, helmSecret(sampleRelease()))

	got, err := service.Upgrade(context.Background(), upgradeSpec())
	if err != nil {
		t.Fatalf("upgrade: %v", err)
	}

	args := strings.Join(runner.args[0], " ")
	//nolint:dupword // the release and the chart share a name
	want := "upgrade podinfo podinfo --namespace demo --version 6.10.0" +
		" --repo https://charts.example.com --values " + runner.valuesPath +
		" --kube-context kind-spinoza"
	if args != want {
		t.Fatalf("args = %q, want %q", args, want)
	}
	if got.Message != "upgraded podinfo to podinfo 6.10.0" {
		t.Fatalf("message = %q, want a plain summary when helm says nothing", got.Message)
	}
	if got.DryRun {
		t.Fatal("a real upgrade was marked as a dry run")
	}
}

func TestUpgradeWritesTheValuesToAPrivateFileAndRemovesIt(t *testing.T) {
	runner := &valuesReadingRunner{}
	service := acting(t, runner, helmSecret(sampleRelease()))

	_, err := service.Upgrade(context.Background(), upgradeSpec())
	if err != nil {
		t.Fatalf("upgrade: %v", err)
	}

	if runner.valuesSeen != "replicaCount: 2\n" {
		t.Fatalf("values = %q, want them written for helm to read", runner.valuesSeen)
	}
	if runner.valuesMode.Perm() != 0o600 {
		t.Fatalf("mode = %v, want 0600", runner.valuesMode.Perm())
	}
	_, statErr := os.Stat(runner.valuesPath)
	if !errors.Is(statErr, fs.ErrNotExist) {
		t.Fatalf("stat after upgrade = %v, want the values file removed", statErr)
	}
}

func TestUpgradePassesAnEmptyValuesFileForNoOverrides(t *testing.T) {
	runner := &valuesReadingRunner{}
	service := acting(t, runner, helmSecret(sampleRelease()))
	spec := upgradeSpec()
	spec.Values = ""

	_, err := service.Upgrade(context.Background(), spec)
	if err != nil {
		t.Fatalf("upgrade: %v", err)
	}

	if runner.valuesPath == "" {
		t.Fatal("no values file was passed; chart defaults must come from an explicit empty file")
	}
	if runner.valuesSeen != "" {
		t.Fatalf("values = %q, want an empty file", runner.valuesSeen)
	}
}

func TestAnIncompleteValuesFileIsRejectedAndRemoved(t *testing.T) {
	values := "replicaCount: 2\n"
	writeFailure := errors.New("disk full")
	closeFailure := errors.New("close failed")
	cases := []struct {
		name string
		file valuesFileStub
		want error
	}{
		{
			name: "write failure",
			file: valuesFileStub{written: 3, writeErr: writeFailure, closeErr: closeFailure},
			want: writeFailure,
		},
		{
			name: "short write",
			file: valuesFileStub{written: len(values) - 1},
			want: io.ErrShortWrite,
		},
		{
			name: "close failure",
			file: valuesFileStub{written: len(values), closeErr: closeFailure},
			want: closeFailure,
		},
	}
	for _, one := range cases {
		t.Run(one.name, func(t *testing.T) {
			file := one.file
			file.name = "values.yaml"
			removed := ""
			path, err := writeValuesWith(
				values,
				func() (temporaryValues, error) {
					return &file, nil
				},
				func(name string) error {
					removed = name
					return nil
				},
			)

			if !errors.Is(err, one.want) {
				t.Fatalf("error = %v, want %v", err, one.want)
			}
			if path != "" {
				t.Fatalf("path = %q, want no incomplete file", path)
			}
			if !file.closed {
				t.Fatal("the incomplete values file was not closed")
			}
			if removed != file.name {
				t.Fatalf("removed = %q, want %q", removed, file.name)
			}
		})
	}
}

func TestUpgradeBuildsAnOCIChartReference(t *testing.T) {
	runner := &valuesReadingRunner{}
	service := acting(t, runner, helmSecret(sampleRelease()))
	spec := upgradeSpec()
	spec.RepoURL = "oci://ghcr.io/acme/charts/"
	spec.OCI = true

	_, err := service.Upgrade(context.Background(), spec)
	if err != nil {
		t.Fatalf("upgrade: %v", err)
	}

	args := strings.Join(runner.args[0], " ")
	want := "upgrade podinfo oci://ghcr.io/acme/charts/podinfo --namespace demo --version 6.10.0" +
		" --values " + runner.valuesPath + " --kube-context kind-spinoza"
	if args != want {
		t.Fatalf("args = %q, want %q", args, want)
	}
}

func TestUpgradeDryRunAsksForAServerRender(t *testing.T) {
	runner := &valuesReadingRunner{}
	runner.out = `{"manifest":"kind: ConfigMap\n"}`
	service := acting(t, runner, helmSecret(sampleRelease()))
	spec := upgradeSpec()
	spec.DryRun = true

	got, err := service.Upgrade(context.Background(), spec)
	if err != nil {
		t.Fatalf("dry run: %v", err)
	}

	args := strings.Join(runner.args[0], " ")
	if !strings.Contains(args, "--dry-run=server --output json") {
		t.Fatalf("args = %q, want the server dry run asked for", args)
	}
	if !got.DryRun {
		t.Fatal("the result was not marked as a dry run")
	}
	if got.Manifest != "kind: ConfigMap\n" {
		t.Fatalf("manifest = %q, want the rendered manifest", got.Manifest)
	}
	if got.Message != "server render of podinfo 6.10.0" {
		t.Fatalf("message = %q", got.Message)
	}
}

func TestUpgradeDryRunReportsUnreadableHelmOutput(t *testing.T) {
	runner := &valuesReadingRunner{}
	runner.out = "not json"
	service := acting(t, runner, helmSecret(sampleRelease()))
	spec := upgradeSpec()
	spec.DryRun = true

	_, err := service.Upgrade(context.Background(), spec)

	if err == nil {
		t.Fatal("expected unreadable dry-run output to surface")
	}
	if !strings.Contains(err.Error(), "dry-run output") {
		t.Fatalf("error = %v, want it to name the dry-run output", err)
	}
}

func TestUpgradeKeepsHelmsOwnMessage(t *testing.T) {
	runner := &valuesReadingRunner{}
	runner.out = `Release "podinfo" has been upgraded. Happy Helming!`
	service := acting(t, runner, helmSecret(sampleRelease()))

	got, err := service.Upgrade(context.Background(), upgradeSpec())
	if err != nil {
		t.Fatalf("upgrade: %v", err)
	}

	if !strings.Contains(got.Message, "Happy Helming") {
		t.Fatalf("message = %q, want helm's own output", got.Message)
	}
}

func TestUpgradeTellsHelmWhichDriverHoldsTheRelease(t *testing.T) {
	runner := &valuesReadingRunner{}
	entry := &corev1.ConfigMap{
		Name:      "sh.helm.release.v1.podinfo.v1",
		Namespace: "demo",
		Labels:    map[string]string{"owner": "helm", "name": "podinfo", "version": "1"},
		Data:      map[string]string{releaseKey: "rubbish"},
	}
	client := k8sfake.NewClientset(entry)
	service := NewService(
		client,
		mirrorMeta(client),
		runner,
		nil,
		actionRepositories(),
		api.ContextRef{Name: "kind-spinoza"},
	)

	_, err := service.Upgrade(context.Background(), upgradeSpec())
	if err != nil {
		t.Fatalf("upgrade: %v", err)
	}

	env := strings.Join(runner.envs[0], " ")
	if env != "HELM_DRIVER=configmap" {
		t.Fatalf("env = %q, want the configmap driver named", env)
	}
}

func TestUpgradeRefusesAReleaseItCannotFind(t *testing.T) {
	runner := &valuesReadingRunner{}
	service := acting(t, runner)

	_, err := service.Upgrade(context.Background(), upgradeSpec())

	if !errors.Is(err, ErrNoRelease) {
		t.Fatalf("err = %v, want ErrNoRelease", err)
	}
	if len(runner.args) != 0 {
		t.Fatal("helm ran for a release that does not exist")
	}
}

func TestUpgradeValidatesItsInputBeforeRunningHelm(t *testing.T) {
	cases := []struct {
		name   string
		mutate func(spec *UpgradeRequest)
	}{
		{"flag-shaped release", func(spec *UpgradeRequest) { spec.Name = "--kubeconfig=/etc/shadow" }},
		{"flag-shaped chart", func(spec *UpgradeRequest) { spec.Chart = "--post-renderer" }},
		{"version that is not semver", func(spec *UpgradeRequest) { spec.Version = "latest" }},
		{"repo with a forbidden scheme", func(spec *UpgradeRequest) { spec.RepoURL = "ftp://charts.example.com" }},
		{"repo with no host", func(spec *UpgradeRequest) { spec.RepoURL = "https://" }},
		{"values that are not a mapping", func(spec *UpgradeRequest) { spec.Values = "- one\n- two\n" }},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			runner := &valuesReadingRunner{}
			service := acting(t, runner, helmSecret(sampleRelease()))
			spec := upgradeSpec()
			tc.mutate(&spec)

			_, err := service.Upgrade(context.Background(), spec)

			if err == nil {
				t.Fatal("expected the input to be refused")
			}
			if len(runner.args) != 0 {
				t.Fatal("helm ran on refused input")
			}
		})
	}
}