diff --git a/docs/development/authentication.md b/docs/development/authentication.md index 25642cc83..8aef9e9d6 100644 --- a/docs/development/authentication.md +++ b/docs/development/authentication.md @@ -9,6 +9,7 @@ SPDX-License-Identifier: Apache-2.0 The following document provides an introduction around the different authentication methods that can take place during an image build when using the Build operator. - [Overview](#overview) +- [Build Secrets Annotation](#build-secrets-annotation) - [Authentication for Git](#authentication-for-git) - [Basic authentication](#basic-authentication) - [SSH authentication](#ssh-authentication) @@ -20,7 +21,32 @@ The following document provides an introduction around the different authenticat ## Overview -There are two places where users might need to define authentication when building images. Authentication to a container registry is the most common one, but also users might have the need to define authentications for pulling source-code from Git. +There are two places where users might need to define authentication when building images. Authentication to a container registry is the most common one, but also users might have the need to define authentications for pulling source-code from Git. Overall, the authentication is done via the definion of [secrets](https://kubernetes.io/docs/concepts/configuration/secret/) in which the require sensitive data will be stored. + +## Build Secrets Annotation + +Users need to add an annotation `build.build.dev/referenced.secret: "true"` to a build secret so that build controller can decide to take a reconcile action when a secret event (`create`, `update` and `delete`) happens. Below is a secret example with build annotation: + +```yaml +apiVersion: v1 +data: + .dockerconfigjson: xxxxx +kind: Secret +metadata: + annotations: + build.build.dev/referenced.secret: "true" + name: secret-docker +type: kubernetes.io/dockerconfigjson +``` + +This annotation will help us filter secrets which are not referenced on a Build instance. That means if a secret doesn't have this annotation, then although event happens on this secret, Build controller will not reconcile. Being able to reconcile on secrets events allow the Build controller to re-trigger validations on the Build configuration, allowing users to understand if a dependency is missing. + +If you are using `kubectl` command create secrets, then you can first create build secret using `kubectl create secret` command and annotate this secret using `kubectl annotate secrets`. Below is an example: + +```sh +kubectl -n ${namespace} create secret docker-registry example-secret --docker-server=${docker-server} --docker-username="${username}" --docker-password="${password}" --docker-email=me@here.com +kubectl -n ${namespace} annotate secrets example-secret build.build.dev/referenced.secret='true' +``` ## Authentication for Git @@ -44,6 +70,7 @@ metadata: annotations: tekton.dev/git-0: github.com tekton.dev/git-1: gitlab.com + build.build.dev/referenced.secret: "true" type: kubernetes.io/ssh-auth data: ssh-privatekey: @@ -64,6 +91,7 @@ metadata: annotations: tekton.dev/git-0: https://github.com tekton.dev/git-1: https://gitlab.com + build.build.dev/referenced.secret: "true" type: kubernetes.io/basic-auth stringData: username: @@ -118,6 +146,7 @@ kubectl --namespace create secret docker-registry \ --docker-password= \ --docker-email=me@here.com +kubectl --namespace annotate secrets build.build.dev/referenced.secret='true' ``` _Notes:_ When generating a secret to access docker hub, the `REGISTRY_HOST` value should be `https://index.docker.io/v1/`, the username is the Docker ID. diff --git a/pkg/apis/build/v1alpha1/build_types.go b/pkg/apis/build/v1alpha1/build_types.go index 830e43d3a..f9e3b0e24 100644 --- a/pkg/apis/build/v1alpha1/build_types.go +++ b/pkg/apis/build/v1alpha1/build_types.go @@ -18,6 +18,10 @@ const ( // AnnotationBuildRunDeletion is a label key for enabling/disabling the BuildRun deletion AnnotationBuildRunDeletion = "build.build.dev/build-run-deletion" + + // AnnotationBuildRefSecret is an annotation that tells the Build Controller to reconcile on + // events of the secret only if is referenced by a Build in the same namespace + AnnotationBuildRefSecret = "build.build.dev/referenced.secret" ) // BuildSpec defines the desired state of Build diff --git a/pkg/controller/build/build_controller.go b/pkg/controller/build/build_controller.go index 2a71660ea..c068395e5 100644 --- a/pkg/controller/build/build_controller.go +++ b/pkg/controller/build/build_controller.go @@ -17,6 +17,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -99,6 +100,96 @@ func add(ctx context.Context, mgr manager.Manager, r reconcile.Reconciler) error // Watch for changes to primary resource Build err = c.Watch(&source.Kind{Type: &build.Build{}}, &handler.EnqueueRequestForObject{}, pred) + + preSecret := predicate.Funcs{ + + // Only filter events where the secret have the Build specific annotation + CreateFunc: func(e event.CreateEvent) bool { + objectAnnotations := e.Meta.GetAnnotations() + if _, ok := buildSecretRefAnnotationExist(objectAnnotations); ok { + return true + } + return false + }, + + // Only filter events where the secret have the Build specific annotation, + // but only if the Build specific annotation changed + UpdateFunc: func(e event.UpdateEvent) bool { + oldAnnotations := e.MetaOld.GetAnnotations() + newAnnotations := e.MetaNew.GetAnnotations() + + if _, oldBuildKey := buildSecretRefAnnotationExist(oldAnnotations); !oldBuildKey { + if _, newBuildKey := buildSecretRefAnnotationExist(newAnnotations); newBuildKey { + return true + } + } + return false + }, + + // Only filter events where the secret have the Build specific annotation + DeleteFunc: func(e event.DeleteEvent) bool { + objectAnnotations := e.Meta.GetAnnotations() + if _, ok := buildSecretRefAnnotationExist(objectAnnotations); ok { + return true + } + return false + }, + } + + err = c.Watch(&source.Kind{Type: &corev1.Secret{}}, &handler.EnqueueRequestsFromMapFunc{ + ToRequests: handler.ToRequestsFunc(func(o handler.MapObject) []reconcile.Request { + + secret := o.Object.(*corev1.Secret) + + buildList := &build.BuildList{} + + // List all builds in the namespace of the current secret + if err := mgr.GetClient().List(ctx, buildList, &client.ListOptions{Namespace: secret.Namespace}); err != nil { + // Avoid entering into the Reconcile space + ctxlog.Info(ctx, "unexpected error happened while listing builds", namespace, secret.Namespace, "error", err) + return []reconcile.Request{} + } + + if len(buildList.Items) == 0 { + // Avoid entering into the Reconcile space + return []reconcile.Request{} + } + + // Only enter the Reconcile space if the secret is referenced on + // any Build in the same namespaces + + reconcileList := []reconcile.Request{} + flagReconcile := false + + for _, build := range buildList.Items { + if build.Spec.Source.SecretRef != nil { + if build.Spec.Source.SecretRef.Name == secret.Name { + flagReconcile = true + } + } + if build.Spec.Output.SecretRef != nil { + if build.Spec.Output.SecretRef.Name == secret.Name { + flagReconcile = true + } + } + if build.Spec.BuilderImage != nil && build.Spec.BuilderImage.SecretRef != nil { + if build.Spec.BuilderImage.SecretRef.Name == secret.Name { + flagReconcile = true + } + } + if flagReconcile { + reconcileList = append(reconcileList, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: build.Name, + Namespace: build.Namespace, + }, + }) + } + } + return reconcileList + }), + }, preSecret) + if err != nil { return err } @@ -164,8 +255,15 @@ func (r *ReconcileBuild) Reconcile(request reconcile.Request) (reconcile.Result, if len(secretNames) > 0 { if err := r.validateSecrets(ctx, secretNames, b.Namespace); err != nil { b.Status.Reason = err.Error() - updateErr := r.client.Status().Update(ctx, b) - return reconcile.Result{}, fmt.Errorf("errors: %v %v", err, updateErr) + if updateErr := r.client.Status().Update(ctx, b); updateErr != nil { + // return an error in case of transient failure, and expect the next + // reconciliation to be able to update the Status of the object + return reconcile.Result{}, fmt.Errorf("errors: %v %v", err, updateErr) + } + // The Secret Resource watcher will Reconcile again once the missing + // secret is created, therefore no need to return an error and enter on an infinite + // reconciliation + return reconcile.Result{}, nil } } @@ -275,7 +373,6 @@ func (r *ReconcileBuild) validateClusterBuildStrategy(ctx context.Context, n str } return nil } - func (r *ReconcileBuild) validateSecrets(ctx context.Context, secretNames []string, ns string) error { list := &corev1.SecretList{} @@ -393,3 +490,10 @@ func (r *ReconcileBuild) retrieveBuildRunsfromBuild(ctx context.Context, b *buil func removeOwnerReferenceByIndex(references []metav1.OwnerReference, i int) []metav1.OwnerReference { return append(references[:i], references[i+1:]...) } + +func buildSecretRefAnnotationExist(annotation map[string]string) (string, bool) { + if val, ok := annotation[build.AnnotationBuildRefSecret]; ok { + return val, true + } + return "", false +} diff --git a/pkg/controller/build/build_controller_test.go b/pkg/controller/build/build_controller_test.go index e463d6df8..afb977c45 100644 --- a/pkg/controller/build/build_controller_test.go +++ b/pkg/controller/build/build_controller_test.go @@ -103,9 +103,8 @@ var _ = Describe("Reconcile Build", func() { statusWriter.UpdateCalls(statusCall) _, err := reconciler.Reconcile(request) - Expect(err).To(HaveOccurred()) + Expect(err).To(BeNil()) Expect(statusWriter.UpdateCallCount()).To(Equal(1)) - Expect(err.Error()).To(ContainSubstring("secret non-existing does not exist")) }) It("succeeds when the secret exists", func() { @@ -166,9 +165,8 @@ var _ = Describe("Reconcile Build", func() { statusWriter.UpdateCalls(statusCall) _, err := reconciler.Reconcile(request) - Expect(err).To(HaveOccurred()) + Expect(err).To(BeNil()) Expect(statusWriter.UpdateCallCount()).To(Equal(1)) - Expect(err.Error()).To(ContainSubstring("secret non-existing does not exist")) }) It("succeeds when the secret exists", func() { @@ -225,9 +223,8 @@ var _ = Describe("Reconcile Build", func() { statusWriter.UpdateCalls(statusCall) _, err := reconciler.Reconcile(request) - Expect(err).To(HaveOccurred()) + Expect(err).To(BeNil()) Expect(statusWriter.UpdateCallCount()).To(Equal(1)) - Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("secret %s does not exist", registrySecret))) }) It("succeed when the secret exists", func() { @@ -270,9 +267,8 @@ var _ = Describe("Reconcile Build", func() { statusWriter.UpdateCalls(statusCall) _, err := reconciler.Reconcile(request) - Expect(err).To(HaveOccurred()) + Expect(err).To(BeNil()) Expect(statusWriter.UpdateCallCount()).To(Equal(1)) - Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("there are no secrets in namespace %s", namespace))) }) }) @@ -300,11 +296,8 @@ var _ = Describe("Reconcile Build", func() { }) _, err := reconciler.Reconcile(request) - Expect(err).To(HaveOccurred()) + Expect(err).To(BeNil()) Expect(statusWriter.UpdateCallCount()).To(Equal(1)) - Expect(err.Error()).To(ContainSubstring("do not exist")) - Expect(err.Error()).To(ContainSubstring("non-existing-source")) - Expect(err.Error()).To(ContainSubstring("non-existing-output")) }) }) diff --git a/test/build_samples.go b/test/build_samples.go index 5d74eb95a..44b7193c2 100644 --- a/test/build_samples.go +++ b/test/build_samples.go @@ -111,6 +111,80 @@ spec: name: fake-secret ` +// BuildWithOutputRefSecret defines a Build with a +// referenced secret under spec.output +const BuildWithOutputRefSecret = ` +apiVersion: build.dev/v1alpha1 +kind: Build +spec: + source: + url: "https://github.com/sbose78/taxi" + strategy: + kind: ClusterBuildStrategy + output: + image: image-registry.openshift-image-registry.svc:5000/example/buildpacks-app + credentials: + name: output-secret + timeout: 5s +` + +// BuildWithSourceRefSecret defines a Build with a +// referenced secret under spec.source +const BuildWithSourceRefSecret = ` +apiVersion: build.dev/v1alpha1 +kind: Build +spec: + source: + url: "https://github.com/sbose78/taxi" + credentials: + name: source-secret + strategy: + kind: ClusterBuildStrategy + output: + image: image-registry.openshift-image-registry.svc:5000/example/buildpacks-app + timeout: 5s +` + +// BuildWithBuilderRefSecret defines a Build with a +// referenced secret under spec.builder +const BuildWithBuilderRefSecret = ` +apiVersion: build.dev/v1alpha1 +kind: Build +spec: + source: + url: "https://github.com/sbose78/taxi" + builder: + image: heroku/buildpacks:18 + credentials: + name: builder-secret + strategy: + kind: ClusterBuildStrategy + output: + image: image-registry.openshift-image-registry.svc:5000/example/buildpacks-app + timeout: 5s +` + +// BuildWithMultipleRefSecrets defines a Build with +// multiple referenced secrets under spec +const BuildWithMultipleRefSecrets = ` +apiVersion: build.dev/v1alpha1 +kind: Build +spec: + source: + url: "https://github.com/sbose78/taxi" + credentials: + name: source-secret + builder: + image: heroku/buildpacks:18 + credentials: + name: builder-secret + strategy: + kind: ClusterBuildStrategy + output: + image: image-registry.openshift-image-registry.svc:5000/example/buildpacks-app + timeout: 5s +` + // BuildCBSWithShortTimeOut defines a Build with a // ClusterBuildStrategy and a short timeout const BuildCBSWithShortTimeOut = ` diff --git a/test/catalog.go b/test/catalog.go index 16df6890b..c7f722584 100644 --- a/test/catalog.go +++ b/test/catalog.go @@ -30,6 +30,27 @@ import ( // Catalog allows you to access helper functions type Catalog struct{} +// SecretWithAnnotation gives you a secret with build annotation +func (c *Catalog) SecretWithAnnotation(name string, ns string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + Annotations: map[string]string{build.AnnotationBuildRefSecret: "true"}, + }, + } +} + +// SecretWithoutAnnotation gives you a secret without build annotation +func (c *Catalog) SecretWithoutAnnotation(name string, ns string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + }, + } +} + // BuildWithClusterBuildStrategy gives you an specific Build CRD func (c *Catalog) BuildWithClusterBuildStrategy(name string, ns string, strategyName string, secretName string) *build.Build { buildStrategy := build.ClusterBuildStrategyKind @@ -176,7 +197,7 @@ func (c *Catalog) FakeSecretList() corev1.SecretList { } } -// FakeSecretListInNamespace to support test +// FakeNoSecretListInNamespace returns an empty secret list func (c *Catalog) FakeNoSecretListInNamespace() corev1.SecretList { return corev1.SecretList{ Items: []corev1.Secret{}, diff --git a/test/integration/build_to_secrets_test.go b/test/integration/build_to_secrets_test.go new file mode 100644 index 000000000..d8f5567d7 --- /dev/null +++ b/test/integration/build_to_secrets_test.go @@ -0,0 +1,515 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package integration_test + +import ( + "fmt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + "github.com/shipwright-io/build/test" + corev1 "k8s.io/api/core/v1" +) + +var _ = Describe("Integration tests Build and referenced Secrets", func() { + + var ( + cbsObject *v1alpha1.ClusterBuildStrategy + buildObject *v1alpha1.Build + ) + // Load the ClusterBuildStrategies before each test case + BeforeEach(func() { + cbsObject, err = tb.Catalog.LoadCBSWithName(STRATEGY+tb.Namespace, []byte(test.ClusterBuildStrategySingleStep)) + Expect(err).To(BeNil()) + + err = tb.CreateClusterBuildStrategy(cbsObject) + Expect(err).To(BeNil()) + }) + + // Delete the ClusterBuildStrategies after each test case + AfterEach(func() { + err := tb.DeleteClusterBuildStrategy(cbsObject.Name) + Expect(err).To(BeNil()) + }) + + Context("when a build reference a secret with annotations for the spec output", func() { + It("should validate the Build after secret deletion", func() { + + // populate Build related vars + buildName := BUILD + tb.Namespace + buildObject, err = tb.Catalog.LoadBuildWithNameAndStrategy( + buildName, + STRATEGY+tb.Namespace, + []byte(test.BuildWithOutputRefSecret), + ) + Expect(err).To(BeNil()) + + sampleSecret := tb.Catalog.SecretWithAnnotation(buildObject.Spec.Output.SecretRef.Name, buildObject.Namespace) + + Expect(tb.CreateSecret(sampleSecret)).To(BeNil()) + + Expect(tb.CreateBuild(buildObject)).To(BeNil()) + + // wait until the Build finish the validation + buildObject, err := tb.GetBuildTillValidation(buildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(buildObject.Status.Reason).To(Equal("Succeeded")) + + // delete a secret + Expect(tb.DeleteSecret(buildObject.Spec.Output.SecretRef.Name)).To(BeNil()) + + // assert that the validation happened one more time + buildObject, err = tb.GetBuildTillRegistration(buildName, corev1.ConditionFalse) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(buildObject.Status.Reason).To(Equal(fmt.Sprintf("secret %s does not exist", buildObject.Spec.Output.SecretRef.Name))) + + }) + + It("should validate when a missing secret is recreated", func() { + // populate Build related vars + buildName := BUILD + tb.Namespace + buildObject, err = tb.Catalog.LoadBuildWithNameAndStrategy( + buildName, + STRATEGY+tb.Namespace, + []byte(test.BuildCBSMinimalWithFakeSecret), + ) + Expect(err).To(BeNil()) + + Expect(tb.CreateBuild(buildObject)).To(BeNil()) + + // wait until the Build finish the validation + buildObject, err := tb.GetBuildTillValidation(buildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(buildObject.Status.Reason).To(Equal(fmt.Sprintf("secret %s does not exist", buildObject.Spec.Output.SecretRef.Name))) + + sampleSecret := tb.Catalog.SecretWithAnnotation(buildObject.Spec.Output.SecretRef.Name, buildObject.Namespace) + + // generate resources + Expect(tb.CreateSecret(sampleSecret)).To(BeNil()) + + // assert that the validation happened one more time + buildObject, err = tb.GetBuildTillRegistration(buildName, corev1.ConditionTrue) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(buildObject.Status.Reason).To(Equal("Succeeded")) + }) + }) + + Context("when a build reference a secret without annotations for the spec output", func() { + It("should not validate the Build after a secret deletion", func() { + + // populate Build related vars + buildName := BUILD + tb.Namespace + buildObject, err = tb.Catalog.LoadBuildWithNameAndStrategy( + buildName, + STRATEGY+tb.Namespace, + []byte(test.BuildWithOutputRefSecret), + ) + Expect(err).To(BeNil()) + + sampleSecret := tb.Catalog.SecretWithoutAnnotation(buildObject.Spec.Output.SecretRef.Name, buildObject.Namespace) + + // generate resources + Expect(tb.CreateSecret(sampleSecret)).To(BeNil()) + Expect(tb.CreateBuild(buildObject)).To(BeNil()) + + // wait until the Build finish the validation + buildObject, err := tb.GetBuildTillValidation(buildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(buildObject.Status.Reason).To(Equal("Succeeded")) + + // delete a secret + Expect(tb.DeleteSecret(buildObject.Spec.Output.SecretRef.Name)).To(BeNil()) + + // assert that the validation happened one more time + buildObject, err = tb.GetBuild(buildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionTrue)) + }) + + It("should not validate when a missing secret is recreated without annotation", func() { + // populate Build related vars + buildName := BUILD + tb.Namespace + buildObject, err = tb.Catalog.LoadBuildWithNameAndStrategy( + buildName, + STRATEGY+tb.Namespace, + []byte(test.BuildCBSMinimalWithFakeSecret), + ) + Expect(err).To(BeNil()) + + Expect(tb.CreateBuild(buildObject)).To(BeNil()) + + // wait until the Build finish the validation + buildObject, err := tb.GetBuildTillValidation(buildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(buildObject.Status.Reason).To(Equal(fmt.Sprintf("secret %s does not exist", buildObject.Spec.Output.SecretRef.Name))) + + sampleSecret := tb.Catalog.SecretWithoutAnnotation(buildObject.Spec.Output.SecretRef.Name, buildObject.Namespace) + + // generate resources + Expect(tb.CreateSecret(sampleSecret)).To(BeNil()) + + // // assert that the validation happened one more time + buildObject, err = tb.GetBuildTillRegistration(buildName, corev1.ConditionFalse) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(buildObject.Status.Reason).To(Equal(fmt.Sprintf("secret %s does not exist", buildObject.Spec.Output.SecretRef.Name))) + + }) + + It("should validate when a missing secret is recreated with annotation", func() { + // populate Build related vars + buildName := BUILD + tb.Namespace + buildObject, err = tb.Catalog.LoadBuildWithNameAndStrategy( + buildName, + STRATEGY+tb.Namespace, + []byte(test.BuildCBSMinimalWithFakeSecret), + ) + Expect(err).To(BeNil()) + + Expect(tb.CreateBuild(buildObject)).To(BeNil()) + + // wait until the Build finish the validation + buildObject, err := tb.GetBuildTillValidation(buildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(buildObject.Status.Reason).To(Equal(fmt.Sprintf("secret %s does not exist", "fake-secret"))) + + sampleSecret := tb.Catalog.SecretWithoutAnnotation(buildObject.Spec.Output.SecretRef.Name, buildObject.Namespace) + + // generate resources + Expect(tb.CreateSecret(sampleSecret)).To(BeNil()) + // validate build status again + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(buildObject.Status.Reason).To(Equal(fmt.Sprintf("secret %s does not exist", "fake-secret"))) + + // we modify the annotation so automatic delete does not take place + data := []byte(fmt.Sprintf(`{"metadata":{"annotations":{"%s":"true"}}}`, v1alpha1.AnnotationBuildRefSecret)) + + _, err = tb.PatchSecret(buildObject.Spec.Output.SecretRef.Name, data) + Expect(err).To(BeNil()) + + // // assert that the validation happened one more time + buildObject, err = tb.GetBuildTillRegistration(buildName, corev1.ConditionTrue) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(buildObject.Status.Reason).To(Equal("Succeeded")) + + }) + }) + + Context("when a build reference a secret with annotations for the spec source", func() { + It("should validate the Build after secret deletion", func() { + + // populate Build related vars + buildName := BUILD + tb.Namespace + buildObject, err = tb.Catalog.LoadBuildWithNameAndStrategy( + buildName, + STRATEGY+tb.Namespace, + []byte(test.BuildWithSourceRefSecret), + ) + Expect(err).To(BeNil()) + + sampleSecret := tb.Catalog.SecretWithAnnotation(buildObject.Spec.Source.SecretRef.Name, buildObject.Namespace) + + Expect(tb.CreateSecret(sampleSecret)).To(BeNil()) + + Expect(tb.CreateBuild(buildObject)).To(BeNil()) + + // wait until the Build finish the validation + buildObject, err := tb.GetBuildTillValidation(buildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(buildObject.Status.Reason).To(Equal("Succeeded")) + + // delete a secret + Expect(tb.DeleteSecret(buildObject.Spec.Source.SecretRef.Name)).To(BeNil()) + + // assert that the validation happened one more time + buildObject, err = tb.GetBuildTillRegistration(buildName, corev1.ConditionFalse) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(buildObject.Status.Reason).To(Equal(fmt.Sprintf("secret %s does not exist", buildObject.Spec.Source.SecretRef.Name))) + + }) + + It("should validate when a missing secret is recreated", func() { + // populate Build related vars + buildName := BUILD + tb.Namespace + buildObject, err = tb.Catalog.LoadBuildWithNameAndStrategy( + buildName, + STRATEGY+tb.Namespace, + []byte(test.BuildWithSourceRefSecret), + ) + Expect(err).To(BeNil()) + + Expect(tb.CreateBuild(buildObject)).To(BeNil()) + + // wait until the Build finish the validation + buildObject, err := tb.GetBuildTillValidation(buildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(buildObject.Status.Reason).To(Equal(fmt.Sprintf("secret %s does not exist", buildObject.Spec.Source.SecretRef.Name))) + + sampleSecret := tb.Catalog.SecretWithAnnotation(buildObject.Spec.Source.SecretRef.Name, buildObject.Namespace) + + // generate resources + Expect(tb.CreateSecret(sampleSecret)).To(BeNil()) + + // assert that the validation happened one more time + buildObject, err = tb.GetBuildTillRegistration(buildName, corev1.ConditionTrue) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(buildObject.Status.Reason).To(Equal("Succeeded")) + }) + }) + + Context("when a build reference a secret with annotations for the spec builder", func() { + It("should validate the Build after secret deletion", func() { + + // populate Build related vars + buildName := BUILD + tb.Namespace + buildObject, err = tb.Catalog.LoadBuildWithNameAndStrategy( + buildName, + STRATEGY+tb.Namespace, + []byte(test.BuildWithBuilderRefSecret), + ) + Expect(err).To(BeNil()) + + sampleSecret := tb.Catalog.SecretWithAnnotation(buildObject.Spec.BuilderImage.SecretRef.Name, buildObject.Namespace) + + Expect(tb.CreateSecret(sampleSecret)).To(BeNil()) + + Expect(tb.CreateBuild(buildObject)).To(BeNil()) + + // wait until the Build finish the validation + buildObject, err := tb.GetBuildTillValidation(buildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(buildObject.Status.Reason).To(Equal("Succeeded")) + + // delete a secret + Expect(tb.DeleteSecret(buildObject.Spec.BuilderImage.SecretRef.Name)).To(BeNil()) + + // assert that the validation happened one more time + buildObject, err = tb.GetBuildTillRegistration(buildName, corev1.ConditionFalse) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(buildObject.Status.Reason).To(Equal(fmt.Sprintf("secret %s does not exist", buildObject.Spec.BuilderImage.SecretRef.Name))) + + }) + + It("should validate when a missing secret is recreated", func() { + // populate Build related vars + buildName := BUILD + tb.Namespace + buildObject, err = tb.Catalog.LoadBuildWithNameAndStrategy( + buildName, + STRATEGY+tb.Namespace, + []byte(test.BuildWithBuilderRefSecret), + ) + Expect(err).To(BeNil()) + + Expect(tb.CreateBuild(buildObject)).To(BeNil()) + + // wait until the Build finish the validation + buildObject, err := tb.GetBuildTillValidation(buildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(buildObject.Status.Reason).To(Equal(fmt.Sprintf("secret %s does not exist", buildObject.Spec.BuilderImage.SecretRef.Name))) + + sampleSecret := tb.Catalog.SecretWithAnnotation(buildObject.Spec.BuilderImage.SecretRef.Name, buildObject.Namespace) + + // generate resources + Expect(tb.CreateSecret(sampleSecret)).To(BeNil()) + + // assert that the validation happened one more time + buildObject, err = tb.GetBuildTillRegistration(buildName, corev1.ConditionTrue) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(buildObject.Status.Reason).To(Equal("Succeeded")) + }) + }) + + Context("when a build reference multiple secrets with annotations for a build instance", func() { + It("should validate the Build after secret deletion", func() { + + // populate Build related vars + buildName := BUILD + tb.Namespace + buildObject, err = tb.Catalog.LoadBuildWithNameAndStrategy( + buildName, + STRATEGY+tb.Namespace, + []byte(test.BuildWithMultipleRefSecrets), + ) + Expect(err).To(BeNil()) + + specSourceSecret := tb.Catalog.SecretWithAnnotation(buildObject.Spec.Source.SecretRef.Name, buildObject.Namespace) + specBuilderSecret := tb.Catalog.SecretWithAnnotation(buildObject.Spec.BuilderImage.SecretRef.Name, buildObject.Namespace) + + Expect(tb.CreateSecret(specSourceSecret)).To(BeNil()) + Expect(tb.CreateSecret(specBuilderSecret)).To(BeNil()) + + Expect(tb.CreateBuild(buildObject)).To(BeNil()) + + // wait until the Build finish the validation + buildObject, err := tb.GetBuildTillValidation(buildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(buildObject.Status.Reason).To(Equal("Succeeded")) + + // delete a secret + Expect(tb.DeleteSecret(specSourceSecret.Name)).To(BeNil()) + Expect(tb.DeleteSecret(specBuilderSecret.Name)).To(BeNil()) + + buildObject, err = tb.GetBuildTillReasonContainsSubstring(buildName, "do not exist") + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(buildObject.Status.Reason).To(ContainSubstring(specSourceSecret.Name)) + Expect(buildObject.Status.Reason).To(ContainSubstring(specBuilderSecret.Name)) + + }) + + It("should validate when a missing secret is recreated", func() { + // populate Build related vars + buildName := BUILD + tb.Namespace + buildObject, err = tb.Catalog.LoadBuildWithNameAndStrategy( + buildName, + STRATEGY+tb.Namespace, + []byte(test.BuildWithMultipleRefSecrets), + ) + Expect(err).To(BeNil()) + + Expect(tb.CreateBuild(buildObject)).To(BeNil()) + + // wait until the Build finish the validation + buildObject, err := tb.GetBuildTillValidation(buildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(buildObject.Status.Reason).To(ContainSubstring("do not exist")) + Expect(buildObject.Status.Reason).To(ContainSubstring(buildObject.Spec.Source.SecretRef.Name)) + Expect(buildObject.Status.Reason).To(ContainSubstring(buildObject.Spec.BuilderImage.SecretRef.Name)) + + specSourceSecret := tb.Catalog.SecretWithAnnotation(buildObject.Spec.Source.SecretRef.Name, buildObject.Namespace) + specBuilderSecret := tb.Catalog.SecretWithAnnotation(buildObject.Spec.BuilderImage.SecretRef.Name, buildObject.Namespace) + + // generate resources + Expect(tb.CreateSecret(specSourceSecret)).To(BeNil()) + Expect(tb.CreateSecret(specBuilderSecret)).To(BeNil()) + + // assert that the validation happened one more time + buildObject, err = tb.GetBuildTillRegistration(buildName, corev1.ConditionTrue) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(buildObject.Status.Reason).To(Equal("Succeeded")) + }) + }) + Context("when multiple builds reference a secret with annotations for the spec.source", func() { + It("should validate the Builds after secret deletion", func() { + + // populate Build related vars + firstBuildName := BUILD + tb.Namespace + firstBuildObject, err := tb.Catalog.LoadBuildWithNameAndStrategy( + firstBuildName, + STRATEGY+tb.Namespace, + []byte(test.BuildWithSourceRefSecret), + ) + Expect(err).To(BeNil()) + + // populate Build related vars + secondBuildName := BUILD + tb.Namespace + "extra-build" + secondBuildObject, err := tb.Catalog.LoadBuildWithNameAndStrategy( + secondBuildName, + STRATEGY+tb.Namespace, + []byte(test.BuildWithSourceRefSecret), + ) + Expect(err).To(BeNil()) + + specSourceSecret := tb.Catalog.SecretWithAnnotation(firstBuildObject.Spec.Source.SecretRef.Name, firstBuildObject.Namespace) + + Expect(tb.CreateSecret(specSourceSecret)).To(BeNil()) + + Expect(tb.CreateBuild(firstBuildObject)).To(BeNil()) + Expect(tb.CreateBuild(secondBuildObject)).To(BeNil()) + + // wait until the Build finish the validation + o, err := tb.GetBuildTillValidation(firstBuildName) + Expect(err).To(BeNil()) + Expect(o.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(o.Status.Reason).To(Equal("Succeeded")) + + o, err = tb.GetBuildTillValidation(secondBuildName) + Expect(err).To(BeNil()) + Expect(o.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(o.Status.Reason).To(Equal("Succeeded")) + + // delete a secret + Expect(tb.DeleteSecret(specSourceSecret.Name)).To(BeNil()) + + // assert that the validation happened one more time + o, err = tb.GetBuildTillRegistration(firstBuildName, corev1.ConditionFalse) + Expect(err).To(BeNil()) + Expect(o.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(o.Status.Reason).To(Equal(fmt.Sprintf("secret %s does not exist", firstBuildObject.Spec.Source.SecretRef.Name))) + + // assert that the validation happened one more time + o, err = tb.GetBuildTillRegistration(secondBuildName, corev1.ConditionFalse) + Expect(err).To(BeNil()) + Expect(o.Status.Registered).To(Equal(corev1.ConditionFalse)) + Expect(o.Status.Reason).To(Equal(fmt.Sprintf("secret %s does not exist", secondBuildObject.Spec.Source.SecretRef.Name))) + }) + It("should validate the Builds when a missing secret is recreated", func() { + // populate Build related vars + firstBuildName := BUILD + tb.Namespace + firstBuildObject, err := tb.Catalog.LoadBuildWithNameAndStrategy( + firstBuildName, + STRATEGY+tb.Namespace, + []byte(test.BuildWithSourceRefSecret), + ) + Expect(err).To(BeNil()) + + // populate Build related vars + secondBuildName := BUILD + tb.Namespace + "extra-build" + secondBuildObject, err := tb.Catalog.LoadBuildWithNameAndStrategy( + secondBuildName, + STRATEGY+tb.Namespace, + []byte(test.BuildWithSourceRefSecret), + ) + Expect(err).To(BeNil()) + + Expect(tb.CreateBuild(firstBuildObject)).To(BeNil()) + Expect(tb.CreateBuild(secondBuildObject)).To(BeNil()) + + // wait until the Builds finish the validation + buildObject, err := tb.GetBuildTillValidation(firstBuildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + + buildObject, err = tb.GetBuildTillValidation(secondBuildName) + Expect(err).To(BeNil()) + Expect(buildObject.Status.Registered).To(Equal(corev1.ConditionFalse)) + + specSourceSecret := tb.Catalog.SecretWithAnnotation(firstBuildObject.Spec.Source.SecretRef.Name, firstBuildObject.Namespace) + + // generate resources + Expect(tb.CreateSecret(specSourceSecret)).To(BeNil()) + + // assert that the validation happened one more time + o, err := tb.GetBuildTillRegistration(firstBuildName, corev1.ConditionTrue) + Expect(err).To(BeNil()) + Expect(o.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(o.Status.Reason).To(Equal("Succeeded")) + + o, err = tb.GetBuildTillRegistration(secondBuildName, corev1.ConditionTrue) + Expect(err).To(BeNil()) + Expect(o.Status.Registered).To(Equal(corev1.ConditionTrue)) + Expect(o.Status.Reason).To(Equal("Succeeded")) + }) + }) +}) diff --git a/test/integration/utils/builds.go b/test/integration/utils/builds.go index 08ba9fa60..2b22d449f 100644 --- a/test/integration/utils/builds.go +++ b/test/integration/utils/builds.go @@ -6,9 +6,13 @@ package utils import ( "context" + "strings" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" ) @@ -54,3 +58,99 @@ func (t *TestBuild) PatchBuildWithPatchType(buildName string, data []byte, pt ty } return b, nil } + +// GetBuildTillValidation polls until a Build gets a validation and updates +// it´s registered field. If timeout is reached or an error is found, it will +// return with an error +func (t *TestBuild) GetBuildTillValidation(name string) (*v1alpha1.Build, error) { + + var ( + pollBuildTillRegistration = func() (bool, error) { + + bInterface := t.BuildClientSet.BuildV1alpha1().Builds(t.Namespace) + + buildRun, err := bInterface.Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return false, err + } + // TODO: we might improve the conditional here + if buildRun.Status.Registered != "" { + return true, nil + } + + return false, nil + } + ) + + brInterface := t.BuildClientSet.BuildV1alpha1().Builds(t.Namespace) + + if err := wait.PollImmediate(t.Interval, t.TimeOut, pollBuildTillRegistration); err != nil { + return nil, err + } + + return brInterface.Get(context.TODO(), name, metav1.GetOptions{}) +} + +// GetBuildTillRegistration polls until a Build gets a desired validation and updates +// it´s registered field. If timeout is reached or an error is found, it will +// return with an error +func (t *TestBuild) GetBuildTillRegistration(name string, condition corev1.ConditionStatus) (*v1alpha1.Build, error) { + + var ( + pollBuildTillRegistration = func() (bool, error) { + + bInterface := t.BuildClientSet.BuildV1alpha1().Builds(t.Namespace) + + buildRun, err := bInterface.Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return false, err + } + // TODO: we might improve the conditional here + if buildRun.Status.Registered == condition { + return true, nil + } + + return false, nil + } + ) + + brInterface := t.BuildClientSet.BuildV1alpha1().Builds(t.Namespace) + + if err := wait.PollImmediate(t.Interval, t.TimeOut, pollBuildTillRegistration); err != nil { + return nil, err + } + + return brInterface.Get(context.TODO(), name, metav1.GetOptions{}) +} + +// GetBuildTillReasonContainsSubstring polls until a Build reason contains the desired +// substring value and updates it´s registered field. If timeout is reached or an error is found, +// it will return with an error +func (t *TestBuild) GetBuildTillReasonContainsSubstring(name string, partOfReason string) (*v1alpha1.Build, error) { + + var ( + pollBuildTillReasonContainsSubString = func() (bool, error) { + + bInterface := t.BuildClientSet.BuildV1alpha1().Builds(t.Namespace) + + buildRun, err := bInterface.Get(context.TODO(), name, metav1.GetOptions{}) + if err != nil && !apierrors.IsNotFound(err) { + return false, err + } + + if strings.Contains(buildRun.Status.Reason, partOfReason) { + return true, nil + } + + return false, nil + } + ) + + brInterface := t.BuildClientSet.BuildV1alpha1().Builds(t.Namespace) + + if err := wait.PollImmediate(t.Interval, t.TimeOut, pollBuildTillReasonContainsSubString); err != nil { + return nil, err + } + + return brInterface.Get(context.TODO(), name, metav1.GetOptions{}) +} diff --git a/test/integration/utils/secrets.go b/test/integration/utils/secrets.go index 5821aadab..9bd8016b9 100644 --- a/test/integration/utils/secrets.go +++ b/test/integration/utils/secrets.go @@ -9,16 +9,42 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ) // This class is intended to host all CRUD calls for testing secrets primitive resources // CreateSecret generates a Secret on the current test namespace -func (t *TestBuild) CreateSecret(ns string, secret *corev1.Secret) error { - client := t.Clientset.CoreV1().Secrets(ns) +func (t *TestBuild) CreateSecret(secret *corev1.Secret) error { + client := t.Clientset.CoreV1().Secrets(t.Namespace) _, err := client.Create(context.TODO(), secret, metav1.CreateOptions{}) if err != nil { return err } return nil } + +// DeleteSecret removes the desired secret +func (t *TestBuild) DeleteSecret(name string) error { + client := t.Clientset.CoreV1().Secrets(t.Namespace) + if err := client.Delete(context.TODO(), name, metav1.DeleteOptions{}); err != nil { + return err + } + return nil +} + +// PatchSecret patches a secret based on name and with the provided data. +// It used the merge type strategy +func (t *TestBuild) PatchSecret(name string, data []byte) (*corev1.Secret, error) { + return t.PatchSecretWithPatchType(name, data, types.MergePatchType) +} + +// PatchSecretWithPatchType patches a secret with a desire data and patch strategy +func (t *TestBuild) PatchSecretWithPatchType(name string, data []byte, pt types.PatchType) (*corev1.Secret, error) { + secInterface := t.Clientset.CoreV1().Secrets(t.Namespace) + b, err := secInterface.Patch(context.TODO(), name, pt, data, metav1.PatchOptions{}) + if err != nil { + return nil, err + } + return b, nil +}