diff --git a/README.md b/README.md index 5561242ac..6171bfc00 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,21 @@ spec: The operator will deploy Shipwright Builds in the provided `targetNamespace`. When `.spec.targetNamespace` is not set, the namespace will default to `shipwright-build`. + +To deploy [Shipwright Triggers](https://github.com/shipwright-io/triggers), add the `triggers` field: + +```yaml +--- +apiVersion: operator.shipwright.io/v1alpha1 +kind: ShipwrightBuild +metadata: + name: shipwright-operator +spec: + targetNamespace: shipwright-build + triggers: + deployment: Enabled +``` + Refer to the [ShipwrightBuild documentation](docs/shipwrightbuild.md) for more information about this custom resource. The operator handles differents environment variables to customize Shiprwright controller installation: @@ -52,6 +67,7 @@ The operator handles differents environment variables to customize Shiprwright c - IMAGE_SHIPWRIGHT_BUNDLE_CONTAINER_IMAGE: defines the Shipwright Bundle Image to use - IMAGE_SHIPWRIGHT_WAITER_CONTAINER_IMAGE: defines the Shipwright Waiter Image to use - IMAGE_SHIPWRIGHT_SHIPWRIGHT_BUILD_WEBHOOK: defines the Shipwright Build Webhook Image to use +- IMAGE_SHIPWRIGHT_SHIPWRIGHT_TRIGGERS: defines the Shipwright Triggers Image to use For more information about the function of these images, please consider the Shipwright Build doc https://github.com/shipwright-io/build/blob/main/docs/configuration.md diff --git a/api/v1alpha1/shipwrightbuild_types.go b/api/v1alpha1/shipwrightbuild_types.go index f7910f147..3e7c87453 100644 --- a/api/v1alpha1/shipwrightbuild_types.go +++ b/api/v1alpha1/shipwrightbuild_types.go @@ -8,10 +8,47 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// TriggersDeployment indicates whether the Shipwright Triggers component +// should be deployed. +// +kubebuilder:validation:Enum=Enabled;Disabled +type TriggersDeployment string + +const ( + // TriggersDeploymentEnabled indicates that the Shipwright Triggers + // component should be deployed. + TriggersDeploymentEnabled TriggersDeployment = "Enabled" + // TriggersDeploymentDisabled indicates that the Shipwright Triggers + // component should not be deployed. + TriggersDeploymentDisabled TriggersDeployment = "Disabled" +) + +// TriggersSpec defines the desired state of the Triggers component. +type TriggersSpec struct { + // Deployment controls whether the triggers component is deployed. + // Defaults to "Disabled". + // +kubebuilder:default=Disabled + // +optional + Deployment TriggersDeployment `json:"deployment,omitempty"` +} + // ShipwrightBuildSpec defines the configuration of a Shipwright Build deployment. type ShipwrightBuildSpec struct { // TargetNamespace is the target namespace where Shipwright's build controller will be deployed. TargetNamespace string `json:"targetNamespace,omitempty"` + + // Triggers configures the deployment of the Shipwright Triggers component. + // When omitted, triggers are not deployed. + // +optional + Triggers *TriggersSpec `json:"triggers,omitempty"` +} + +// TriggersEnabled returns true if the Triggers component should be deployed. +// Triggers are only deployed when spec.triggers.deployment is set to "Enabled". +func (s *ShipwrightBuildSpec) TriggersEnabled() bool { + if s.Triggers == nil { + return false + } + return s.Triggers.Deployment == TriggersDeploymentEnabled } // ShipwrightBuildStatus defines the observed state of ShipwrightBuild diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 68890bb47..d56ea3972 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -18,7 +18,7 @@ func (in *ShipwrightBuild) DeepCopyInto(out *ShipwrightBuild) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -75,6 +75,11 @@ func (in *ShipwrightBuildList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ShipwrightBuildSpec) DeepCopyInto(out *ShipwrightBuildSpec) { *out = *in + if in.Triggers != nil { + in, out := &in.Triggers, &out.Triggers + *out = new(TriggersSpec) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShipwrightBuildSpec. @@ -108,3 +113,18 @@ func (in *ShipwrightBuildStatus) DeepCopy() *ShipwrightBuildStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TriggersSpec) DeepCopyInto(out *TriggersSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TriggersSpec. +func (in *TriggersSpec) DeepCopy() *TriggersSpec { + if in == nil { + return nil + } + out := new(TriggersSpec) + in.DeepCopyInto(out) + return out +} diff --git a/bundle/manifests/operator.shipwright.io_shipwrightbuilds.yaml b/bundle/manifests/operator.shipwright.io_shipwrightbuilds.yaml index 90841897f..5491dd09a 100644 --- a/bundle/manifests/operator.shipwright.io_shipwrightbuilds.yaml +++ b/bundle/manifests/operator.shipwright.io_shipwrightbuilds.yaml @@ -45,6 +45,21 @@ spec: description: TargetNamespace is the target namespace where Shipwright's build controller will be deployed. type: string + triggers: + description: |- + Triggers configures the deployment of the Shipwright Triggers component. + When omitted, triggers are not deployed. + properties: + deployment: + default: Disabled + description: |- + Deployment controls whether the triggers component is deployed. + Defaults to "Disabled". + enum: + - Enabled + - Disabled + type: string + type: object type: object status: description: ShipwrightBuildStatus defines the observed state of ShipwrightBuild diff --git a/bundle/manifests/shipwright-operator.clusterserviceversion.yaml b/bundle/manifests/shipwright-operator.clusterserviceversion.yaml index 714270bc4..b4497db9d 100644 --- a/bundle/manifests/shipwright-operator.clusterserviceversion.yaml +++ b/bundle/manifests/shipwright-operator.clusterserviceversion.yaml @@ -325,6 +325,17 @@ spec: - delete - patch - update + - apiGroups: + - "" + resourceNames: + - shipwright-triggers + resources: + - serviceaccounts + - services + verbs: + - delete + - patch + - update - apiGroups: - admissionregistration.k8s.io - admissionregistration.k8s.io/v1beta1 @@ -389,6 +400,16 @@ spec: - delete - patch - update + - apiGroups: + - apps + resourceNames: + - shipwright-triggers + resources: + - deployments + verbs: + - delete + - patch + - update - apiGroups: - apps resourceNames: @@ -405,6 +426,14 @@ spec: - deployments/finalizers verbs: - update + - apiGroups: + - apps + resourceNames: + - shipwright-triggers + resources: + - deployments/finalizers + verbs: + - update - apiGroups: - cert-manager.io resourceNames: @@ -505,6 +534,19 @@ spec: - delete - patch - update + - apiGroups: + - rbac.authorization.k8s.io + resourceNames: + - shipwright-triggers + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - delete + - patch + - update - apiGroups: - rbac.authorization.k8s.io resourceNames: @@ -549,6 +591,50 @@ spec: - subjectaccessreviews verbs: - create + - apiGroups: + - shipwright.io + resources: + - buildruns + verbs: + - create + - get + - list + - update + - watch + - apiGroups: + - shipwright.io + resources: + - builds + verbs: + - get + - list + - watch + - apiGroups: + - tekton.dev + resources: + - customruns + verbs: + - get + - list + - watch + - apiGroups: + - tekton.dev + resources: + - customruns/finalizers + - customruns/status + verbs: + - patch + - update + - apiGroups: + - tekton.dev + resources: + - pipelineruns + verbs: + - get + - list + - patch + - update + - watch serviceAccountName: shipwright-operator deployments: - label: diff --git a/config/crd/bases/operator.shipwright.io_shipwrightbuilds.yaml b/config/crd/bases/operator.shipwright.io_shipwrightbuilds.yaml index 8341ab0bd..862dc642d 100644 --- a/config/crd/bases/operator.shipwright.io_shipwrightbuilds.yaml +++ b/config/crd/bases/operator.shipwright.io_shipwrightbuilds.yaml @@ -45,6 +45,21 @@ spec: description: TargetNamespace is the target namespace where Shipwright's build controller will be deployed. type: string + triggers: + description: |- + Triggers configures the deployment of the Shipwright Triggers component. + When omitted, triggers are not deployed. + properties: + deployment: + default: Disabled + description: |- + Deployment controls whether the triggers component is deployed. + Defaults to "Disabled". + enum: + - Enabled + - Disabled + type: string + type: object type: object status: description: ShipwrightBuildStatus defines the observed state of ShipwrightBuild diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 4f6af73ff..5b41b67a7 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -20,3 +20,5 @@ resources: - shipwright_build_aggregate_role_binding.yaml - shipwright_build_webhook_role.yaml - shipwright_build_webhook_role_binding.yaml +- shipwright_triggers_controller_role.yaml +- shipwright_triggers_controller_role_binding.yaml diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index fccf7b657..2d91e92eb 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -51,6 +51,17 @@ rules: - delete - patch - update +- apiGroups: + - "" + resourceNames: + - shipwright-triggers + resources: + - serviceaccounts + - services + verbs: + - delete + - patch + - update - apiGroups: - admissionregistration.k8s.io - admissionregistration.k8s.io/v1beta1 @@ -115,6 +126,16 @@ rules: - delete - patch - update +- apiGroups: + - apps + resourceNames: + - shipwright-triggers + resources: + - deployments + verbs: + - delete + - patch + - update - apiGroups: - apps resourceNames: @@ -131,6 +152,14 @@ rules: - deployments/finalizers verbs: - update +- apiGroups: + - apps + resourceNames: + - shipwright-triggers + resources: + - deployments/finalizers + verbs: + - update - apiGroups: - cert-manager.io resourceNames: @@ -231,6 +260,19 @@ rules: - delete - patch - update +- apiGroups: + - rbac.authorization.k8s.io + resourceNames: + - shipwright-triggers + resources: + - clusterrolebindings + - clusterroles + - rolebindings + - roles + verbs: + - delete + - patch + - update - apiGroups: - rbac.authorization.k8s.io resourceNames: diff --git a/config/rbac/shipwright_triggers_controller_role.yaml b/config/rbac/shipwright_triggers_controller_role.yaml new file mode 100644 index 000000000..cb2dd9e1d --- /dev/null +++ b/config/rbac/shipwright_triggers_controller_role.yaml @@ -0,0 +1,20 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: triggers-controller +rules: + - apiGroups: ['shipwright.io'] + resources: ['buildruns'] + verbs: ['create', 'get', 'list', 'update', 'watch'] + - apiGroups: ['shipwright.io'] + resources: ['builds'] + verbs: ['get', 'list', 'watch'] + - apiGroups: ['tekton.dev'] + resources: ['customruns'] + verbs: ['get', 'list', 'watch'] + - apiGroups: ['tekton.dev'] + resources: ['customruns/finalizers', 'customruns/status'] + verbs: ['patch', 'update'] + - apiGroups: ['tekton.dev'] + resources: ['pipelineruns'] + verbs: ['get', 'list', 'patch', 'update', 'watch'] diff --git a/config/rbac/shipwright_triggers_controller_role_binding.yaml b/config/rbac/shipwright_triggers_controller_role_binding.yaml new file mode 100644 index 000000000..4b216b217 --- /dev/null +++ b/config/rbac/shipwright_triggers_controller_role_binding.yaml @@ -0,0 +1,12 @@ +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: triggers-controller +roleRef: + kind: ClusterRole + name: triggers-controller + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: ServiceAccount + name: operator + namespace: system diff --git a/config/samples/operator_v1alpha1_shipwrightbuild_with_triggers.yaml b/config/samples/operator_v1alpha1_shipwrightbuild_with_triggers.yaml new file mode 100644 index 000000000..eec21f5d1 --- /dev/null +++ b/config/samples/operator_v1alpha1_shipwrightbuild_with_triggers.yaml @@ -0,0 +1,8 @@ +apiVersion: operator.shipwright.io/v1alpha1 +kind: ShipwrightBuild +metadata: + name: shipwright-build +spec: + targetNamespace: shipwright-build + triggers: + deployment: Enabled diff --git a/controllers/buildstrategies_test.go b/controllers/buildstrategies_test.go index 18099bf26..a3e970dee 100644 --- a/controllers/buildstrategies_test.go +++ b/controllers/buildstrategies_test.go @@ -20,7 +20,7 @@ var _ = Describe("Install embedded build strategies", func() { BeforeEach(func(ctx SpecContext) { setupTektonCRDs(ctx) createTektonConfig(ctx) - build = createShipwrightBuild(ctx, "shipwright") + build = createShipwrightBuild(ctx, "cluster", "shipwright") test.CRDEventuallyExists(ctx, k8sClient, "clusterbuildstrategies.shipwright.io") }) diff --git a/controllers/default_test.go b/controllers/default_test.go index 56fcbab60..4c33cc46a 100644 --- a/controllers/default_test.go +++ b/controllers/default_test.go @@ -48,7 +48,7 @@ var _ = g.Describe("Reconcile default ShipwrightBuild installation", func() { // setting up the namespaces, where Shipwright Controller will be deployed setupTektonCRDs(ctx) createTektonConfig(ctx) - build = createShipwrightBuild(ctx, targetNamespace) + build = createShipwrightBuild(ctx, "cluster", targetNamespace) }) g.AfterEach(func(ctx g.SpecContext) { diff --git a/controllers/shipwrightbuild_controller.go b/controllers/shipwrightbuild_controller.go index 6c7dfaa4b..c9b3ef704 100644 --- a/controllers/shipwrightbuild_controller.go +++ b/controllers/shipwrightbuild_controller.go @@ -32,6 +32,7 @@ import ( "github.com/shipwright-io/operator/pkg/certmanager" "github.com/shipwright-io/operator/pkg/common" "github.com/shipwright-io/operator/pkg/tekton" + "github.com/shipwright-io/operator/pkg/triggers" ) const ( @@ -62,6 +63,7 @@ type ShipwrightBuildReconciler struct { Manifest manifestival.Manifest // release manifests render TektonManifest manifestival.Manifest // Tekton release manifest render BuildStrategyManifest manifestival.Manifest // Build strategies manifest to render + TriggersManifest manifestival.Manifest // Triggers manifest to render } type TektonCheckResult struct { @@ -312,6 +314,12 @@ func (r *ShipwrightBuildReconciler) Reconcile(ctx context.Context, req ctrl.Requ logger.Info("Finalizers removed, deletion of manifests completed!") return NoRequeue() } + logger.Info("Deleting triggers resources") + if err := r.deleteTriggersManifest(targetNamespace); err != nil { + logger.Error(err, "deleting triggers resources") + return RequeueWithError(err) + } + logger.Info("Deleting cluster build strategies") if err := r.BuildStrategyManifest.Delete(); err != nil { logger.Error(err, "deleting cluster build strategies") @@ -386,6 +394,46 @@ func (r *ShipwrightBuildReconciler) Reconcile(ctx context.Context, req ctrl.Requ return Requeue() } + // Reconcile triggers + if b.Spec.TriggersEnabled() { + triggersManifest, err := r.TriggersManifest. + Filter(manifestival.Not(manifestival.ByKind("Namespace"))). + Transform( + // TODO: Remove this when we remove the target namespace feature. + // See https://github.com/shipwright-io/operator/issues/241 + manifestival.InjectNamespace(targetNamespace), + common.DeploymentImages(images), + ) + if err != nil { + logger.Error(err, "transforming triggers manifests") + return RequeueWithError(err) + } + + requeue, err = triggers.ReconcileTriggers(ctx, r.CRDClient, logger, triggersManifest) + if err != nil { + logger.Error(err, "reconcile triggers") + return RequeueWithError(err) + } + if requeue { + logger.Info("requeue waiting for triggers preconditions") + apimeta.SetStatusCondition(&b.Status.Conditions, metav1.Condition{ + Type: ConditionReady, + Status: metav1.ConditionUnknown, + Reason: "TriggersWaiting", + Message: "Waiting for triggers preconditions to be met", + }) + if updateErr := r.Client.Status().Update(ctx, b); updateErr != nil { + return RequeueWithError(updateErr) + } + return Requeue() + } + } else { + if err := r.deleteTriggersManifest(targetNamespace); err != nil { + logger.Error(err, "cleaning up triggers resources") + return RequeueWithError(err) + } + } + apimeta.SetStatusCondition(&b.Status.Conditions, metav1.Condition{ Type: ConditionReady, Status: metav1.ConditionTrue, @@ -400,6 +448,19 @@ func (r *ShipwrightBuildReconciler) Reconcile(ctx context.Context, req ctrl.Requ return NoRequeue() } +// deleteTriggersManifest deletes the triggers resources in the given namespace. +func (r *ShipwrightBuildReconciler) deleteTriggersManifest(targetNamespace string) error { + triggersManifest, err := r.TriggersManifest. + Filter(manifestival.Not(manifestival.ByKind("Namespace"))). + // TODO: Remove this when we remove the target namespace feature. + // See https://github.com/shipwright-io/operator/issues/241 + Transform(manifestival.InjectNamespace(targetNamespace)) + if err != nil { + return err + } + return triggersManifest.Delete() +} + // setupManifestival instantiate manifestival with local controller attributes, as well as tekton prereqs. func (r *ShipwrightBuildReconciler) setupManifestival() error { var err error @@ -411,6 +472,10 @@ func (r *ShipwrightBuildReconciler) setupManifestival() error { if err != nil { return err } + r.TriggersManifest, err = common.SetupManifestival(r.Client, "triggers-release.yaml", false, r.Logger) + if err != nil { + return err + } return nil } diff --git a/controllers/shipwrightbuild_rbac.go b/controllers/shipwrightbuild_rbac.go index be7e9b89b..85b7b2f74 100644 --- a/controllers/shipwrightbuild_rbac.go +++ b/controllers/shipwrightbuild_rbac.go @@ -37,6 +37,14 @@ package controllers // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,resourceNames=shipwright-build-webhook,verbs=update;patch;delete // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,resourceNames=shipwright-build-webhook,verbs=update;patch;delete // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,resourceNames=shipwright-build-webhook,verbs=update;patch;delete +// +kubebuilder:rbac:groups=apps,resources=deployments,resourceNames=shipwright-triggers,verbs=update;patch;delete +// +kubebuilder:rbac:groups=apps,resources=deployments/finalizers,resourceNames=shipwright-triggers,verbs=update +// +kubebuilder:rbac:groups=core,resources=serviceaccounts,resourceNames=shipwright-triggers,verbs=update;patch;delete +// +kubebuilder:rbac:groups=core,resources=services,resourceNames=shipwright-triggers,verbs=update;patch;delete +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles,resourceNames=shipwright-triggers,verbs=update;patch;delete +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,resourceNames=shipwright-triggers,verbs=update;patch;delete +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,resourceNames=shipwright-triggers,verbs=update;patch;delete +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,resourceNames=shipwright-triggers,verbs=update;patch;delete // +kubebuilder:rbac:groups=cert-manager.io,resources=issuers,verbs=get;list;watch;create // +kubebuilder:rbac:groups=cert-manager.io,resources=issuers,resourceNames=selfsigned-issuer,verbs=update;patch;delete // +kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch;create diff --git a/controllers/suite_test.go b/controllers/suite_test.go index a98e62d3e..456cbebc9 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -144,13 +144,13 @@ func setupTektonCRDs(ctx context.Context) { Expect(err).NotTo(HaveOccurred()) } -// createShipwrightBuild creates an instance of the ShipwrightBuild object with the given target -// namespace. -func createShipwrightBuild(ctx context.Context, targetNamespace string) *operatorv1alpha1.ShipwrightBuild { +// createShipwrightBuild creates an instance of the ShipwrightBuild object with the given name and +// target namespace. +func createShipwrightBuild(ctx context.Context, name string, targetNamespace string) *operatorv1alpha1.ShipwrightBuild { By("creating a ShipwrightBuild instance") build := &operatorv1alpha1.ShipwrightBuild{ ObjectMeta: metav1.ObjectMeta{ - Name: "cluster", + Name: name, }, Spec: operatorv1alpha1.ShipwrightBuildSpec{ TargetNamespace: targetNamespace, @@ -166,6 +166,28 @@ func createShipwrightBuild(ctx context.Context, targetNamespace string) *operato return build } +// createShipwrightBuildWithTriggers creates an instance of the ShipwrightBuild object with triggers enabled. +func createShipwrightBuildWithTriggers(ctx context.Context, name string, targetNamespace string) *operatorv1alpha1.ShipwrightBuild { + By("creating a ShipwrightBuild instance with triggers enabled") + build := &operatorv1alpha1.ShipwrightBuild{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: operatorv1alpha1.ShipwrightBuildSpec{ + TargetNamespace: targetNamespace, + Triggers: &operatorv1alpha1.TriggersSpec{ + Deployment: operatorv1alpha1.TriggersDeploymentEnabled, + }, + }, + } + err := k8sClient.Create(ctx, build, &client.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("waiting for the finalizer to be set") + test.EventuallyContainFinalizer(ctx, k8sClient, build, FinalizerAnnotation) + return build +} + // deleteShipwrightBuild tears down the given ShipwrightBuild instance. func deleteShipwrightBuild(ctx context.Context, build *operatorv1alpha1.ShipwrightBuild) { By("deleting the ShipwrightBuild instance") diff --git a/controllers/triggers_test.go b/controllers/triggers_test.go new file mode 100644 index 000000000..81f5841cd --- /dev/null +++ b/controllers/triggers_test.go @@ -0,0 +1,145 @@ +package controllers + +import ( + g "github.com/onsi/ginkgo/v2" + o "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/shipwright-io/operator/api/v1alpha1" + "github.com/shipwright-io/operator/test" +) + +var _ = g.Describe("Reconcile ShipwrightBuild with Triggers", func() { + + const targetNamespace = "triggers-test-ns" + var build *v1alpha1.ShipwrightBuild + + triggersDeployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: targetNamespace, + Name: "shipwright-triggers", + }, + } + + g.When("triggers are enabled", g.Ordered, func() { + + g.BeforeAll(func(ctx g.SpecContext) { + setupTektonCRDs(ctx) + createTektonConfig(ctx) + build = createShipwrightBuildWithTriggers(ctx, "triggers-enabled", targetNamespace) + }) + + g.AfterAll(func(ctx g.SpecContext) { + deleteShipwrightBuild(ctx, build) + deleteTektonConfig(ctx) + }) + + g.It("creates triggers resources", func(ctx g.SpecContext) { + test.EventuallyExists(ctx, k8sClient, triggersDeployment.DeepCopy()) + test.EventuallyExists(ctx, k8sClient, &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Namespace: targetNamespace, Name: "shipwright-triggers"}, + }) + test.EventuallyExists(ctx, k8sClient, &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Namespace: targetNamespace, Name: "shipwright-triggers"}, + }) + test.EventuallyExists(ctx, k8sClient, &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{Name: "shipwright-triggers"}, + }) + test.EventuallyExists(ctx, k8sClient, &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: "shipwright-triggers"}, + }) + }) + }) + + g.When("triggers are enabled and the ShipwrightBuild is deleted", g.Ordered, func() { + const deleteNamespace = "triggers-delete-ns" + + g.BeforeAll(func(ctx g.SpecContext) { + setupTektonCRDs(ctx) + createTektonConfig(ctx) + build = createShipwrightBuildWithTriggers(ctx, "triggers-delete", deleteNamespace) + }) + + g.AfterAll(func(ctx g.SpecContext) { + deleteShipwrightBuild(ctx, build) + deleteTektonConfig(ctx) + }) + + g.It("removes the triggers deployment", func(ctx g.SpecContext) { + expectedDeployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Namespace: deleteNamespace, Name: "shipwright-triggers"}, + } + test.EventuallyExists(ctx, k8sClient, expectedDeployment) + + err := k8sClient.Delete(ctx, build, &client.DeleteOptions{}) + o.Expect(err).NotTo(o.HaveOccurred()) + + test.EventuallyRemoved(ctx, k8sClient, expectedDeployment) + }) + }) + + g.When("triggers are disabled after being enabled", g.Ordered, func() { + const disableNamespace = "triggers-disable-ns" + + g.BeforeAll(func(ctx g.SpecContext) { + setupTektonCRDs(ctx) + createTektonConfig(ctx) + build = createShipwrightBuildWithTriggers(ctx, "triggers-disable", disableNamespace) + }) + + g.AfterAll(func(ctx g.SpecContext) { + deleteShipwrightBuild(ctx, build) + deleteTektonConfig(ctx) + }) + + g.It("removes triggers resources when disabled via spec update", func(ctx g.SpecContext) { + expectedDeployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Namespace: disableNamespace, Name: "shipwright-triggers"}, + } + test.EventuallyExists(ctx, k8sClient, expectedDeployment) + + g.By("disabling triggers") + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(build), build) + o.Expect(err).NotTo(o.HaveOccurred()) + build.Spec.Triggers.Deployment = v1alpha1.TriggersDeploymentDisabled + err = k8sClient.Update(ctx, build) + o.Expect(err).NotTo(o.HaveOccurred()) + + test.EventuallyRemoved(ctx, k8sClient, expectedDeployment) + }) + }) + + g.When("triggers are not enabled", g.Ordered, func() { + const noTriggersNamespace = "no-triggers-test-ns" + + g.BeforeAll(func(ctx g.SpecContext) { + setupTektonCRDs(ctx) + createTektonConfig(ctx) + build = createShipwrightBuild(ctx, "no-triggers", noTriggersNamespace) + }) + + g.AfterAll(func(ctx g.SpecContext) { + deleteShipwrightBuild(ctx, build) + deleteTektonConfig(ctx) + }) + + g.It("does not create the triggers deployment", func(ctx g.SpecContext) { + test.EventuallyExists(ctx, k8sClient, &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Namespace: noTriggersNamespace, Name: "shipwright-build-controller"}, + }) + + noTriggersDeployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Namespace: noTriggersNamespace, Name: "shipwright-triggers"}, + } + o.Consistently(func() bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(noTriggersDeployment), noTriggersDeployment) + return err != nil + }, "3s", "500ms").Should(o.BeTrue(), "triggers deployment should not be created") + }) + }) +}) diff --git a/docs/shipwrightbuild.md b/docs/shipwrightbuild.md index 39e67206e..cb9dc1d10 100644 --- a/docs/shipwrightbuild.md +++ b/docs/shipwrightbuild.md @@ -10,6 +10,9 @@ When the `ShipwrightBuild` instance is created, the following components are ins - The custom resources to run Shipwright builds (`ClusterBuildStrategy`, `BuildStrategy`, `Build`, `BuildRun`). - Shipwright Build's controller, conversion webhook, and associated CA certificates. +- Optionally, [Shipwright Triggers](https://github.com/shipwright-io/triggers) — a controller that + automatically creates BuildRuns based on events (e.g., Tekton PipelineRun completions). Enabled + via `spec.triggers.deployment: Enabled`. - The following example `ClusterBuildStrategies`: - `buildah-shipwright-managed-push` - `buildah-strategy-managed-push` @@ -27,4 +30,5 @@ When the `ShipwrightBuild` instance is created, the following components are ins | Field | Description | | ----- | ----------- | | spec.targetNamespace | The target namespace where Shipwright Build will be deployed. If omitted, this will default to `shipwright-build` | +| spec.triggers.deployment | When set to `Enabled`, deploys Shipwright Triggers alongside Build. Triggers are not deployed when this field is omitted or set to `Disabled`. Defaults to `Disabled`. | | status.conditions | Conditions which report the status of Shipwright Build. Current reported conditions:

- `Ready` | diff --git a/kodata/triggers-release.yaml b/kodata/triggers-release.yaml new file mode 100644 index 000000000..9f144ff8b --- /dev/null +++ b/kodata/triggers-release.yaml @@ -0,0 +1,260 @@ +# Source: shipwright-triggers/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + namespace: shipwright-build + name: shipwright-triggers + labels: + helm.sh/chart: shipwright-triggers-0.0.1 + meta.helm.sh/release-name: shipwright-triggers + meta.helm.sh/release-namespace: shipwright-build + app.kubernetes.io/name: shipwright-triggers + app.kubernetes.io/instance: shipwright-triggers + app.kubernetes.io/managed-by: Helm +--- +# Source: shipwright-triggers/templates/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + helm.sh/chart: shipwright-triggers-0.0.1 + meta.helm.sh/release-name: shipwright-triggers + meta.helm.sh/release-namespace: shipwright-build + app.kubernetes.io/name: shipwright-triggers + app.kubernetes.io/instance: shipwright-triggers + app.kubernetes.io/managed-by: Helm + name: shipwright-triggers +rules: + - apiGroups: + - shipwright.io + resources: + - buildruns + verbs: + - create + - get + - list + - update + - watch + - apiGroups: + - shipwright.io + resources: + - builds + verbs: + - get + - list + - watch + - apiGroups: + - tekton.dev + resources: + - customruns + verbs: + - get + - list + - watch + - apiGroups: + - tekton.dev + resources: + - customruns/finalizers + - customruns/status + verbs: + - patch + - update + - apiGroups: + - tekton.dev + resources: + - pipelineruns + verbs: + - get + - list + - patch + - update + - watch +--- +# Source: shipwright-triggers/templates/role-binding.yaml +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: shipwright-triggers + labels: + helm.sh/chart: shipwright-triggers-0.0.1 + meta.helm.sh/release-name: shipwright-triggers + meta.helm.sh/release-namespace: shipwright-build + app.kubernetes.io/name: shipwright-triggers + app.kubernetes.io/instance: shipwright-triggers + app.kubernetes.io/managed-by: Helm +subjects: + - kind: ServiceAccount + namespace: shipwright-build + name: shipwright-triggers +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: shipwright-triggers +--- +# Source: shipwright-triggers/templates/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + namespace: shipwright-build + labels: + helm.sh/chart: shipwright-triggers-0.0.1 + meta.helm.sh/release-name: shipwright-triggers + meta.helm.sh/release-namespace: shipwright-build + app.kubernetes.io/name: shipwright-triggers + app.kubernetes.io/instance: shipwright-triggers + app.kubernetes.io/managed-by: Helm + name: shipwright-triggers +rules: + - apiGroups: + - shipwright.io + resources: + - buildruns + verbs: + - create + - get + - list + - update + - watch + - apiGroups: + - shipwright.io + resources: + - builds + verbs: + - get + - list + - watch + - apiGroups: + - tekton.dev + resources: + - customruns + verbs: + - get + - list + - watch + - apiGroups: + - tekton.dev + resources: + - customruns/finalizers + - customruns/status + verbs: + - patch + - update + - apiGroups: + - tekton.dev + resources: + - pipelineruns + verbs: + - get + - list + - patch + - update + - watch +--- +# Source: shipwright-triggers/templates/role-binding.yaml +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + namespace: shipwright-build + name: shipwright-triggers + labels: + helm.sh/chart: shipwright-triggers-0.0.1 + meta.helm.sh/release-name: shipwright-triggers + meta.helm.sh/release-namespace: shipwright-build + app.kubernetes.io/name: shipwright-triggers + app.kubernetes.io/instance: shipwright-triggers + app.kubernetes.io/managed-by: Helm +subjects: + - kind: ServiceAccount + namespace: shipwright-build + name: shipwright-triggers +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: shipwright-triggers +--- +# Source: shipwright-triggers/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + namespace: shipwright-build + name: shipwright-triggers + labels: + helm.sh/chart: shipwright-triggers-0.0.1 + meta.helm.sh/release-name: shipwright-triggers + meta.helm.sh/release-namespace: shipwright-build + app.kubernetes.io/name: shipwright-triggers + app.kubernetes.io/instance: shipwright-triggers + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: shipwright-triggers + app.kubernetes.io/instance: shipwright-triggers +--- +# Source: shipwright-triggers/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: shipwright-build + name: shipwright-triggers + labels: + helm.sh/chart: shipwright-triggers-0.0.1 + meta.helm.sh/release-name: shipwright-triggers + meta.helm.sh/release-namespace: shipwright-build + app.kubernetes.io/name: shipwright-triggers + app.kubernetes.io/instance: shipwright-triggers + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: shipwright-triggers + app.kubernetes.io/instance: shipwright-triggers + template: + metadata: + labels: + app.kubernetes.io/name: shipwright-triggers + app.kubernetes.io/instance: shipwright-triggers + spec: + serviceAccountName: shipwright-triggers + securityContext: + runAsNonRoot: true + containers: + - name: shipwright-triggers + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsGroup: 1000 + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + image: "ghcr.io/shipwright-io/triggers/triggers@sha256:66166ea3a992d251d72d8ae548b9d31f48ad41ac2f98f385641627a18e0c5666" + args: + - --health-probe-bind-address + - ":8081" + imagePullPolicy: IfNotPresent + ports: + - name: webhook + containerPort: 80 + protocol: TCP + - name: probe + containerPort: 8081 + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + port: probe + readinessProbe: + httpGet: + path: /readyz + port: probe + +--- diff --git a/pkg/tekton/tekton.go b/pkg/tekton/tekton.go index b3ede3698..4f7ef0e1b 100644 --- a/pkg/tekton/tekton.go +++ b/pkg/tekton/tekton.go @@ -14,7 +14,9 @@ import ( // ReconcileTekton ensures that Tekton Pipelines has been installed. // If Tekton Pipelines has not been installed, ReconcileTekton will create a TektonConfig object -// so that the Tekton Operator deploys Tekton Pipelines. +// with the "lite" profile so that the Tekton Operator deploys Tekton Pipelines. +// If a TektonConfig already exists, ReconcileTekton leaves it untouched, regardless of its +// profile, so that externally managed configurations are preserved. func ReconcileTekton(ctx context.Context, crdClient crdclientv1.ApiextensionsV1Interface, tektonOperatorClient tektonoperatorclientv1alpha1.OperatorV1alpha1Interface) (*tektonoperatorv1alpha1.TektonConfig, bool, error) { @@ -39,6 +41,7 @@ func ReconcileTekton(ctx context.Context, if tektonVersion.Major() < common.TektonOpMinSupportedMajor+1 && tektonVersion.Minor() < common.TektonOpMinSupportedMinor { return nil, true, fmt.Errorf("insufficient Tekton Operator version - must be greater than %s", common.TektonOpMinSupportedVersion) } + tektonConfigPresent, err := IsTektonConfigPresent(ctx, tektonOperatorClient) if err != nil { return nil, true, err @@ -46,10 +49,9 @@ func ReconcileTekton(ctx context.Context, if tektonConfigPresent { return nil, false, nil } - // the tekton operator 'lite' profile is all Shipwright currently needs, so configure that up; - // when Shipwright starts leveraging triggers, we will want to bump up to a 'base' or higher + tektonConfig, err := CreateTektonConfigWithProfileAndTargetNamespace(ctx, - tektonOperatorClient, "lite", "tekton-pipelines") + tektonOperatorClient, tektonoperatorv1alpha1.ProfileLite, "tekton-pipelines") if err != nil { return tektonConfig, true, err } diff --git a/pkg/tekton/tekton_test.go b/pkg/tekton/tekton_test.go index ae1abe3e7..f18a8cc3e 100644 --- a/pkg/tekton/tekton_test.go +++ b/pkg/tekton/tekton_test.go @@ -111,6 +111,29 @@ func TestReconcileTekton(t *testing.T) { expectRequeue: false, expectError: false, }, + { + // Externally managed TektonConfig with a richer profile must not be + // touched by the operator. + name: "TektonConfig externally managed with basic profile", + tektonConfigCRD: &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tektonconfigs.operator.tekton.dev", + Labels: map[string]string{ + "operator.tekton.dev/release": common.TektonOpMinSupportedVersion, + }, + }, + }, + tektonConfigObj: &tektonoperatorv1alpha1.TektonConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "config", + }, + Spec: tektonoperatorv1alpha1.TektonConfigSpec{ + Profile: tektonoperatorv1alpha1.ProfileBasic, + }, + }, + expectRequeue: false, + expectError: false, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -146,12 +169,15 @@ func TestReconcileTekton(t *testing.T) { if action.Matches("create", "tektonconfigs") { createOccurred = true } + // The operator must never update an existing TektonConfig. + g.Expect(action.Matches("update", "tektonconfigs")).To(o.BeFalse(), + "ReconcileTekton must not update an existing TektonConfig") } g.Expect(createOccurred).To(o.Equal(tc.expectTektonConfigCreateAction)) if tc.expectTektonConfigCreateAction && tc.createTektonConfigErr == nil { g.Expect(tektonConfig).NotTo(o.BeNil()) g.Expect(tektonConfig.Name).To(o.Equal("config")) - g.Expect(tektonConfig.Spec.Profile).To(o.Equal("lite")) + g.Expect(tektonConfig.Spec.Profile).To(o.Equal(tektonoperatorv1alpha1.ProfileLite)) g.Expect(tektonConfig.Spec.TargetNamespace).To(o.Equal("tekton-pipelines")) g.Expect(tektonConfig.Spec.Pruner.Disabled).To(o.Equal(false)) g.Expect(tektonConfig.Spec.Pruner.Keep).NotTo(o.BeNil()) diff --git a/pkg/triggers/triggers.go b/pkg/triggers/triggers.go new file mode 100644 index 000000000..fea6e21ed --- /dev/null +++ b/pkg/triggers/triggers.go @@ -0,0 +1,31 @@ +package triggers + +import ( + "context" + + "github.com/go-logr/logr" + "github.com/manifestival/manifestival" + "github.com/shipwright-io/operator/pkg/common" + crdclientv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" +) + +const buildsCRD = "builds.shipwright.io" + +// ReconcileTriggers reconciles the desired Triggers deployment on the cluster. +// Returns `true` if triggers were not installed and a requeue is required. +func ReconcileTriggers(ctx context.Context, crdClient crdclientv1.ApiextensionsV1Interface, log logr.Logger, manifest manifestival.Manifest) (bool, error) { + crdExists, err := common.CRDExist(ctx, crdClient, buildsCRD) + if err != nil { + return true, err + } + // If the CRD for Shipwright's builds is not installed yet, the reconciler should requeue. + if !crdExists { + return true, nil + } + // Apply the provided manifest containing the triggers resources + err = manifest.Apply() + if err != nil { + return true, err + } + return false, nil +} diff --git a/pkg/triggers/triggers_test.go b/pkg/triggers/triggers_test.go new file mode 100644 index 000000000..1c602fc8f --- /dev/null +++ b/pkg/triggers/triggers_test.go @@ -0,0 +1,87 @@ +package triggers + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + "github.com/shipwright-io/operator/pkg/common" +) + +func TestReconcileTriggers(t *testing.T) { + cases := []struct { + name string + installCRDs bool + expectRequeue bool + expectResourcesInstalled bool + }{ + { + name: "no builds CRD", + installCRDs: false, + expectRequeue: true, + }, + { + name: "builds CRD exists", + installCRDs: true, + expectRequeue: false, + expectResourcesInstalled: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + o := NewWithT(t) + ctx := context.Background() + + objects := []runtime.Object{} + if tc.installCRDs { + objects = append(objects, &crdv1.CustomResourceDefinition{ + ObjectMeta: v1.ObjectMeta{ + Name: buildsCRD, + }, + }) + } + crdClient := apiextensionsfake.NewSimpleClientset(objects...) + + k8sScheme := runtime.NewScheme() + err := scheme.AddToScheme(k8sScheme) + o.Expect(err).NotTo(HaveOccurred()) + + k8sClient := fake.NewClientBuilder().WithScheme(k8sScheme).Build() + log := zap.New() + + manifests, err := common.SetupManifestival(k8sClient, "triggers-release.yaml", false, log) + o.Expect(err).NotTo(HaveOccurred(), "setting up Manifestival") + + requeue, err := ReconcileTriggers(ctx, crdClient.ApiextensionsV1(), log, manifests) + o.Expect(err).NotTo(HaveOccurred(), "reconciling triggers") + o.Expect(requeue).To(BeEquivalentTo(tc.expectRequeue), "check reconcile requeue") + + if tc.expectResourcesInstalled { + deployment := &appsv1.Deployment{} + err := k8sClient.Get(ctx, client.ObjectKey{Namespace: "shipwright-build", Name: "shipwright-triggers"}, deployment) + o.Expect(err).NotTo(HaveOccurred(), "triggers deployment should exist") + + service := &corev1.Service{} + err = k8sClient.Get(ctx, client.ObjectKey{Namespace: "shipwright-build", Name: "shipwright-triggers"}, service) + o.Expect(err).NotTo(HaveOccurred(), "triggers service should exist") + + sa := &corev1.ServiceAccount{} + err = k8sClient.Get(ctx, client.ObjectKey{Namespace: "shipwright-build", Name: "shipwright-triggers"}, sa) + o.Expect(err).NotTo(HaveOccurred(), "triggers service account should exist") + } + }) + } +}