740 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			740 lines
		
	
	
		
			24 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"
 | |
| 	"net/http"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/darkowlzz/controller-check/status"
 | |
| 	"github.com/fluxcd/pkg/apis/meta"
 | |
| 	"github.com/fluxcd/pkg/helmtestserver"
 | |
| 	"github.com/fluxcd/pkg/runtime/conditions"
 | |
| 	"github.com/fluxcd/pkg/runtime/patch"
 | |
| 	. "github.com/onsi/gomega"
 | |
| 	corev1 "k8s.io/api/core/v1"
 | |
| 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 | |
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | |
| 	"k8s.io/client-go/tools/record"
 | |
| 	"sigs.k8s.io/controller-runtime/pkg/client"
 | |
| 	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
 | |
| 
 | |
| 	sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
 | |
| 	"github.com/fluxcd/source-controller/internal/helm/repository"
 | |
| 	sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
 | |
| )
 | |
| 
 | |
| func TestHelmRepositoryReconciler_Reconcile(t *testing.T) {
 | |
| 	g := NewWithT(t)
 | |
| 
 | |
| 	testServer, err := helmtestserver.NewTempHelmServer()
 | |
| 	g.Expect(err).NotTo(HaveOccurred())
 | |
| 	defer os.RemoveAll(testServer.Root())
 | |
| 
 | |
| 	g.Expect(testServer.PackageChart("testdata/charts/helmchart")).To(Succeed())
 | |
| 	g.Expect(testServer.GenerateIndex()).To(Succeed())
 | |
| 
 | |
| 	testServer.Start()
 | |
| 	defer testServer.Stop()
 | |
| 
 | |
| 	obj := &sourcev1.HelmRepository{
 | |
| 		ObjectMeta: metav1.ObjectMeta{
 | |
| 			GenerateName: "helmrepository-reconcile-",
 | |
| 			Namespace:    "default",
 | |
| 		},
 | |
| 		Spec: sourcev1.HelmRepositorySpec{
 | |
| 			Interval: metav1.Duration{Duration: interval},
 | |
| 			URL:      testServer.URL(),
 | |
| 		},
 | |
| 	}
 | |
| 	g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
 | |
| 
 | |
| 	key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace}
 | |
| 
 | |
| 	// Wait for finalizer to be set
 | |
| 	g.Eventually(func() bool {
 | |
| 		if err := testEnv.Get(ctx, key, obj); err != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		return len(obj.Finalizers) > 0
 | |
| 	}, timeout).Should(BeTrue())
 | |
| 
 | |
| 	// Wait for HelmRepository to be Ready
 | |
| 	g.Eventually(func() bool {
 | |
| 		if err := testEnv.Get(ctx, key, obj); err != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		if !conditions.IsReady(obj) && obj.Status.Artifact == nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		readyCondition := conditions.Get(obj, meta.ReadyCondition)
 | |
| 		return readyCondition.Status == metav1.ConditionTrue &&
 | |
| 			obj.Generation == readyCondition.ObservedGeneration &&
 | |
| 			obj.Generation == obj.Status.ObservedGeneration
 | |
| 	}, timeout).Should(BeTrue())
 | |
| 
 | |
| 	// Check if the object status is valid.
 | |
| 	condns := &status.Conditions{NegativePolarity: helmRepoReadyConditions.NegativePolarity}
 | |
| 	checker := status.NewChecker(testEnv.Client, testEnv.GetScheme(), condns)
 | |
| 	checker.CheckErr(ctx, obj)
 | |
| 
 | |
| 	// Patch the object with reconcile request annotation.
 | |
| 	patchHelper, err := patch.NewHelper(obj, testEnv.Client)
 | |
| 	g.Expect(err).ToNot(HaveOccurred())
 | |
| 	annotations := map[string]string{
 | |
| 		meta.ReconcileRequestAnnotation: "now",
 | |
| 	}
 | |
| 	obj.SetAnnotations(annotations)
 | |
| 	g.Expect(patchHelper.Patch(ctx, obj)).ToNot(HaveOccurred())
 | |
| 	g.Eventually(func() bool {
 | |
| 		if err := testEnv.Get(ctx, key, obj); err != nil {
 | |
| 			return false
 | |
| 		}
 | |
| 		return obj.Status.LastHandledReconcileAt == "now"
 | |
| 	}, timeout).Should(BeTrue())
 | |
| 
 | |
| 	g.Expect(testEnv.Delete(ctx, obj)).To(Succeed())
 | |
| 
 | |
| 	// Wait for HelmRepository to be deleted
 | |
| 	g.Eventually(func() bool {
 | |
| 		if err := testEnv.Get(ctx, key, obj); err != nil {
 | |
| 			return apierrors.IsNotFound(err)
 | |
| 		}
 | |
| 		return false
 | |
| 	}, timeout).Should(BeTrue())
 | |
| }
 | |
| 
 | |
| func TestHelmRepositoryReconciler_reconcileStorage(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name             string
 | |
| 		beforeFunc       func(obj *sourcev1.HelmRepository, storage *Storage) error
 | |
| 		want             sreconcile.Result
 | |
| 		wantErr          bool
 | |
| 		assertArtifact   *sourcev1.Artifact
 | |
| 		assertConditions []metav1.Condition
 | |
| 		assertPaths      []string
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "garbage collects",
 | |
| 			beforeFunc: func(obj *sourcev1.HelmRepository, storage *Storage) error {
 | |
| 				revisions := []string{"a", "b", "c"}
 | |
| 				for n := range revisions {
 | |
| 					v := revisions[n]
 | |
| 					obj.Status.Artifact = &sourcev1.Artifact{
 | |
| 						Path:     fmt.Sprintf("/reconcile-storage/%s.txt", v),
 | |
| 						Revision: v,
 | |
| 					}
 | |
| 					if err := testStorage.MkdirAll(*obj.Status.Artifact); err != nil {
 | |
| 						return err
 | |
| 					}
 | |
| 					if err := testStorage.AtomicWriteFile(obj.Status.Artifact, strings.NewReader(v), 0644); err != nil {
 | |
| 						return err
 | |
| 					}
 | |
| 				}
 | |
| 				testStorage.SetArtifactURL(obj.Status.Artifact)
 | |
| 				return nil
 | |
| 			},
 | |
| 			assertArtifact: &sourcev1.Artifact{
 | |
| 				Path:     "/reconcile-storage/c.txt",
 | |
| 				Revision: "c",
 | |
| 				Checksum: "2e7d2c03a9507ae265ecf5b5356885a53393a2029d241394997265a1a25aefc6",
 | |
| 				URL:      testStorage.Hostname + "/reconcile-storage/c.txt",
 | |
| 			},
 | |
| 			assertPaths: []string{
 | |
| 				"/reconcile-storage/c.txt",
 | |
| 				"!/reconcile-storage/b.txt",
 | |
| 				"!/reconcile-storage/a.txt",
 | |
| 			},
 | |
| 			want: sreconcile.ResultSuccess,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "notices missing artifact in storage",
 | |
| 			beforeFunc: func(obj *sourcev1.HelmRepository, storage *Storage) error {
 | |
| 				obj.Status.Artifact = &sourcev1.Artifact{
 | |
| 					Path:     "/reconcile-storage/invalid.txt",
 | |
| 					Revision: "d",
 | |
| 				}
 | |
| 				testStorage.SetArtifactURL(obj.Status.Artifact)
 | |
| 				return nil
 | |
| 			},
 | |
| 			want: sreconcile.ResultSuccess,
 | |
| 			assertPaths: []string{
 | |
| 				"!/reconcile-storage/invalid.txt",
 | |
| 			},
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(meta.ReconcilingCondition, "NoArtifact", "no artifact for resource in storage"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "updates hostname on diff from current",
 | |
| 			beforeFunc: func(obj *sourcev1.HelmRepository, storage *Storage) error {
 | |
| 				obj.Status.Artifact = &sourcev1.Artifact{
 | |
| 					Path:     "/reconcile-storage/hostname.txt",
 | |
| 					Revision: "f",
 | |
| 					Checksum: "3b9c358f36f0a31b6ad3e14f309c7cf198ac9246e8316f9ce543d5b19ac02b80",
 | |
| 					URL:      "http://outdated.com/reconcile-storage/hostname.txt",
 | |
| 				}
 | |
| 				if err := testStorage.MkdirAll(*obj.Status.Artifact); err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 				if err := testStorage.AtomicWriteFile(obj.Status.Artifact, strings.NewReader("file"), 0644); err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 				return nil
 | |
| 			},
 | |
| 			want: sreconcile.ResultSuccess,
 | |
| 			assertPaths: []string{
 | |
| 				"/reconcile-storage/hostname.txt",
 | |
| 			},
 | |
| 			assertArtifact: &sourcev1.Artifact{
 | |
| 				Path:     "/reconcile-storage/hostname.txt",
 | |
| 				Revision: "f",
 | |
| 				Checksum: "3b9c358f36f0a31b6ad3e14f309c7cf198ac9246e8316f9ce543d5b19ac02b80",
 | |
| 				URL:      testStorage.Hostname + "/reconcile-storage/hostname.txt",
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			g := NewWithT(t)
 | |
| 
 | |
| 			r := &HelmRepositoryReconciler{
 | |
| 				EventRecorder: record.NewFakeRecorder(32),
 | |
| 				Storage:       testStorage,
 | |
| 			}
 | |
| 
 | |
| 			obj := &sourcev1.HelmRepository{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					GenerateName: "test-",
 | |
| 				},
 | |
| 			}
 | |
| 			if tt.beforeFunc != nil {
 | |
| 				g.Expect(tt.beforeFunc(obj, testStorage)).To(Succeed())
 | |
| 			}
 | |
| 
 | |
| 			var chartRepo repository.ChartRepository
 | |
| 			var artifact sourcev1.Artifact
 | |
| 
 | |
| 			got, err := r.reconcileStorage(context.TODO(), obj, &artifact, &chartRepo)
 | |
| 			g.Expect(err != nil).To(Equal(tt.wantErr))
 | |
| 			g.Expect(got).To(Equal(tt.want))
 | |
| 
 | |
| 			g.Expect(obj.Status.Artifact).To(MatchArtifact(tt.assertArtifact))
 | |
| 			if tt.assertArtifact != nil && tt.assertArtifact.URL != "" {
 | |
| 				g.Expect(obj.Status.Artifact.URL).To(Equal(tt.assertArtifact.URL))
 | |
| 			}
 | |
| 			g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
 | |
| 
 | |
| 			for _, p := range tt.assertPaths {
 | |
| 				absoluteP := filepath.Join(testStorage.BasePath, p)
 | |
| 				if !strings.HasPrefix(p, "!") {
 | |
| 					g.Expect(absoluteP).To(BeAnExistingFile())
 | |
| 					continue
 | |
| 				}
 | |
| 				g.Expect(absoluteP).NotTo(BeAnExistingFile())
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
 | |
| 	type options struct {
 | |
| 		username   string
 | |
| 		password   string
 | |
| 		publicKey  []byte
 | |
| 		privateKey []byte
 | |
| 		ca         []byte
 | |
| 	}
 | |
| 
 | |
| 	tests := []struct {
 | |
| 		name             string
 | |
| 		protocol         string
 | |
| 		server           options
 | |
| 		secret           *corev1.Secret
 | |
| 		beforeFunc       func(t *WithT, obj *sourcev1.HelmRepository)
 | |
| 		afterFunc        func(t *WithT, obj *sourcev1.HelmRepository)
 | |
| 		want             sreconcile.Result
 | |
| 		wantErr          bool
 | |
| 		assertConditions []metav1.Condition
 | |
| 	}{
 | |
| 		{
 | |
| 			name:     "HTTP without secretRef makes ArtifactOutdated=True",
 | |
| 			protocol: "http",
 | |
| 			want:     sreconcile.ResultSuccess,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision"),
 | |
| 				*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new index revision"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "HTTP with Basic Auth secret makes ArtifactOutdated=True",
 | |
| 			protocol: "http",
 | |
| 			server: options{
 | |
| 				username: "git",
 | |
| 				password: "1234",
 | |
| 			},
 | |
| 			secret: &corev1.Secret{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "basic-auth",
 | |
| 				},
 | |
| 				Data: map[string][]byte{
 | |
| 					"username": []byte("git"),
 | |
| 					"password": []byte("1234"),
 | |
| 				},
 | |
| 			},
 | |
| 			beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
 | |
| 				obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "basic-auth"}
 | |
| 			},
 | |
| 			want: sreconcile.ResultSuccess,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision"),
 | |
| 				*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new index revision"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "HTTPS with CAFile secret makes ArtifactOutdated=True",
 | |
| 			protocol: "https",
 | |
| 			server: options{
 | |
| 				publicKey:  tlsPublicKey,
 | |
| 				privateKey: tlsPrivateKey,
 | |
| 				ca:         tlsCA,
 | |
| 			},
 | |
| 			secret: &corev1.Secret{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "ca-file",
 | |
| 				},
 | |
| 				Data: map[string][]byte{
 | |
| 					"caFile": tlsCA,
 | |
| 				},
 | |
| 			},
 | |
| 			beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
 | |
| 				obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "ca-file"}
 | |
| 			},
 | |
| 			want: sreconcile.ResultSuccess,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision"),
 | |
| 				*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new index revision"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "HTTPS with invalid CAFile secret makes FetchFailed=True and returns error",
 | |
| 			protocol: "https",
 | |
| 			server: options{
 | |
| 				publicKey:  tlsPublicKey,
 | |
| 				privateKey: tlsPrivateKey,
 | |
| 				ca:         tlsCA,
 | |
| 			},
 | |
| 			secret: &corev1.Secret{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "invalid-ca",
 | |
| 				},
 | |
| 				Data: map[string][]byte{
 | |
| 					"caFile": []byte("invalid"),
 | |
| 				},
 | |
| 			},
 | |
| 			beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
 | |
| 				obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "invalid-ca"}
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(sourcev1.FetchFailedCondition, meta.FailedReason, "can't create TLS config for client: failed to append certificates from file"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Invalid URL makes FetchFailed=True and returns stalling error",
 | |
| 			protocol: "http",
 | |
| 			beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
 | |
| 				obj.Spec.URL = strings.ReplaceAll(obj.Spec.URL, "http://", "")
 | |
| 			},
 | |
| 			want:    sreconcile.ResultEmpty,
 | |
| 			wantErr: true,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.URLInvalidReason, "first path segment in URL cannot contain colon"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Unsupported scheme makes FetchFailed=True and returns stalling error",
 | |
| 			protocol: "http",
 | |
| 			beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
 | |
| 				obj.Spec.URL = strings.ReplaceAll(obj.Spec.URL, "http://", "ftp://")
 | |
| 			},
 | |
| 			want:    sreconcile.ResultEmpty,
 | |
| 			wantErr: true,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(sourcev1.FetchFailedCondition, meta.FailedReason, "scheme \"ftp\" not supported"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Missing secret returns FetchFailed=True and returns error",
 | |
| 			protocol: "http",
 | |
| 			beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
 | |
| 				obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "non-existing"}
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "secrets \"non-existing\" not found"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Malformed secret returns FetchFailed=True and returns error",
 | |
| 			protocol: "http",
 | |
| 			secret: &corev1.Secret{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "malformed-basic-auth",
 | |
| 				},
 | |
| 				Data: map[string][]byte{
 | |
| 					"username": []byte("git"),
 | |
| 				},
 | |
| 			},
 | |
| 			beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
 | |
| 				obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "malformed-basic-auth"}
 | |
| 			},
 | |
| 			wantErr: true,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "required fields 'username' and 'password"),
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		obj := &sourcev1.HelmRepository{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				GenerateName: "auth-strategy-",
 | |
| 			},
 | |
| 			Spec: sourcev1.HelmRepositorySpec{
 | |
| 				Interval: metav1.Duration{Duration: interval},
 | |
| 				Timeout:  &metav1.Duration{Duration: interval},
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			g := NewWithT(t)
 | |
| 
 | |
| 			server, err := helmtestserver.NewTempHelmServer()
 | |
| 			g.Expect(err).NotTo(HaveOccurred())
 | |
| 			defer os.RemoveAll(server.Root())
 | |
| 
 | |
| 			g.Expect(server.PackageChart("testdata/charts/helmchart")).To(Succeed())
 | |
| 			g.Expect(server.GenerateIndex()).To(Succeed())
 | |
| 
 | |
| 			if len(tt.server.username+tt.server.password) > 0 {
 | |
| 				server.WithMiddleware(func(handler http.Handler) http.Handler {
 | |
| 					return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 						u, p, ok := r.BasicAuth()
 | |
| 						if !ok || u != tt.server.username || p != tt.server.password {
 | |
| 							w.WriteHeader(401)
 | |
| 							return
 | |
| 						}
 | |
| 						handler.ServeHTTP(w, r)
 | |
| 					})
 | |
| 				})
 | |
| 			}
 | |
| 
 | |
| 			secret := tt.secret.DeepCopy()
 | |
| 			switch tt.protocol {
 | |
| 			case "http":
 | |
| 				server.Start()
 | |
| 				defer server.Stop()
 | |
| 				obj.Spec.URL = server.URL()
 | |
| 			case "https":
 | |
| 				g.Expect(server.StartTLS(tt.server.publicKey, tt.server.privateKey, tt.server.ca, "example.com")).To(Succeed())
 | |
| 				defer server.Stop()
 | |
| 				obj.Spec.URL = server.URL()
 | |
| 			default:
 | |
| 				t.Fatalf("unsupported protocol %q", tt.protocol)
 | |
| 			}
 | |
| 
 | |
| 			if tt.beforeFunc != nil {
 | |
| 				tt.beforeFunc(g, obj)
 | |
| 			}
 | |
| 
 | |
| 			builder := fakeclient.NewClientBuilder().WithScheme(testEnv.GetScheme())
 | |
| 			if secret != nil {
 | |
| 				builder.WithObjects(secret.DeepCopy())
 | |
| 			}
 | |
| 
 | |
| 			r := &HelmRepositoryReconciler{
 | |
| 				EventRecorder: record.NewFakeRecorder(32),
 | |
| 				Client:        builder.Build(),
 | |
| 				Storage:       testStorage,
 | |
| 				Getters:       testGetters,
 | |
| 			}
 | |
| 
 | |
| 			var chartRepo repository.ChartRepository
 | |
| 			var artifact sourcev1.Artifact
 | |
| 			got, err := r.reconcileSource(context.TODO(), obj, &artifact, &chartRepo)
 | |
| 			defer os.Remove(chartRepo.CachePath)
 | |
| 
 | |
| 			g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
 | |
| 			g.Expect(err != nil).To(Equal(tt.wantErr))
 | |
| 			g.Expect(got).To(Equal(tt.want))
 | |
| 
 | |
| 			if tt.afterFunc != nil {
 | |
| 				tt.afterFunc(g, obj)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestHelmRepositoryReconciler_reconcileArtifact(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name             string
 | |
| 		beforeFunc       func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, index *repository.ChartRepository)
 | |
| 		afterFunc        func(t *WithT, obj *sourcev1.HelmRepository)
 | |
| 		want             sreconcile.Result
 | |
| 		wantErr          bool
 | |
| 		assertConditions []metav1.Condition
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "Archiving artifact to storage makes Ready=True",
 | |
| 			beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, index *repository.ChartRepository) {
 | |
| 				obj.Spec.Interval = metav1.Duration{Duration: interval}
 | |
| 			},
 | |
| 			want: sreconcile.ResultSuccess,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "stored artifact for revision 'existing'"),
 | |
| 				*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new index revision 'existing'"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "Up-to-date artifact should not update status",
 | |
| 			beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, index *repository.ChartRepository) {
 | |
| 				obj.Spec.Interval = metav1.Duration{Duration: interval}
 | |
| 				obj.Status.Artifact = artifact.DeepCopy()
 | |
| 			},
 | |
| 			afterFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
 | |
| 				t.Expect(obj.Status.URL).To(BeEmpty())
 | |
| 			},
 | |
| 			want: sreconcile.ResultSuccess,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "stored artifact for revision 'existing'"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "Removes ArtifactOutdatedCondition after creating a new artifact",
 | |
| 			beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, index *repository.ChartRepository) {
 | |
| 				obj.Spec.Interval = metav1.Duration{Duration: interval}
 | |
| 				conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "Foo", "")
 | |
| 			},
 | |
| 			want: sreconcile.ResultSuccess,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "stored artifact for revision 'existing'"),
 | |
| 				*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new index revision 'existing'"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "Creates latest symlink to the created artifact",
 | |
| 			beforeFunc: func(t *WithT, obj *sourcev1.HelmRepository, artifact sourcev1.Artifact, index *repository.ChartRepository) {
 | |
| 				obj.Spec.Interval = metav1.Duration{Duration: interval}
 | |
| 			},
 | |
| 			afterFunc: func(t *WithT, obj *sourcev1.HelmRepository) {
 | |
| 				localPath := testStorage.LocalPath(*obj.GetArtifact())
 | |
| 				symlinkPath := filepath.Join(filepath.Dir(localPath), "index.yaml")
 | |
| 				targetFile, err := os.Readlink(symlinkPath)
 | |
| 				t.Expect(err).NotTo(HaveOccurred())
 | |
| 				t.Expect(localPath).To(Equal(targetFile))
 | |
| 			},
 | |
| 			want: sreconcile.ResultSuccess,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, "stored artifact for revision 'existing'"),
 | |
| 				*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new index revision 'existing'"),
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			g := NewWithT(t)
 | |
| 
 | |
| 			r := &HelmRepositoryReconciler{
 | |
| 				EventRecorder: record.NewFakeRecorder(32),
 | |
| 				Storage:       testStorage,
 | |
| 			}
 | |
| 
 | |
| 			obj := &sourcev1.HelmRepository{
 | |
| 				TypeMeta: metav1.TypeMeta{
 | |
| 					Kind: sourcev1.HelmRepositoryKind,
 | |
| 				},
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					GenerateName: "test-bucket-",
 | |
| 					Generation:   1,
 | |
| 					Namespace:    "default",
 | |
| 				},
 | |
| 				Spec: sourcev1.HelmRepositorySpec{
 | |
| 					Timeout: &metav1.Duration{Duration: timeout},
 | |
| 					URL:     "https://example.com/index.yaml",
 | |
| 				},
 | |
| 			}
 | |
| 
 | |
| 			tmpDir, err := os.MkdirTemp("", "test-reconcile-artifact-")
 | |
| 			g.Expect(err).ToNot(HaveOccurred())
 | |
| 			defer os.RemoveAll(tmpDir)
 | |
| 
 | |
| 			// Create an empty cache file.
 | |
| 			cachePath := filepath.Join(tmpDir, "index.yaml")
 | |
| 			cacheFile, err := os.Create(cachePath)
 | |
| 			g.Expect(err).ToNot(HaveOccurred())
 | |
| 			g.Expect(cacheFile.Close()).ToNot(HaveOccurred())
 | |
| 
 | |
| 			chartRepo, err := repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil)
 | |
| 			g.Expect(err).ToNot(HaveOccurred())
 | |
| 			chartRepo.CachePath = cachePath
 | |
| 
 | |
| 			artifact := testStorage.NewArtifactFor(obj.Kind, obj, "existing", "foo.tar.gz")
 | |
| 			// Checksum of the index file calculated by the ChartRepository.
 | |
| 			artifact.Checksum = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
 | |
| 
 | |
| 			if tt.beforeFunc != nil {
 | |
| 				tt.beforeFunc(g, obj, artifact, chartRepo)
 | |
| 			}
 | |
| 
 | |
| 			got, err := r.reconcileArtifact(context.TODO(), obj, &artifact, chartRepo)
 | |
| 			g.Expect(err != nil).To(Equal(tt.wantErr))
 | |
| 			g.Expect(got).To(Equal(tt.want))
 | |
| 
 | |
| 			// On error, artifact is empty. Check artifacts only on successful
 | |
| 			// reconcile.
 | |
| 			if !tt.wantErr {
 | |
| 				g.Expect(obj.Status.Artifact).To(MatchArtifact(artifact.DeepCopy()))
 | |
| 			}
 | |
| 			g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
 | |
| 
 | |
| 			if tt.afterFunc != nil {
 | |
| 				tt.afterFunc(g, obj)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestHelmRepositoryReconciler_reconcileSubRecs(t *testing.T) {
 | |
| 	// Helper to build simple helmRepoReconcilerFunc with result and error.
 | |
| 	buildReconcileFuncs := func(r sreconcile.Result, e error) helmRepoReconcilerFunc {
 | |
| 		return func(ctx context.Context, obj *sourcev1.HelmRepository, artifact *sourcev1.Artifact, repo *repository.ChartRepository) (sreconcile.Result, error) {
 | |
| 			return r, e
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	tests := []struct {
 | |
| 		name               string
 | |
| 		generation         int64
 | |
| 		observedGeneration int64
 | |
| 		reconcileFuncs     []helmRepoReconcilerFunc
 | |
| 		wantResult         sreconcile.Result
 | |
| 		wantErr            bool
 | |
| 		assertConditions   []metav1.Condition
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "successful reconciliations",
 | |
| 			reconcileFuncs: []helmRepoReconcilerFunc{
 | |
| 				buildReconcileFuncs(sreconcile.ResultSuccess, nil),
 | |
| 			},
 | |
| 			wantResult: sreconcile.ResultSuccess,
 | |
| 			wantErr:    false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:               "successful reconciliation with generation difference",
 | |
| 			generation:         3,
 | |
| 			observedGeneration: 2,
 | |
| 			reconcileFuncs: []helmRepoReconcilerFunc{
 | |
| 				buildReconcileFuncs(sreconcile.ResultSuccess, nil),
 | |
| 			},
 | |
| 			wantResult: sreconcile.ResultSuccess,
 | |
| 			wantErr:    false,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(meta.ReconcilingCondition, "NewGeneration", "reconciling new generation 3"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "failed reconciliation",
 | |
| 			reconcileFuncs: []helmRepoReconcilerFunc{
 | |
| 				buildReconcileFuncs(sreconcile.ResultEmpty, fmt.Errorf("some error")),
 | |
| 			},
 | |
| 			wantResult: sreconcile.ResultEmpty,
 | |
| 			wantErr:    true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "multiple object status conditions mutations",
 | |
| 			reconcileFuncs: []helmRepoReconcilerFunc{
 | |
| 				func(ctx context.Context, obj *sourcev1.HelmRepository, artifact *sourcev1.Artifact, repo *repository.ChartRepository) (sreconcile.Result, error) {
 | |
| 					conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision")
 | |
| 					return sreconcile.ResultSuccess, nil
 | |
| 				},
 | |
| 				func(ctx context.Context, obj *sourcev1.HelmRepository, artifact *sourcev1.Artifact, repo *repository.ChartRepository) (sreconcile.Result, error) {
 | |
| 					conditions.MarkTrue(obj, meta.ReconcilingCondition, "Progressing", "creating artifact")
 | |
| 					return sreconcile.ResultSuccess, nil
 | |
| 				},
 | |
| 			},
 | |
| 			wantResult: sreconcile.ResultSuccess,
 | |
| 			wantErr:    false,
 | |
| 			assertConditions: []metav1.Condition{
 | |
| 				*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new index revision"),
 | |
| 				*conditions.TrueCondition(meta.ReconcilingCondition, "Progressing", "creating artifact"),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "subrecs with one result=Requeue, no error",
 | |
| 			reconcileFuncs: []helmRepoReconcilerFunc{
 | |
| 				buildReconcileFuncs(sreconcile.ResultSuccess, nil),
 | |
| 				buildReconcileFuncs(sreconcile.ResultRequeue, nil),
 | |
| 				buildReconcileFuncs(sreconcile.ResultSuccess, nil),
 | |
| 			},
 | |
| 			wantResult: sreconcile.ResultRequeue,
 | |
| 			wantErr:    false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "subrecs with error before result=Requeue",
 | |
| 			reconcileFuncs: []helmRepoReconcilerFunc{
 | |
| 				buildReconcileFuncs(sreconcile.ResultSuccess, nil),
 | |
| 				buildReconcileFuncs(sreconcile.ResultEmpty, fmt.Errorf("some error")),
 | |
| 				buildReconcileFuncs(sreconcile.ResultRequeue, nil),
 | |
| 			},
 | |
| 			wantResult: sreconcile.ResultEmpty,
 | |
| 			wantErr:    true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			g := NewWithT(t)
 | |
| 
 | |
| 			r := &HelmRepositoryReconciler{}
 | |
| 			obj := &sourcev1.HelmRepository{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					GenerateName: "test-",
 | |
| 					Generation:   tt.generation,
 | |
| 				},
 | |
| 				Status: sourcev1.HelmRepositoryStatus{
 | |
| 					ObservedGeneration: tt.observedGeneration,
 | |
| 				},
 | |
| 			}
 | |
| 
 | |
| 			ctx := context.TODO()
 | |
| 
 | |
| 			gotRes, gotErr := r.reconcile(ctx, obj, tt.reconcileFuncs)
 | |
| 			g.Expect(gotErr != nil).To(Equal(tt.wantErr))
 | |
| 			g.Expect(gotRes).To(Equal(tt.wantResult))
 | |
| 
 | |
| 			g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
 | |
| 		})
 | |
| 	}
 | |
| }
 |