helm-controller/controllers/helmrelease_controller_char...

534 lines
14 KiB
Go

/*
Copyright 2020 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 controllers
import (
"context"
"fmt"
"testing"
"time"
"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/go-logr/logr"
. "github.com/onsi/gomega"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
v2 "github.com/fluxcd/helm-controller/api/v2beta1"
)
func TestHelmReleaseReconciler_reconcileChart(t *testing.T) {
tests := []struct {
name string
hr *v2.HelmRelease
hc *sourcev1.HelmChart
expectHelmChartStatus string
expectGC bool
expectErr bool
noCrossNamspaceRef bool
}{
{
name: "new HelmChart",
hr: &v2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: "test-release",
Namespace: "default",
},
Spec: v2.HelmReleaseSpec{
Interval: metav1.Duration{Duration: time.Minute},
Chart: v2.HelmChartTemplate{
Spec: v2.HelmChartTemplateSpec{
Chart: "chart",
SourceRef: v2.CrossNamespaceObjectReference{
Name: "test-repository",
Kind: "HelmRepository",
},
},
},
},
},
hc: nil,
expectHelmChartStatus: "default/default-test-release",
},
{
name: "existing HelmChart",
hr: &v2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: "test-release",
Namespace: "default",
},
Spec: v2.HelmReleaseSpec{
Interval: metav1.Duration{Duration: time.Minute},
Chart: v2.HelmChartTemplate{
Spec: v2.HelmChartTemplateSpec{
Chart: "chart",
SourceRef: v2.CrossNamespaceObjectReference{
Name: "test-repository",
Kind: "HelmRepository",
},
},
},
},
},
hc: &sourcev1.HelmChart{
ObjectMeta: metav1.ObjectMeta{
Name: "default-test-release",
Namespace: "default",
},
Spec: sourcev1.HelmChartSpec{
Chart: "chart",
SourceRef: sourcev1.LocalHelmChartSourceReference{
Name: "test-repository",
Kind: "HelmRepository",
},
},
},
expectHelmChartStatus: "default/default-test-release",
},
{
name: "modified HelmChart",
hr: &v2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: "test-release",
Namespace: "default",
},
Spec: v2.HelmReleaseSpec{
Interval: metav1.Duration{Duration: time.Minute},
Chart: v2.HelmChartTemplate{
Spec: v2.HelmChartTemplateSpec{
Chart: "chart",
SourceRef: v2.CrossNamespaceObjectReference{
Name: "test-repository",
Kind: "HelmRepository",
Namespace: "cross",
},
},
},
},
Status: v2.HelmReleaseStatus{
HelmChart: "default/default-test-release",
},
},
hc: &sourcev1.HelmChart{
ObjectMeta: metav1.ObjectMeta{
Name: "default-test-release",
Namespace: "default",
},
Spec: sourcev1.HelmChartSpec{
Chart: "chart",
SourceRef: sourcev1.LocalHelmChartSourceReference{
Name: "test-repository",
Kind: "HelmRepository",
},
},
},
expectHelmChartStatus: "cross/default-test-release",
expectGC: true,
},
{
name: "block cross namespace access when flag is set",
hr: &v2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: "test-release",
Namespace: "default",
},
Spec: v2.HelmReleaseSpec{
Interval: metav1.Duration{Duration: time.Minute},
Chart: v2.HelmChartTemplate{
Spec: v2.HelmChartTemplateSpec{
Chart: "chart",
SourceRef: v2.CrossNamespaceObjectReference{
Name: "test-repository",
Kind: "HelmRepository",
Namespace: "cross",
},
},
},
},
Status: v2.HelmReleaseStatus{
HelmChart: "",
},
},
noCrossNamspaceRef: true,
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
g.Expect(v2.AddToScheme(scheme.Scheme)).To(Succeed())
g.Expect(sourcev1.AddToScheme(scheme.Scheme)).To(Succeed())
var c client.Client
if tt.hc != nil {
c = fake.NewFakeClientWithScheme(scheme.Scheme, tt.hc)
} else {
c = fake.NewFakeClientWithScheme(scheme.Scheme)
}
r := &HelmReleaseReconciler{
Client: c,
NoCrossNamespaceRef: tt.noCrossNamspaceRef,
}
hc, err := r.reconcileChart(logr.NewContext(context.TODO(), logr.Discard()), tt.hr)
if tt.expectErr {
g.Expect(err).To(HaveOccurred())
g.Expect(hc).To(BeNil())
} else {
g.Expect(err).NotTo(HaveOccurred())
g.Expect(hc).NotTo(BeNil())
}
g.Expect(tt.hr.Status.HelmChart).To(Equal(tt.expectHelmChartStatus))
if tt.expectGC {
objKey := client.ObjectKeyFromObject(tt.hc)
err = c.Get(context.TODO(), objKey, tt.hc.DeepCopy())
g.Expect(apierrors.IsNotFound(err)).To(BeTrue())
}
})
}
}
func TestHelmReleaseReconciler_deleteHelmChart(t *testing.T) {
tests := []struct {
name string
hc *sourcev1.HelmChart
hr *v2.HelmRelease
expectHelmChartStatus string
expectErr bool
}{
{
name: "delete existing HelmChart",
hc: &sourcev1.HelmChart{
ObjectMeta: metav1.ObjectMeta{
Name: "test-chart",
Namespace: "default",
},
},
hr: &v2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: "test-release",
},
Status: v2.HelmReleaseStatus{
HelmChart: "default/test-chart",
},
},
expectHelmChartStatus: "",
expectErr: false,
},
{
name: "delete already removed HelmChart",
hc: nil,
hr: &v2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: "test-release",
},
Status: v2.HelmReleaseStatus{
HelmChart: "default/test-chart",
},
},
expectHelmChartStatus: "",
expectErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
g.Expect(v2.AddToScheme(scheme.Scheme)).To(Succeed())
g.Expect(sourcev1.AddToScheme(scheme.Scheme)).To(Succeed())
var c client.Client
if tt.hc != nil {
c = fake.NewFakeClientWithScheme(scheme.Scheme, tt.hc)
} else {
c = fake.NewFakeClientWithScheme(scheme.Scheme)
}
r := &HelmReleaseReconciler{
Client: c,
}
err := r.deleteHelmChart(context.TODO(), tt.hr)
if tt.expectErr {
g.Expect(err).To(HaveOccurred())
} else {
g.Expect(err).NotTo(HaveOccurred())
}
g.Expect(tt.hr.Status.HelmChart).To(Equal(tt.expectHelmChartStatus))
})
}
}
func Test_buildHelmChartFromTemplate(t *testing.T) {
hrWithChartTemplate := v2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: "test-release",
Namespace: "default",
},
Spec: v2.HelmReleaseSpec{
Interval: metav1.Duration{Duration: time.Minute},
Chart: v2.HelmChartTemplate{
Spec: v2.HelmChartTemplateSpec{
Chart: "chart",
Version: "1.0.0",
SourceRef: v2.CrossNamespaceObjectReference{
Name: "test-repository",
Kind: "HelmRepository",
},
Interval: &metav1.Duration{Duration: 2 * time.Minute},
ValuesFiles: []string{"values.yaml"},
},
},
},
}
tests := []struct {
name string
modify func(release *v2.HelmRelease)
want *sourcev1.HelmChart
}{
{
name: "builds HelmChart from HelmChartTemplate",
modify: func(*v2.HelmRelease) {},
want: &sourcev1.HelmChart{
ObjectMeta: metav1.ObjectMeta{
Name: "default-test-release",
Namespace: "default",
},
Spec: sourcev1.HelmChartSpec{
Chart: "chart",
Version: "1.0.0",
SourceRef: sourcev1.LocalHelmChartSourceReference{
Name: "test-repository",
Kind: "HelmRepository",
},
Interval: metav1.Duration{Duration: 2 * time.Minute},
ValuesFiles: []string{"values.yaml"},
},
},
},
{
name: "takes SourceRef namespace into account",
modify: func(hr *v2.HelmRelease) {
hr.Spec.Chart.Spec.SourceRef.Namespace = "cross"
},
want: &sourcev1.HelmChart{
ObjectMeta: metav1.ObjectMeta{
Name: "default-test-release",
Namespace: "cross",
},
Spec: sourcev1.HelmChartSpec{
Chart: "chart",
Version: "1.0.0",
SourceRef: sourcev1.LocalHelmChartSourceReference{
Name: "test-repository",
Kind: "HelmRepository",
},
Interval: metav1.Duration{Duration: 2 * time.Minute},
ValuesFiles: []string{"values.yaml"},
},
},
},
{
name: "falls back to HelmRelease interval",
modify: func(hr *v2.HelmRelease) {
hr.Spec.Chart.Spec.Interval = nil
},
want: &sourcev1.HelmChart{
ObjectMeta: metav1.ObjectMeta{
Name: "default-test-release",
Namespace: "default",
},
Spec: sourcev1.HelmChartSpec{
Chart: "chart",
Version: "1.0.0",
SourceRef: sourcev1.LocalHelmChartSourceReference{
Name: "test-repository",
Kind: "HelmRepository",
},
Interval: metav1.Duration{Duration: time.Minute},
ValuesFiles: []string{"values.yaml"},
},
},
},
{
name: "take cosign verification into account",
modify: func(hr *v2.HelmRelease) {
hr.Spec.Chart.Spec.Verify = &v2.HelmChartTemplateVerification{
Provider: "cosign",
SecretRef: &meta.LocalObjectReference{
Name: "cosign-key",
},
}
},
want: &sourcev1.HelmChart{
ObjectMeta: metav1.ObjectMeta{
Name: "default-test-release",
Namespace: "default",
},
Spec: sourcev1.HelmChartSpec{
Chart: "chart",
Version: "1.0.0",
SourceRef: sourcev1.LocalHelmChartSourceReference{
Name: "test-repository",
Kind: "HelmRepository",
},
Interval: metav1.Duration{Duration: 2 * time.Minute},
ValuesFiles: []string{"values.yaml"},
Verify: &sourcev1.OCIRepositoryVerification{
Provider: "cosign",
SecretRef: &meta.LocalObjectReference{
Name: "cosign-key",
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
hr := hrWithChartTemplate.DeepCopy()
tt.modify(hr)
g.Expect(buildHelmChartFromTemplate(hr)).To(Equal(tt.want))
})
}
}
func Test_helmChartRequiresUpdate(t *testing.T) {
hrWithChartTemplate := v2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: "test-release",
},
Spec: v2.HelmReleaseSpec{
Interval: metav1.Duration{Duration: time.Minute},
Chart: v2.HelmChartTemplate{
Spec: v2.HelmChartTemplateSpec{
Chart: "chart",
Version: "1.0.0",
SourceRef: v2.CrossNamespaceObjectReference{
Name: "test-repository",
Kind: "HelmRepository",
},
Interval: &metav1.Duration{Duration: 2 * time.Minute},
Verify: &v2.HelmChartTemplateVerification{
Provider: "cosign",
},
},
},
},
}
tests := []struct {
name string
modify func(*v2.HelmRelease, *sourcev1.HelmChart)
want bool
}{
{
name: "detects no change",
modify: func(*v2.HelmRelease, *sourcev1.HelmChart) {},
want: false,
},
{
name: "detects chart change",
modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
hr.Spec.Chart.Spec.Chart = "new"
},
want: true,
},
{
name: "detects version change",
modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
hr.Spec.Chart.Spec.Version = "2.0.0"
},
want: true,
},
{
name: "detects chart source name change",
modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
hr.Spec.Chart.Spec.SourceRef.Name = "new"
},
want: true,
},
{
name: "detects chart source kind change",
modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
hr.Spec.Chart.Spec.SourceRef.Kind = "GitRepository"
},
want: true,
},
{
name: "detects interval change",
modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
hr.Spec.Chart.Spec.Interval = nil
},
want: true,
},
{
name: "detects reconcile strategy change",
modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
hr.Spec.Chart.Spec.ReconcileStrategy = "Revision"
},
want: true,
},
{
name: "detects values files change",
modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
hr.Spec.Chart.Spec.ValuesFiles = []string{"values-prod.yaml"}
},
want: true,
},
{
name: "detects values file change",
modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
hr.Spec.Chart.Spec.ValuesFile = "values-prod.yaml"
},
want: true,
},
{
name: "detects verify change",
modify: func(hr *v2.HelmRelease, hc *sourcev1.HelmChart) {
hr.Spec.Chart.Spec.Verify.Provider = "foo-bar"
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
hr := hrWithChartTemplate.DeepCopy()
hc := buildHelmChartFromTemplate(hr)
// second copy to avoid modifying the original
hr = hrWithChartTemplate.DeepCopy()
g.Expect(helmChartRequiresUpdate(hr, hc)).To(Equal(false))
tt.modify(hr, hc)
fmt.Println("verify", hr.Spec.Chart.Spec.Verify.Provider, hc.Spec.Verify.Provider)
g.Expect(helmChartRequiresUpdate(hr, hc)).To(Equal(tt.want))
})
}
}