helm-controller/internal/reconcile/install_test.go

448 lines
14 KiB
Go

/*
Copyright 2022 The Flux 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 reconcile
import (
"context"
"errors"
"fmt"
"testing"
"time"
. "github.com/onsi/gomega"
"helm.sh/helm/v3/pkg/chart"
helmchartutil "helm.sh/helm/v3/pkg/chartutil"
helmrelease "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil"
helmstorage "helm.sh/helm/v3/pkg/storage"
helmdriver "helm.sh/helm/v3/pkg/storage/driver"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/record"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
v2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/helm-controller/internal/action"
"github.com/fluxcd/helm-controller/internal/chartutil"
"github.com/fluxcd/helm-controller/internal/digest"
"github.com/fluxcd/helm-controller/internal/release"
"github.com/fluxcd/helm-controller/internal/storage"
"github.com/fluxcd/helm-controller/internal/testutil"
)
func TestInstall_Reconcile(t *testing.T) {
tests := []struct {
name string
// driver allows for modifying the Helm storage driver.
driver func(driver helmdriver.Driver) helmdriver.Driver
// releases is the list of releases that are stored in the driver
// before install.
releases func(namespace string) []*helmrelease.Release
// chart to install.
chart *chart.Chart
// values to use during install.
values helmchartutil.Values
// spec modifies the HelmRelease object spec before install.
spec func(spec *v2.HelmReleaseSpec)
// status to configure on the HelmRelease object before install.
status func(releases []*helmrelease.Release) v2.HelmReleaseStatus
// wantErr is the error that is expected to be returned.
wantErr error
// expectedConditions are the conditions that are expected to be set on
// the HelmRelease after install.
expectConditions []metav1.Condition
// expectHistory is the expected History of the HelmRelease after
// install.
expectHistory func(releases []*helmrelease.Release) v2.Snapshots
// expectFailures is the expected Failures count of the HelmRelease.
expectFailures int64
// expectInstallFailures is the expected InstallFailures count of the
// HelmRelease.
expectInstallFailures int64
// expectUpgradeFailures is the expected UpgradeFailures count of the
// HelmRelease.
expectUpgradeFailures int64
}{
{
name: "install success",
chart: testutil.BuildChart(),
expectConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, v2.InstallSucceededReason,
"Helm install succeeded"),
*conditions.TrueCondition(v2.ReleasedCondition, v2.InstallSucceededReason,
"Helm install succeeded"),
},
expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
return v2.Snapshots{
release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
}
},
},
{
name: "install failure",
chart: testutil.BuildChart(testutil.ChartWithFailingHook()),
expectConditions: []metav1.Condition{
*conditions.FalseCondition(meta.ReadyCondition, v2.InstallFailedReason,
"failed post-install"),
*conditions.FalseCondition(v2.ReleasedCondition, v2.InstallFailedReason,
"failed post-install"),
},
expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
return v2.Snapshots{
release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
}
},
expectFailures: 1,
expectInstallFailures: 1,
},
{
name: "install failure without storage update",
driver: func(driver helmdriver.Driver) helmdriver.Driver {
return &storage.Failing{
Driver: driver,
CreateErr: fmt.Errorf("storage create error"),
}
},
chart: testutil.BuildChart(),
wantErr: fmt.Errorf("storage create error"),
expectConditions: []metav1.Condition{
*conditions.FalseCondition(meta.ReadyCondition, v2.InstallFailedReason,
"storage create error"),
*conditions.FalseCondition(v2.ReleasedCondition, v2.InstallFailedReason,
"storage create error"),
},
expectFailures: 1,
expectInstallFailures: 0,
},
{
name: "install with current",
releases: func(namespace string) []*helmrelease.Release {
return []*helmrelease.Release{
testutil.BuildRelease(&helmrelease.MockReleaseOptions{
Name: mockReleaseName,
Namespace: namespace,
Chart: testutil.BuildChart(),
Version: 1,
Status: helmrelease.StatusUninstalled,
}),
}
},
spec: func(spec *v2.HelmReleaseSpec) {
spec.Install = &v2.Install{
Replace: true,
}
},
status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
return v2.HelmReleaseStatus{
History: v2.Snapshots{
release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
},
}
},
chart: testutil.BuildChart(),
expectConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, v2.InstallSucceededReason,
"Helm install succeeded"),
*conditions.TrueCondition(v2.ReleasedCondition, v2.InstallSucceededReason,
"Helm install succeeded"),
},
expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
return v2.Snapshots{
release.ObservedToSnapshot(release.ObserveRelease(releases[1])),
}
},
},
{
name: "install with stale current",
status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
return v2.HelmReleaseStatus{
History: v2.Snapshots{
release.ObservedToSnapshot(release.ObserveRelease(testutil.BuildRelease(&helmrelease.MockReleaseOptions{
Name: mockReleaseName,
Namespace: "other",
Version: 1,
Status: helmrelease.StatusUninstalled,
Chart: testutil.BuildChart(),
}))),
},
}
},
chart: testutil.BuildChart(),
expectConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, v2.InstallSucceededReason,
"Helm install succeeded"),
*conditions.TrueCondition(v2.ReleasedCondition, v2.InstallSucceededReason,
"Helm install succeeded"),
},
expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
return v2.Snapshots{
release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
}
},
},
{
name: "install with stale conditions",
status: func(releases []*helmrelease.Release) v2.HelmReleaseStatus {
return v2.HelmReleaseStatus{
Conditions: []metav1.Condition{
*conditions.FalseCondition(v2.TestSuccessCondition, v2.TestFailedReason, ""),
*conditions.TrueCondition(v2.RemediatedCondition, v2.UninstallSucceededReason, ""),
},
}
},
chart: testutil.BuildChart(),
expectConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReadyCondition, v2.InstallSucceededReason,
"Helm install succeeded"),
*conditions.TrueCondition(v2.ReleasedCondition, v2.InstallSucceededReason,
"Helm install succeeded"),
},
expectHistory: func(releases []*helmrelease.Release) v2.Snapshots {
return v2.Snapshots{
release.ObservedToSnapshot(release.ObserveRelease(releases[0])),
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
namedNS, err := testEnv.CreateNamespace(context.TODO(), mockReleaseNamespace)
g.Expect(err).NotTo(HaveOccurred())
t.Cleanup(func() {
_ = testEnv.Delete(context.TODO(), namedNS)
})
releaseNamespace := namedNS.Name
var releases []*helmrelease.Release
if tt.releases != nil {
releases = tt.releases(releaseNamespace)
releaseutil.SortByRevision(releases)
}
obj := &v2.HelmRelease{
Spec: v2.HelmReleaseSpec{
ReleaseName: mockReleaseName,
TargetNamespace: releaseNamespace,
StorageNamespace: releaseNamespace,
Timeout: &metav1.Duration{Duration: 100 * time.Millisecond},
},
}
if tt.spec != nil {
tt.spec(&obj.Spec)
}
if tt.status != nil {
obj.Status = tt.status(releases)
}
getter, err := RESTClientGetterFromManager(testEnv.Manager, obj.GetReleaseNamespace())
g.Expect(err).ToNot(HaveOccurred())
cfg, err := action.NewConfigFactory(getter,
action.WithStorage(action.DefaultStorageDriver, obj.GetStorageNamespace()),
)
g.Expect(err).ToNot(HaveOccurred())
store := helmstorage.Init(cfg.Driver)
for _, r := range releases {
g.Expect(store.Create(r)).To(Succeed())
}
if tt.driver != nil {
cfg.Driver = tt.driver(cfg.Driver)
}
recorder := new(record.FakeRecorder)
got := (NewInstall(cfg, recorder)).Reconcile(context.TODO(), &Request{
Object: obj,
Chart: tt.chart,
Values: tt.values,
})
if tt.wantErr != nil {
g.Expect(got).To(Equal(tt.wantErr))
} else {
g.Expect(got).ToNot(HaveOccurred())
}
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.expectConditions))
releases, _ = store.History(mockReleaseName)
releaseutil.SortByRevision(releases)
if tt.expectHistory != nil {
g.Expect(obj.Status.History).To(testutil.Equal(tt.expectHistory(releases)))
} else {
g.Expect(obj.Status.History).To(BeEmpty(), "expected history to be empty")
}
g.Expect(obj.Status.Failures).To(Equal(tt.expectFailures))
g.Expect(obj.Status.InstallFailures).To(Equal(tt.expectInstallFailures))
g.Expect(obj.Status.UpgradeFailures).To(Equal(tt.expectUpgradeFailures))
})
}
}
func TestInstall_failure(t *testing.T) {
var (
obj = &v2.HelmRelease{
Spec: v2.HelmReleaseSpec{
ReleaseName: mockReleaseName,
TargetNamespace: mockReleaseNamespace,
},
Status: v2.HelmReleaseStatus{
LastAttemptedRevisionDigest: "sha256:1234567890",
},
}
chrt = testutil.BuildChart()
err = errors.New("installation error")
)
t.Run("records failure", func(t *testing.T) {
g := NewWithT(t)
recorder := testutil.NewFakeRecorder(10, false)
r := &Install{
eventRecorder: recorder,
}
req := &Request{Object: obj.DeepCopy(), Chart: chrt, Values: map[string]interface{}{"foo": "bar"}}
r.failure(req, nil, err)
expectMsg := fmt.Sprintf(fmtInstallFailure, mockReleaseNamespace, mockReleaseName, chrt.Name(),
chrt.Metadata.Version, err.Error())
g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
*conditions.FalseCondition(v2.ReleasedCondition, v2.InstallFailedReason, expectMsg),
}))
g.Expect(req.Object.Status.Failures).To(Equal(int64(1)))
g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
{
Type: corev1.EventTypeWarning,
Reason: v2.InstallFailedReason,
Message: expectMsg,
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
eventMetaGroupKey(metaOCIDigestKey): obj.Status.LastAttemptedRevisionDigest,
eventMetaGroupKey(eventv1.MetaRevisionKey): chrt.Metadata.Version,
eventMetaGroupKey(eventv1.MetaTokenKey): chartutil.DigestValues(digest.Canonical, req.Values).String(),
},
},
},
}))
})
t.Run("records failure with logs", func(t *testing.T) {
g := NewWithT(t)
recorder := testutil.NewFakeRecorder(10, false)
r := &Install{
eventRecorder: recorder,
}
req := &Request{Object: obj.DeepCopy(), Chart: chrt}
r.failure(req, mockLogBuffer(5, 10), err)
expectSubStr := "Last Helm logs"
g.Expect(conditions.IsFalse(req.Object, v2.ReleasedCondition)).To(BeTrue())
g.Expect(conditions.GetMessage(req.Object, v2.ReleasedCondition)).ToNot(ContainSubstring(expectSubStr))
events := recorder.GetEvents()
g.Expect(events).To(HaveLen(1))
g.Expect(events[0].Message).To(ContainSubstring(expectSubStr))
})
}
func TestInstall_success(t *testing.T) {
var (
cur = testutil.BuildRelease(&helmrelease.MockReleaseOptions{
Name: mockReleaseName,
Namespace: mockReleaseNamespace,
Chart: testutil.BuildChart(),
})
obj = &v2.HelmRelease{
Status: v2.HelmReleaseStatus{
History: v2.Snapshots{
release.ObservedToSnapshot(release.ObserveRelease(cur)),
},
},
}
)
t.Run("records success", func(t *testing.T) {
g := NewWithT(t)
recorder := testutil.NewFakeRecorder(10, false)
r := &Install{
eventRecorder: recorder,
}
req := &Request{
Object: obj.DeepCopy(),
}
r.success(req)
expectMsg := fmt.Sprintf(fmtInstallSuccess,
fmt.Sprintf("%s/%s.v%d", mockReleaseNamespace, mockReleaseName, obj.Status.History.Latest().Version),
fmt.Sprintf("%s@%s", obj.Status.History.Latest().ChartName, obj.Status.History.Latest().ChartVersion))
g.Expect(req.Object.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{
*conditions.TrueCondition(v2.ReleasedCondition, v2.InstallSucceededReason, expectMsg),
}))
g.Expect(recorder.GetEvents()).To(ConsistOf([]corev1.Event{
{
Type: corev1.EventTypeNormal,
Reason: v2.InstallSucceededReason,
Message: expectMsg,
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
eventMetaGroupKey(eventv1.MetaRevisionKey): obj.Status.History.Latest().ChartVersion,
eventMetaGroupKey(eventv1.MetaTokenKey): obj.Status.History.Latest().ConfigDigest,
},
},
},
}))
})
t.Run("records success with TestSuccess=False", func(t *testing.T) {
g := NewWithT(t)
recorder := testutil.NewFakeRecorder(10, false)
r := &Install{
eventRecorder: recorder,
}
obj := obj.DeepCopy()
obj.Spec.Test = &v2.Test{Enable: true}
req := &Request{Object: obj}
r.success(req)
g.Expect(conditions.IsTrue(req.Object, v2.ReleasedCondition)).To(BeTrue())
cond := conditions.Get(req.Object, v2.TestSuccessCondition)
g.Expect(cond).ToNot(BeNil())
expectMsg := fmt.Sprintf(fmtTestPending,
fmt.Sprintf("%s/%s.v%d", mockReleaseNamespace, mockReleaseName, obj.Status.History.Latest().Version),
fmt.Sprintf("%s@%s", obj.Status.History.Latest().ChartName, obj.Status.History.Latest().ChartVersion))
g.Expect(cond.Message).To(Equal(expectMsg))
})
}