From c9f5af7ddcf6c14aae46860d36d56ec80c122e27 Mon Sep 17 00:00:00 2001 From: rashedkvm Date: Tue, 5 Jul 2022 13:52:05 +0300 Subject: [PATCH] Implements basic auth with static credentials OCIRepository Signed-off-by: rashedkvm --- controllers/ocirepository_controller.go | 71 ++- controllers/ocirepository_controller_test.go | 515 +++++++++++++++++- .../testdata/podinfo/podinfo-6.1.4.tar | Bin 0 -> 14848 bytes .../testdata/podinfo/podinfo-6.1.5.tar | Bin 0 -> 14848 bytes .../testdata/podinfo/podinfo-6.1.6.tar | Bin 0 -> 14848 bytes 5 files changed, 569 insertions(+), 17 deletions(-) create mode 100644 controllers/testdata/podinfo/podinfo-6.1.4.tar create mode 100644 controllers/testdata/podinfo/podinfo-6.1.5.tar create mode 100644 controllers/testdata/podinfo/podinfo-6.1.6.tar diff --git a/controllers/ocirepository_controller.go b/controllers/ocirepository_controller.go index fb7ad29c..0e441f8a 100644 --- a/controllers/ocirepository_controller.go +++ b/controllers/ocirepository_controller.go @@ -25,10 +25,14 @@ import ( "time" "github.com/Masterminds/semver/v3" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/authn/k8schain" "github.com/google/go-containerregistry/pkg/crane" gcrv1 "github.com/google/go-containerregistry/pkg/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/uuid" kuberecorder "k8s.io/client-go/tools/record" @@ -280,8 +284,16 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration) defer cancel() + // Generates registry credential keychain + keychain, err := r.keychain(ctx, obj) + if err != nil { + e := &serror.Event{Err: err, Reason: sourcev1.OCIOperationFailedReason} + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Error()) + return sreconcile.ResultEmpty, e + } + // Determine which artifact revision to pull - url, err := r.getArtifactURL(ctxTimeout, obj) + url, err := r.getArtifactURL(ctxTimeout, obj, keychain) if err != nil { e := &serror.Event{Err: err, Reason: sourcev1.OCIOperationFailedReason} conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Error()) @@ -289,7 +301,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour } // Pull artifact from the remote container registry - img, err := crane.Pull(url, r.craneOptions(ctxTimeout)...) + img, err := crane.Pull(url, r.craneOptions(ctxTimeout, keychain)...) if err != nil { e := &serror.Event{Err: err, Reason: sourcev1.OCIOperationFailedReason} conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Error()) @@ -352,7 +364,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour } // getArtifactURL determines which tag or digest should be used and returns the OCI artifact FQN. -func (r *OCIRepositoryReconciler) getArtifactURL(ctx context.Context, obj *sourcev1.OCIRepository) (string, error) { +func (r *OCIRepositoryReconciler) getArtifactURL(ctx context.Context, obj *sourcev1.OCIRepository, keychain authn.Keychain) (string, error) { url := obj.Spec.URL if obj.Spec.Reference != nil { if obj.Spec.Reference.Digest != "" { @@ -360,7 +372,7 @@ func (r *OCIRepositoryReconciler) getArtifactURL(ctx context.Context, obj *sourc } if obj.Spec.Reference.SemVer != "" { - tag, err := r.getTagBySemver(ctx, url, obj.Spec.Reference.SemVer) + tag, err := r.getTagBySemver(ctx, url, obj.Spec.Reference.SemVer, keychain) if err != nil { return "", err } @@ -377,8 +389,8 @@ func (r *OCIRepositoryReconciler) getArtifactURL(ctx context.Context, obj *sourc // getTagBySemver call the remote container registry, fetches all the tags from the repository, // and returns the latest tag according to the semver expression. -func (r *OCIRepositoryReconciler) getTagBySemver(ctx context.Context, url, exp string) (string, error) { - tags, err := crane.ListTags(url, r.craneOptions(ctx)...) +func (r *OCIRepositoryReconciler) getTagBySemver(ctx context.Context, url, exp string, keychain authn.Keychain) (string, error) { + tags, err := crane.ListTags(url, r.craneOptions(ctx, keychain)...) if err != nil { return "", err } @@ -408,11 +420,56 @@ func (r *OCIRepositoryReconciler) getTagBySemver(ctx context.Context, url, exp s return matchingVersions[0].Original(), nil } +// keychain generates the credential keychain based on the resource +// configuration. If no auth is specified a default keychain with +// anonymous access is returned +func (r *OCIRepositoryReconciler) keychain(ctx context.Context, obj *sourcev1.OCIRepository) (authn.Keychain, error) { + pullSecretNames := sets.NewString() + + // lookup auth secret + if obj.Spec.SecretRef != nil { + pullSecretNames.Insert(obj.Spec.SecretRef.Name) + } + + // lookup service account + if obj.Spec.ServiceAccountName != "" { + serviceAccountName := obj.Spec.ServiceAccountName + serviceAccount := corev1.ServiceAccount{} + err := r.Get(ctx, types.NamespacedName{Namespace: obj.Namespace, Name: serviceAccountName}, &serviceAccount) + if err != nil { + return nil, err + } + for _, ips := range serviceAccount.ImagePullSecrets { + pullSecretNames.Insert(ips.Name) + } + } + + // if no pullsecrets available return DefaultKeyChain + if len(pullSecretNames) == 0 { + return authn.DefaultKeychain, nil + } + + // lookup image pull secrets + imagePullSecrets := make([]corev1.Secret, len(pullSecretNames)) + for i, imagePullSecretName := range pullSecretNames.List() { + imagePullSecret := corev1.Secret{} + err := r.Get(ctx, types.NamespacedName{Namespace: obj.Namespace, Name: imagePullSecretName}, &imagePullSecret) + if err != nil { + r.eventLogf(ctx, obj, events.EventSeverityTrace, "secret %q not found", imagePullSecretName) + return nil, err + } + imagePullSecrets[i] = imagePullSecret + } + + return k8schain.NewFromPullSecrets(ctx, imagePullSecrets) +} + // craneOptions sets the timeout and user agent for all operations against remote container registries. -func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context) []crane.Option { +func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context, keychain authn.Keychain) []crane.Option { return []crane.Option{ crane.WithContext(ctx), crane.WithUserAgent("flux/v2"), + crane.WithAuthFromKeychain(keychain), } } diff --git a/controllers/ocirepository_controller_test.go b/controllers/ocirepository_controller_test.go index 044d8666..bcae3ad1 100644 --- a/controllers/ocirepository_controller_test.go +++ b/controllers/ocirepository_controller_test.go @@ -1,6 +1,27 @@ +/* +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 controllers import ( + "fmt" + "net/http/httptest" + "net/url" + "os" + "path" + "path/filepath" "testing" "time" @@ -8,8 +29,14 @@ import ( "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/runtime/patch" + "github.com/fluxcd/pkg/untar" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" . "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" kstatus "sigs.k8s.io/cli-utils/pkg/kstatus/status" @@ -17,24 +44,60 @@ import ( ) func TestOCIRepository_Reconcile(t *testing.T) { + g := NewWithT(t) + + // Registry server with public images + regServer := httptest.NewServer(registry.New()) + versions := []string{"6.1.4", "6.1.5", "6.1.6"} + podinfoVersions := make(map[string]podinfoImage) + + for i := 0; i < len(versions); i++ { + pi, err := createPodinfoImageFromTar(fmt.Sprintf("podinfo-%s.tar", versions[i]), versions[i], regServer) + g.Expect(err).ToNot(HaveOccurred()) + + podinfoVersions[versions[i]] = *pi + + } + tests := []struct { - name string - url string - tag string - semver string - digest string + name string + url string + tag string + semver string + digest string + assertArtifact []artifactFixture }{ { name: "public tag", - url: "ghcr.io/stefanprodan/manifests/podinfo", - tag: "6.1.6", - digest: "3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de", + url: podinfoVersions["6.1.6"].url, + tag: podinfoVersions["6.1.6"].tag, + digest: podinfoVersions["6.1.6"].digest.Hex, + assertArtifact: []artifactFixture{ + { + expectedPath: "kustomize/deployment.yaml", + expectedChecksum: "6fd625effe6bb805b6a78943ee082a4412e763edb7fcaed6e8fe644d06cbf423", + }, + { + expectedPath: "kustomize/hpa.yaml", + expectedChecksum: "d20e92e3b2926ebfee1644be0f4d0abadebfa95a8005c12f71bfd534a4be4ff9", + }, + }, }, { name: "public semver", - url: "ghcr.io/stefanprodan/manifests/podinfo", + url: podinfoVersions["6.1.5"].url, semver: ">= 6.1 <= 6.1.5", - digest: "1d1bf6980fc86f69481bd8c875c531aa23d761ac890ce2594d4df2b39ecd8713", + digest: podinfoVersions["6.1.5"].digest.Hex, + assertArtifact: []artifactFixture{ + { + expectedPath: "kustomize/deployment.yaml", + expectedChecksum: "dce4f5f780a8e8994b06031e5b567bf488ceaaaabd9bd3fc278b4f3bfc8c577b", + }, + { + expectedPath: "kustomize/hpa.yaml", + expectedChecksum: "d20e92e3b2926ebfee1644be0f4d0abadebfa95a8005c12f71bfd534a4be4ff9", + }, + }, }, } @@ -95,6 +158,36 @@ func TestOCIRepository_Reconcile(t *testing.T) { // Check if the revision matches the expected digest g.Expect(obj.Status.Artifact.Revision).To(Equal(tt.digest)) + // Check if the artifact storage path matches the expected file path + localPath := testStorage.LocalPath(*obj.Status.Artifact) + t.Logf("artifact local path: %s", localPath) + + f, err := os.Open(localPath) + g.Expect(err).ToNot(HaveOccurred()) + defer f.Close() + + // create a tmp directory to extract artifact + tmp, err := os.MkdirTemp("", "ocirepository-test-") + g.Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(tmp) + + ep, err := untar.Untar(f, tmp) + g.Expect(err).ToNot(HaveOccurred()) + t.Logf("extracted summary: %s", ep) + + for _, af := range tt.assertArtifact { + expectedFile := filepath.Join(tmp, af.expectedPath) + g.Expect(expectedFile).To(BeAnExistingFile()) + + f2, err := os.Open(expectedFile) + g.Expect(err).ToNot(HaveOccurred()) + defer f2.Close() + + h := testStorage.Checksum(f2) + t.Logf("file %q hash: %q", expectedFile, h) + g.Expect(h).To(Equal(af.expectedChecksum)) + } + // Check if the object status is valid condns := &status.Conditions{NegativePolarity: ociRepositoryReadyCondition.NegativePolarity} checker := status.NewChecker(testEnv.Client, condns) @@ -133,3 +226,405 @@ func TestOCIRepository_Reconcile(t *testing.T) { }) } } + +func TestOCIRepository_SecretRef(t *testing.T) { + g := NewWithT(t) + + // Instantiate Authenticated Registry Server + regServer, err := setupRegistryServer(ctx) + g.Expect(err).ToNot(HaveOccurred()) + + // Create Test Image + image, err := crane.Load(path.Join("testdata", "podinfo", "podinfo-6.1.6.tar")) + g.Expect(err).ToNot(HaveOccurred()) + + repositoryURL := fmt.Sprintf("%s/podinfo", regServer.registryHost) + + // Push Test Image + err = crane.Push(image, repositoryURL, crane.WithAuth(&authn.Basic{ + Username: testRegistryUsername, + Password: testRegistryPassword, + })) + g.Expect(err).ToNot(HaveOccurred()) + + // Test Image digest + podinfoImageDigest, err := image.Digest() + g.Expect(err).ToNot(HaveOccurred()) + + tests := []struct { + name string + url string + digest v1.Hash + includeSecretRef bool + includeServiceAccount bool + }{ + { + name: "private-registry-access-via-secretref", + url: repositoryURL, + digest: podinfoImageDigest, + includeSecretRef: true, + includeServiceAccount: false, + }, + { + name: "private-registry-access-via-serviceaccount", + url: repositoryURL, + digest: podinfoImageDigest, + includeSecretRef: false, + includeServiceAccount: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "ocirepository-test") + g.Expect(err).ToNot(HaveOccurred()) + defer func() { g.Expect(testEnv.Delete(ctx, ns)).To(Succeed()) }() + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "auth-secretref", + Namespace: ns.Name, + }, + Type: corev1.SecretTypeDockerConfigJson, + StringData: map[string]string{ + ".dockerconfigjson": fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`, tt.url, testRegistryUsername, testRegistryPassword), + }, + } + g.Expect(testEnv.CreateAndWait(ctx, secret)).To(Succeed()) + defer func() { g.Expect(testEnv.Delete(ctx, secret)).To(Succeed()) }() + + serviceAccount := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "sa-ocitest", + Namespace: ns.Name, + }, + ImagePullSecrets: []corev1.LocalObjectReference{{Name: secret.Name}}, + } + g.Expect(testEnv.CreateAndWait(ctx, serviceAccount)).To(Succeed()) + defer func() { g.Expect(testEnv.Delete(ctx, serviceAccount)).To(Succeed()) }() + + obj := &sourcev1.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "ocirepository-test-resource", + Namespace: ns.Name, + }, + Spec: sourcev1.OCIRepositorySpec{ + URL: tt.url, + Interval: metav1.Duration{Duration: 60 * time.Minute}, + Reference: &sourcev1.OCIRepositoryRef{Digest: tt.digest.String()}, + }, + } + + if tt.includeSecretRef { + obj.Spec.SecretRef = &meta.LocalObjectReference{Name: secret.Name} + } + + if tt.includeServiceAccount { + obj.Spec.ServiceAccountName = serviceAccount.Name + } + + g.Expect(testEnv.Create(ctx, obj)).To(Succeed()) + + key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace} + + // Wait for the 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(BeFalse()) + + // Wait for the object to be Ready + g.Eventually(func() bool { + if err := testEnv.Get(ctx, key, obj); err != nil { + return false + } + if !conditions.IsReady(obj) { + return false + } + readyCondition := conditions.Get(obj, meta.ReadyCondition) + return obj.Generation == readyCondition.ObservedGeneration && + obj.Generation == obj.Status.ObservedGeneration + }, timeout).Should(BeTrue()) + + t.Log(obj.Status.Artifact.Revision) + + // Check if the revision matches the expected digest + g.Expect(obj.Status.Artifact.Revision).To(Equal(tt.digest.Hex)) + + // Check if the artifact storage path matches the expected file path + localPath := testStorage.LocalPath(*obj.Status.Artifact) + t.Logf("artifact local path: %s", localPath) + + f, err := os.Open(localPath) + g.Expect(err).ToNot(HaveOccurred()) + defer f.Close() + + // create a tmp directory to extract artifact + tmp, err := os.MkdirTemp("", "ocirepository-test-") + g.Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(tmp) + + ep, err := untar.Untar(f, tmp) + g.Expect(err).ToNot(HaveOccurred()) + t.Logf("extracted summary: %s", ep) + + expectedFile := filepath.Join(tmp, `kustomize/deployment.yaml`) + g.Expect(expectedFile).To(BeAnExistingFile()) + + f2, err := os.Open(expectedFile) + g.Expect(err).ToNot(HaveOccurred()) + defer f2.Close() + + h := testStorage.Checksum(f2) + t.Logf("hash: %q", h) + g.Expect(h).To(Equal("6fd625effe6bb805b6a78943ee082a4412e763edb7fcaed6e8fe644d06cbf423")) + + // Check if the object status is valid + condns := &status.Conditions{NegativePolarity: ociRepositoryReadyCondition.NegativePolarity} + checker := status.NewChecker(testEnv.Client, condns) + checker.CheckErr(ctx, obj) + + // kstatus client conformance check + u, err := patch.ToUnstructured(obj) + g.Expect(err).ToNot(HaveOccurred()) + res, err := kstatus.Compute(u) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(res.Status).To(Equal(kstatus.CurrentStatus)) + + // 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()) + + // Wait for the object to be deleted + g.Expect(testEnv.Delete(ctx, obj)).To(Succeed()) + g.Eventually(func() bool { + if err := testEnv.Get(ctx, key, obj); err != nil { + return apierrors.IsNotFound(err) + } + return false + }, timeout).Should(BeTrue()) + + }) + } +} + +func TestOCIRepository_FailedAuth(t *testing.T) { + g := NewWithT(t) + + // Instantiate Authenticated Registry Server + regServer, err := setupRegistryServer(ctx) + g.Expect(err).ToNot(HaveOccurred()) + + // Create Test Image + image, err := crane.Load(path.Join("testdata", "podinfo", "podinfo-6.1.6.tar")) + g.Expect(err).ToNot(HaveOccurred()) + + repositoryURL := fmt.Sprintf("%s/podinfo", regServer.registryHost) + + // Push Test Image + err = crane.Push(image, repositoryURL, crane.WithAuth(&authn.Basic{ + Username: testRegistryUsername, + Password: testRegistryPassword, + })) + g.Expect(err).ToNot(HaveOccurred()) + + // Test Image digest + podinfoImageDigest, err := image.Digest() + g.Expect(err).ToNot(HaveOccurred()) + + tests := []struct { + name string + url string + digest v1.Hash + repoUsername string + repoPassword string + includeSecretRef bool + includeServiceAccount bool + }{ + { + name: "missing-auth", + url: repositoryURL, + repoUsername: "", + repoPassword: "", + digest: podinfoImageDigest, + includeSecretRef: false, + includeServiceAccount: false, + }, + { + name: "invalid-auth-via-secret", + url: repositoryURL, + repoUsername: "InvalidUser", + repoPassword: "InvalidPassword", + digest: podinfoImageDigest, + includeSecretRef: true, + includeServiceAccount: false, + }, + { + name: "invalid-auth-via-service-account", + url: repositoryURL, + repoUsername: "InvalidUser", + repoPassword: "InvalidPassword", + digest: podinfoImageDigest, + includeSecretRef: false, + includeServiceAccount: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + ns, err := testEnv.CreateNamespace(ctx, "ocirepository-test") + g.Expect(err).ToNot(HaveOccurred()) + defer func() { g.Expect(testEnv.Delete(ctx, ns)).To(Succeed()) }() + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "auth-secretref", + Namespace: ns.Name, + }, + Type: corev1.SecretTypeDockerConfigJson, + StringData: map[string]string{ + ".dockerconfigjson": fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`, tt.url, tt.repoUsername, tt.repoPassword), + }, + } + g.Expect(testEnv.CreateAndWait(ctx, secret)).To(Succeed()) + defer func() { g.Expect(testEnv.Delete(ctx, secret)).To(Succeed()) }() + + serviceAccount := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "sa-ocitest", + Namespace: ns.Name, + }, + ImagePullSecrets: []corev1.LocalObjectReference{{Name: secret.Name}}, + } + g.Expect(testEnv.CreateAndWait(ctx, serviceAccount)).To(Succeed()) + defer func() { g.Expect(testEnv.Delete(ctx, serviceAccount)).To(Succeed()) }() + + obj := &sourcev1.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "ocirepository-test-resource", + Namespace: ns.Name, + }, + Spec: sourcev1.OCIRepositorySpec{ + URL: tt.url, + Interval: metav1.Duration{Duration: 60 * time.Minute}, + Reference: &sourcev1.OCIRepositoryRef{Digest: tt.digest.String()}, + }, + } + + if tt.includeSecretRef { + obj.Spec.SecretRef = &meta.LocalObjectReference{Name: secret.Name} + } + + if tt.includeServiceAccount { + obj.Spec.ServiceAccountName = serviceAccount.Name + } + + g.Expect(testEnv.Create(ctx, obj)).To(Succeed()) + + key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace} + + failedObj := sourcev1.OCIRepository{} + + // Wait for the finalizer to be set + g.Eventually(func() bool { + if err := testEnv.Get(ctx, key, &failedObj); err != nil { + return false + } + return len(failedObj.Finalizers) > 0 + }, timeout).Should(BeTrue()) + + // Wait for the object to fail + g.Eventually(func() bool { + if err := testEnv.Get(ctx, key, &failedObj); err != nil { + return false + } + readyCondition := conditions.Get(&failedObj, meta.ReadyCondition) + if readyCondition == nil { + return false + } + return obj.Generation == readyCondition.ObservedGeneration && + !conditions.IsReady(&failedObj) + }, timeout).Should(BeTrue()) + + g.Expect(testEnv.Get(ctx, key, &failedObj)).To(Succeed()) + readyCondition := conditions.Get(&failedObj, meta.ReadyCondition) + g.Expect(readyCondition.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(readyCondition.Message).Should(ContainSubstring("UNAUTHORIZED: authentication required; [map[Action:pull Class: Name:podinfo Type:repository]]")) + + // Wait for the object to be deleted + g.Expect(testEnv.Delete(ctx, &failedObj)).To(Succeed()) + g.Eventually(func() bool { + if err := testEnv.Get(ctx, key, &failedObj); err != nil { + return apierrors.IsNotFound(err) + } + return false + }, timeout).Should(BeTrue()) + }) + } +} + +type artifactFixture struct { + expectedPath string + expectedChecksum string +} +type podinfoImage struct { + url string + tag string + digest v1.Hash +} + +func createPodinfoImageFromTar(tarFileName, tag string, imageServer *httptest.Server) (*podinfoImage, error) { + + // Create Image + image, err := crane.Load(path.Join("testdata", "podinfo", tarFileName)) + if err != nil { + return nil, err + } + + url, err := url.Parse(imageServer.URL) + if err != nil { + return nil, err + } + repositoryURL := fmt.Sprintf("%s/podinfo", url.Host) + + // Image digest + podinfoImageDigest, err := image.Digest() + if err != nil { + return nil, err + } + + // Push image + err = crane.Push(image, repositoryURL) + if err != nil { + return nil, err + } + + // Tag the image + err = crane.Tag(repositoryURL, tag) + if err != nil { + return nil, err + } + + return &podinfoImage{ + url: repositoryURL, + tag: tag, + digest: podinfoImageDigest, + }, nil +} diff --git a/controllers/testdata/podinfo/podinfo-6.1.4.tar b/controllers/testdata/podinfo/podinfo-6.1.4.tar new file mode 100644 index 0000000000000000000000000000000000000000..dbc58051dbcb9bd5d231b40d8df6e69b123986be GIT binary patch literal 14848 zcmeHOZ*SW+6VK~>3PQtx4XABWmSkB#UTjH+2J4z2P5J=OP!u#p*<2-2qiADpknjE+ z^)IsPI8N;L*;WYwY?H_1@s4-=-HE~WaO8~`afn0xiBEh&Bi~^Zdv?TF#IT2bpNu9E zr5?A3_6XbX(~n#?a;W3m{XcS<9&LXx!Ps-0BV0b~pLpn!y3J4=VH-QHeT49KwukAR z=b9>*eqV0ui`$pqR~mn7QN?CLbEfBtTY+W0u$bah^ALW(qDZg!yT&*u{{9ODGm&el zmR9il?PJ7u?_XFlFZn7dbp+pUExpXlTrpj8l>@_2M9~KkLf`LJKBKnl1tT0`MuuJ( z`nF?FScDypeb@CdCr-p|n>#+4OkfKRy~yUSi#;+KO)zF|Lf%?^*ne~gFq zo+$T%O$cn4F8*&*>Vf)5fA5p;K$3K9{vR6H+yD2+f5f&MY*_n0#t{F%%Fg`86*Yku8t7XrBr1~h*>6xgceVJ;t%%n&`u9D&+{dq3phSh62;c+#*4O2UP#Bm4Jzvelx zuj3Dj9IkJ2=m($e_>Xu6<~NbQd;tHG(XhjR59;%f9sR*aI00oMKJi1cUEyWS`lS4i zkRbv4clqCn8o$Vo#`=ShXR%x+Jk|XrP2vO2X6(PE{x`I}wfY}%hPdOZp(j&vs(7BC!I&5zVKdCb(3@)o^~dSbFa|C%29oJb z1`_bH3Ff1J-K2CuMNB6#$YX{fWXg&Gx@r$F?05{-O{vPU3Ds;?;;)(+towdNz=bDS z+|1QpyP1Qgsnk@%vhsR-u^ynC@p;}Ca*#7cjUbj*b8Z!qnUS#Q7EvY@D8m|$#-o}9 z)&i*Tp6i-iu#!sKhFgQCn*qI5+l0-;!T{F*O=nCM;%Oey6r>4hI;ad8czx1$R!3X{ z0KSy5V9NlVM{lIQ1d~B{XsFB*n%NPm=bvevH8d?jFN+nA{juNb1u?W_G7ix7>1EU2 zX4M0wDZ%ol^K+Jn4x_P>2FSQH+mltQ}Z+^M>aC83Q^y2l!Rl^LS1&!xs_kTBZ z+|h{DyBnccEO-iDf2ri8@nj<-|Er85wYZz8M&c3}&3hdiJR`PNYH$8lM>rH37*)tNPI*;gD-?=z|v~5D)(@OEc0e54N&g z&K2V=KR1SiNWRhn7R%-Vvf-ppS{j2u%@cT21W}*ZzqJjg_@8s0Z)5`RAU5;x9ANSA zQP%Z5Lj(8tZX zu;44H#3xg}13(b|RFQGD128oF_%&5iuCI7ho+B%BR+!Vo*w1jMX!`l%LW5sD!gFk>eY~tmr<+;O@LyGkX8y)&j9hO=mq>&NL%;TsPaaOSbQpg zUqy$|cc@q%nrBEvXr2SiLLx_WA{V?xG#-DCWP|`80wgZN9uLv!^|?)|_1RxI(Lk>D}^C*bUcrh1Mrak10Y|DR&UXVbQQvEBi^Ul!>5 zAN?YqSr8Ko>b&sRs$NvOLhVci8t9?=5P?12Lu(af9vo ztJRjZ5=N_H&`$BI6VUX+iFaQh*pz;kr*oODH(8#^42!Y%DRY~ymGf_6Swf70=m9Nt z&7B514Rjj#_h|qMnIeKh%r5u1pS$|)?|tp^8-oA$aiRX6yzrW&l;Bag0=Aq`lHi~zIhqx?wch|Y&4z}q?It_Fh K=rr&pHSj+Qtdi;g literal 0 HcmV?d00001 diff --git a/controllers/testdata/podinfo/podinfo-6.1.5.tar b/controllers/testdata/podinfo/podinfo-6.1.5.tar new file mode 100644 index 0000000000000000000000000000000000000000..335d6a5ad4fb20c1a81bf57d507be7c60dbeeae4 GIT binary patch literal 14848 zcmeHNZExE+63*xR3POtmZh_h+^=b*ohrOhS2G?tXH0c6c6a__5Hm{PXrDS8>Apd=b zdW-BjaZaleL*;n27l6Urv|GSr0Z_h8@JZ3K@7`wLJ#(47=JZH7ECjJC__7U1UCXX4_w*BPs zC*$wp<<{H7?+@mIz4QNnh0J!0X-EDO)9h+PTmL;1>i>6{nLl~{-x?w^0%PB4V7xGx z;X3S(F(~IYL9iVWC%h(v-W?4Si~9je``+sMt{2KPa>Wta+UI|9It4a9I!tf|3~(Jmg$o9{3o`B zA^rpYJ75qVP(^d`j;lh*EI>5Ri^-B41D1&bbk@y>)q9qNE5z^=^K)D1Y7R%!z z=S)*mkzH{bt=@9Lc2N-^3y6v@h1UE^K!s7O*CG{VfE;Mam7FPF6lWll03__hTnw#6 zsQ^jNR$4J=kqNNOZgQ}I*CgnV{&kblB^3z`6R<~%Aylq%c)scjXmvc{3=F7cQ(Cfl zjlWuDFz)*iffk z_n302z!`?``CiKcV*ylnuXV#N7)j%8t*t}Toxo^4Hc{8H(7-i7vpG|hc_oi&2G&G0 zn>2w8+%XyJ$kd-o;t~MxrA!1{1?W6}Bg;z&8I*^POAn#z9ig`Wd0FNiPtVZHYQ+B3 zpY~e8j8Zb01nBzovZMD^jlgM2u%c`HQYHAX*ZR21naWv@xpQM-Er+B9tArnwTn zdA95iwJrdc7iZ^h{(bTB=KSO7#p{c! zP8mW=nk@9}|HZN$&tvWEMkoH#6b6xU*lTWSgRL*r z!yjd6+M3nDMpnp$V!W551itEE_kt3 z37i&h*Wucr;(>K+Ab*_)?(yF|r-uf9jY&K3A7I0c^M7Kx-f;f+bpd^*)EfRDAR;m15oe^plVI9HKE1}y01|S33*+`s?r1~rYdQ*F>Mb}zp7R!^Od!2 zYl|vxN|A`q74U2L5c&mG!$XT4i5M*kfLTZsh=y{>drZFn4V z?&o4V1NgW;pdWvlR{NPH(Y2u67yhR?FRD|aO{NMBj8MCXz?>eTjT426*1*?lejR_? zsr_{h`N^cZm{0nVIjj?OUO#gFgKgq%F1=Ovu;@uw*Y}Xn zl{Lhum>%%bKps3Wcwq3r|DOk#J+-)R5{F`JhB$_MpfT}dY)6cS6f^i@?pdbgnV#j+ zID$8GoME^nYVs+Mr{n$a|6t46c>iM(><;+)S9b(p+1@uZjk~)c9iI@Jfo1T(;DNyd-_iqr1CU#khyVZp literal 0 HcmV?d00001 diff --git a/controllers/testdata/podinfo/podinfo-6.1.6.tar b/controllers/testdata/podinfo/podinfo-6.1.6.tar new file mode 100644 index 0000000000000000000000000000000000000000..09616c2dfabc3ca74b251763d2de655d892e2629 GIT binary patch literal 14848 zcmeHNZExE)5YFfP3PQzz4XADE{RPN}E$Pr;SskQF2dpRpTB2;OlBiL%Q8&nc-%)Ro zT_;XzHz-yK0U{soj>kLR@gYSo@;vIhB-F9%IuUkE98Mw&`_#r5$2z7_>>8o1o4V&< z2UFK`V$U>VOfBM?=ICJigRzBO+dcq=>RJEarB&~`YwN}VvbU?=onu}|BEY0wH7U#C zX$QBfzGss6c8&kLR~K(j$8R1o7wZ^1mesnZ@>J_8_-Qt6l?x4Q+xP(O9FvEPYTADA z`0K-G@p9ws{{MI8ft~aJpMqyw#b;D?DLzDj&hWh_3GxG<}{~L`(TA=MZ4YZe9 zC@7IsDT5&DgYxeZQU-2J{PsHj?g zm6C+Bw=!ax7Q`ee=u54o@wxnxNCk-2oR@OhzK?U7t?DU%jk6GpsQF)I3oXdzN%B%V zPGLmV>uE}+wW05LGEZsIwEV@zN0v=bSZ(dAOp0aBSq64hvg7QxIZIkqXJkT?TKu;3 zOz)4@AGce|N#DFj5A2%wql7G}7%B{X@AGi|*Bz)AYxqw$%^v^V>(6~Q3`ZYf1>`CF zO!ujFMVDICpSJ&l-KO2||5lv%jefMzA4N1z_%fxL94<+k?6E%;{|EMers3$T`LA0h zhWPjRZ;wH^M-|E0J1PpsvjCAiFGdUf5U@-Xpp$k!9Hvx~h)5C~BBan+fN~BY191N|6 z6o4eBOQjgJ@B~<cWN*zsT2nJNLDUso< z!e5Ot828CTcro3b+PnItxWC?};B%g^a2cS}_zjom5HctaEteWX+dD!{|1&A`mZxLrc{yU=^Sw?h zm?3${lK@>FpSSd`ssT7nu&`(wzf=i6?6f|taw_t$!`!;Dw$AtcPU~h>Ax~2RDbg|y z(6Di2hc*}}2OX^pcz`NuGpHjK{}mZ!UoF_RMG2Nouq9fskPcKH9yMvGCWLwvS3(yw zIV!WWB35Jp7V~rvQ2=a5 z(eUFMZEX%JwuFZynZbTDP!So;C`sh(b4QM4Ofr(3C=ay-NmZ-nku0UK*WAzsTVJS$ z-^C=wKpit8kN7$oKOsTw}akY-D^jTw0Rb#)vor<`M zH*2Js11ceaG7YFT00l1qs#XL`5h}c(`WiHlkXBW!EKPu7%92(Y)ARuKt84}QS6bV& zHmLGSvV?sufnUvs&jjn951jvCOLy1u-?W|n{Aa&GxJO$z|J$RVx78l=5zBjZ;&Plo zO`zgbZQxf=Th@*+IwuC*Eq=WMs#aL>?hXhxZNID4x!SF_yF9fsEM@Pu%w4|Do_~|d z3SyK@_jsu%_a5jy(0kzj=K<=5wxMG`_NWQBE?tLso)<^9t4ATGcw)f?P>*;x@=dH8 z*t1-V7{tfU1QR0)!{P4tf3RtEF&xA`CXY93*iL=;{s-LtUC)1AcX5CIx81RO;D8K1u5aUMdsz!0o8dT)Uy^B= z%^N>DHj}PszM55^A2p~IXX-jpDY?I!Nl*$t^hXVwCyRhs6%4+PGg@rs*qyp9{~_Dg y_W!zL_xZoO;~$~nMtfX_s#UwZBLK_xu9>Ob-uCJEh}iTjy$5;^^d9(@9{3LiHH=>X literal 0 HcmV?d00001