internal/helm/upgrade_test.go
Rama main · réplica de lectura del repositorio original.
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")
}
})
}
}