Skip to content

Commit

Permalink
feat: Install Cluster Build Strategies with Operator
Browse files Browse the repository at this point in the history
Install the build strategy samples from shipwright-io/build, at the
v0.12.0 tag. Only cluster build strategies were added - namespace scoped
build strategies were dropped. Red Hat-specific strategies were also
dropped, in part to avoid potential trademark issues. The build strategies
use v1alpha1 APIs because this is the stored version in v0.12.0. Testing
revealed that if we reconciled v1beta1 APIs, we would need to wait for
(and mock out!) the conversion webhook deployment. Lastly, the operator
was granted RBAC permission to administer all ClusterBuildStrategies.

Reconciling build strategies required adding logic to wait for the
required CRDs to be installed on the cluster first. If a requeue is
required, the operator will report the Ready status condition as "Unknown."
The manifestival library code also had to enhanced to optionally recurse
a directory for manifests to deploy.

Finally, development of this feature revealed refactoring opportunities
with respect to our use of k8s and controller-runtime client libraries, and
the way we are organizing/managing Manifestival-driven reconcilers. Some
refactoring of test code was included to simplify future testing efforts.
  • Loading branch information
adambkaplan committed Nov 6, 2023
1 parent 0d64e38 commit 5260204
Show file tree
Hide file tree
Showing 22 changed files with 1,557 additions and 148 deletions.
12 changes: 12 additions & 0 deletions bundle/manifests/shipwright-operator.clusterserviceversion.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,18 @@ spec:
- delete
- patch
- update
- apiGroups:
- shipwright.io
resources:
- clusterbuildstrategies
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- authentication.k8s.io
resources:
Expand Down
12 changes: 12 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,15 @@ rules:
- delete
- patch
- update
- apiGroups:
- shipwright.io
resources:
- clusterbuildstrategies
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
48 changes: 48 additions & 0 deletions controllers/buildstrategies_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package controllers

import (
"fmt"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

buildv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1"
"github.com/shipwright-io/operator/api/v1alpha1"
"github.com/shipwright-io/operator/test"
)

var _ = Describe("Install embedded build strategies", func() {

var build *v1alpha1.ShipwrightBuild

BeforeEach(func(ctx SpecContext) {
setupTektonCRDs(ctx)
build = createShipwrightBuild(ctx, "shipwright")
test.CRDEventuallyExists(ctx, k8sClient, "clusterbuildstrategies.shipwright.io")
})

When("the install build strategies feature is enabled", func() {

It("applies the embedded build strategy manifests to the cluster", func(ctx SpecContext) {
expectedBuildStrategies, err := test.ParseBuildStrategyNames()
Expect(err).NotTo(HaveOccurred())
for _, strategy := range expectedBuildStrategies {
strategyObj := &buildv1alpha1.ClusterBuildStrategy{
ObjectMeta: metav1.ObjectMeta{
Name: strategy,
},
}
By(fmt.Sprintf("checking for build strategy %q", strategy))
test.EventuallyExists(ctx, k8sClient, strategyObj)
}

})
})

AfterEach(func(ctx SpecContext) {
deleteShipwrightBuild(ctx, build)
})

})
119 changes: 3 additions & 116 deletions controllers/default_test.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,21 @@
package controllers

import (
"context"

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"
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/shipwright-io/operator/api/v1alpha1"
"github.com/shipwright-io/operator/pkg/common"
"github.com/shipwright-io/operator/test"
)

// createNamespace creates the namespace informed.
func createNamespace(ctx context.Context, name string) {
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}}
err := k8sClient.Get(ctx, types.NamespacedName{Name: ns.Name}, ns)
if errors.IsNotFound(err) {
err = k8sClient.Create(ctx, ns, &client.CreateOptions{})
}
o.Expect(err).NotTo(o.HaveOccurred())
}

var _ = g.Describe("Reconcile default ShipwrightBuild installation", func() {

// namespace where ShipwrightBuild instance will be located
const namespace = "namespace"
// targetNamespace namespace where shipwright Controller and dependencies will be located
const targetNamespace = "target-namespace"
// build Build instance employed during testing
Expand Down Expand Up @@ -62,109 +44,14 @@ var _ = g.Describe("Reconcile default ShipwrightBuild installation", func() {
},
}

truePtr := true
g.BeforeEach(func(ctx g.SpecContext) {
// setting up the namespaces, where Shipwright Controller will be deployed
createNamespace(ctx, namespace)

g.By("does tekton taskrun crd exist")
err := k8sClient.Get(ctx, types.NamespacedName{Name: "taskruns.tekton.dev"}, &crdv1.CustomResourceDefinition{})
if errors.IsNotFound(err) {
g.By("creating tekton taskrun crd")
taskRunCRD := &crdv1.CustomResourceDefinition{}
taskRunCRD.Name = "taskruns.tekton.dev"
taskRunCRD.Spec.Group = "tekton.dev"
taskRunCRD.Spec.Scope = crdv1.NamespaceScoped
taskRunCRD.Spec.Versions = []crdv1.CustomResourceDefinitionVersion{
{
Name: "v1beta1",
Storage: true,
Schema: &crdv1.CustomResourceValidation{
OpenAPIV3Schema: &crdv1.JSONSchemaProps{
Type: "object",
XPreserveUnknownFields: &truePtr,
},
},
},
}
taskRunCRD.Spec.Names.Plural = "taskruns"
taskRunCRD.Spec.Names.Singular = "taskrun"
taskRunCRD.Spec.Names.Kind = "TaskRun"
taskRunCRD.Spec.Names.ListKind = "TaskRunList"
taskRunCRD.Status.StoredVersions = []string{"v1beta1"}
err = k8sClient.Create(ctx, taskRunCRD, &client.CreateOptions{})
o.Expect(err).NotTo(o.HaveOccurred())

}
o.Expect(err).NotTo(o.HaveOccurred())

g.By("does tektonconfig crd exist")
err = k8sClient.Get(ctx, types.NamespacedName{Name: "tektonconfigs.operator.tekton.dev"}, &crdv1.CustomResourceDefinition{})
if errors.IsNotFound(err) {
tektonOpCRD := &crdv1.CustomResourceDefinition{}
tektonOpCRD.Name = "tektonconfigs.operator.tekton.dev"
tektonOpCRD.Labels = map[string]string{"operator.tekton.dev/release": common.TektonOpMinSupportedVersion}
tektonOpCRD.Spec.Group = "operator.tekton.dev"
tektonOpCRD.Spec.Scope = crdv1.ClusterScoped
tektonOpCRD.Spec.Versions = []crdv1.CustomResourceDefinitionVersion{
{
Name: "v1alpha1",
Storage: true,
Schema: &crdv1.CustomResourceValidation{
OpenAPIV3Schema: &crdv1.JSONSchemaProps{
Type: "object",
XPreserveUnknownFields: &truePtr,
},
},
},
}
tektonOpCRD.Spec.Names.Plural = "tektonconfigs"
tektonOpCRD.Spec.Names.Singular = "tektonconfig"
tektonOpCRD.Spec.Names.Kind = "TektonConfig"
tektonOpCRD.Spec.Names.ListKind = "TektonConfigList"
tektonOpCRD.Status.StoredVersions = []string{"v1alpha1"}
err = k8sClient.Create(ctx, tektonOpCRD, &client.CreateOptions{})
o.Expect(err).NotTo(o.HaveOccurred())
}
o.Expect(err).NotTo(o.HaveOccurred())

g.By("creating a ShipwrightBuild instance")
build = &v1alpha1.ShipwrightBuild{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: "cluster",
},
Spec: v1alpha1.ShipwrightBuildSpec{
TargetNamespace: targetNamespace,
},
}
err = k8sClient.Create(ctx, build, &client.CreateOptions{})
o.Expect(err).NotTo(o.HaveOccurred())

// when the finalizer is in place, the deployment of manifest elements is done, and therefore
// functional testing can proceed
g.By("waiting for the finalizer to be set")
test.EventuallyContainFinalizer(ctx, k8sClient, build, FinalizerAnnotation)
setupTektonCRDs(ctx)
build = createShipwrightBuild(ctx, targetNamespace)
})

g.AfterEach(func(ctx g.SpecContext) {
g.By("deleting the ShipwrightBuild instance")
namespacedName := types.NamespacedName{Namespace: namespace, Name: build.Name}
err := k8sClient.Get(ctx, namespacedName, build)
if errors.IsNotFound(err) {
return
}
o.Expect(err).NotTo(o.HaveOccurred())

err = k8sClient.Delete(ctx, build, &client.DeleteOptions{})
// the delete e2e's can delete this object before this AfterEach runs
if errors.IsNotFound(err) {
return
}
o.Expect(err).NotTo(o.HaveOccurred())

g.By("waiting for ShipwrightBuild instance to be completely removed")
test.EventuallyRemoved(ctx, k8sClient, build)
deleteShipwrightBuild(ctx, build)

g.By("checking that the shipwright-build-controller deployment has been removed")
deployment := baseDeployment.DeepCopy()
Expand Down
50 changes: 44 additions & 6 deletions controllers/shipwrightbuild_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package controllers
import (
"context"
"fmt"
"path/filepath"

"github.com/go-logr/logr"
"github.com/manifestival/manifestival"
Expand All @@ -25,6 +26,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"

"github.com/shipwright-io/operator/api/v1alpha1"
"github.com/shipwright-io/operator/pkg/buildstrategy"
"github.com/shipwright-io/operator/pkg/certmanager"
"github.com/shipwright-io/operator/pkg/common"
"github.com/shipwright-io/operator/pkg/tekton"
Expand Down Expand Up @@ -53,10 +55,11 @@ type ShipwrightBuildReconciler struct {
CRDClient crdclientv1.ApiextensionsV1Interface
TektonOperatorClient tektonoperatorv1alpha1client.OperatorV1alpha1Interface

Logger logr.Logger // decorated logger
Scheme *runtime.Scheme // runtime scheme
Manifest manifestival.Manifest // release manifests render
TektonManifest manifestival.Manifest // Tekton release manifest render
Logger logr.Logger // decorated logger
Scheme *runtime.Scheme // runtime scheme
Manifest manifestival.Manifest // release manifests render
TektonManifest manifestival.Manifest // Tekton release manifest render
BuildStrategyManifest manifestival.Manifest // Build strategies manifest to render
}

// setFinalizer append finalizer on the resource, and uses local client to update it immediately.
Expand Down Expand Up @@ -195,6 +198,11 @@ func (r *ShipwrightBuildReconciler) Reconcile(ctx context.Context, req ctrl.Requ
logger.Info("Finalizers removed, deletion of manifests completed!")
return NoRequeue()
}
logger.Info("Deleting cluster build strategies")
if err := r.BuildStrategyManifest.Delete(); err != nil {
logger.Error(err, "deleting cluster build strategies")
return RequeueWithError(err)
}

logger.Info("Deleting manifests...")
if err := manifest.Delete(); err != nil {
Expand Down Expand Up @@ -229,6 +237,29 @@ func (r *ShipwrightBuildReconciler) Reconcile(ctx context.Context, req ctrl.Requ
logger.Error(err, "setting the finalizer")
return RequeueWithError(err)
}

requeue, err = buildstrategy.ReconcileBuildStrategies(ctx,
r.CRDClient,
logger,
r.BuildStrategyManifest)
if err != nil {
logger.Error(err, "reconcile cluster build strategies")
return RequeueWithError(err)
}
if requeue {
logger.Info("requeue waiting for cluster build strategy preconditions")
apimeta.SetStatusCondition(&b.Status.Conditions, metav1.Condition{
Type: ConditionReady,
Status: metav1.ConditionUnknown,
Reason: "ClusterBuildStrategiesWaiting",
Message: "Waiting for cluster build strategies to be deployed",
})
if updateErr := r.Client.Status().Update(ctx, b); updateErr != nil {
return RequeueWithError(err)
}
return Requeue()
}

apimeta.SetStatusCondition(&b.Status.Conditions, metav1.Condition{
Type: ConditionReady,
Status: metav1.ConditionTrue,
Expand All @@ -246,8 +277,15 @@ func (r *ShipwrightBuildReconciler) Reconcile(ctx context.Context, req ctrl.Requ
// setupManifestival instantiate manifestival with local controller attributes, as well as tekton prereqs.
func (r *ShipwrightBuildReconciler) setupManifestival() error {
var err error
r.Manifest, err = common.SetupManifestival(r.Client, "release.yaml", r.Logger)
return err
r.Manifest, err = common.SetupManifestival(r.Client, "release.yaml", false, r.Logger)
if err != nil {
return err
}
r.BuildStrategyManifest, err = common.SetupManifestival(r.Client, filepath.Join("samples", "buildstrategy"), true, r.Logger)
if err != nil {
return err
}
return nil
}

// SetupWithManager sets up the controller with the Manager, by instantiating Manifestival and
Expand Down
22 changes: 18 additions & 4 deletions controllers/shipwrightbuild_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,18 @@ func testShipwrightBuildReconcilerReconcile(t *testing.T, targetNamespace string
ctx := context.TODO()
res, err := r.Reconcile(ctx, req)
g.Expect(err).To(o.BeNil())
g.Expect(res.Requeue).To(o.BeFalse())
// TODO: Code technically uses two different clientsets that don't talk to each other.
// This makes testing brittle and unable to capture the behavior on a real cluster.
// Requeue can return "true" because the tests think the CRD for ClusterBuildStrategies
// do not exist yet.
g.Expect(res.Requeue).To(o.BeTrue(), "checking requeue for Reconcile")
err = c.Get(ctx, deploymentName, &appsv1.Deployment{})
g.Expect(err).To(o.BeNil())
err = c.Get(ctx, namespacedName, b)
g.Expect(err).To(o.BeNil())
g.Expect(b.Status.IsReady()).To(o.BeTrue())
// Likewise, the ShipwrightBuild object will not report itself ready because it is waiting
// for the ClusterBuildStrategy CRD to be created first.
g.Expect(b.Status.IsReady()).To(o.BeFalse(), "checking ShipwrightBuild readiness")
})

t.Run("rollout-manifests-with-images-env-vars", func(t *testing.T) {
Expand All @@ -174,14 +180,20 @@ func testShipwrightBuildReconcilerReconcile(t *testing.T, targetNamespace string
deployment := &appsv1.Deployment{}
res, err := r.Reconcile(ctx, req)
g.Expect(err).To(o.BeNil())
g.Expect(res.Requeue).To(o.BeFalse())
// TODO: Code technically uses two different clientsets that don't talk to each other.
// This makes testing brittle and unable to capture the behavior on a real cluster.
// Requeue can return "true" because the tests think the CRD for ClusterBuildStrategies
// do not exist yet.
g.Expect(res.Requeue).To(o.BeTrue())
err = c.Get(ctx, deploymentName, deployment)
g.Expect(err).To(o.BeNil())
containers := deployment.Spec.Template.Spec.Containers
g.Expect(containers[0].Image).To(o.Equal("ghcr.io/shipwright-io/build/shipwright-build-controller:nightly-2023-05-05-1683263383"))
err = c.Get(ctx, namespacedName, b)
g.Expect(err).To(o.BeNil())
g.Expect(b.Status.IsReady()).To(o.BeTrue())
// Likewise, the ShipwrightBuild object will not report itself ready because it is waiting
// for the ClusterBuildStrategy CRD to be created first.
g.Expect(b.Status.IsReady()).To(o.BeFalse())
})

// rolling back all changes, making sure the main deployment is also not found afterwards
Expand All @@ -193,6 +205,8 @@ func testShipwrightBuildReconcilerReconcile(t *testing.T, targetNamespace string

// setting a deletion timestemp on the build object, it triggers the rollback logic so the
// reconciliation should remove the objects previously deployed

// TODO: Refactor to use owner references so the rollback is handled by Kubernetes itself.
b.SetDeletionTimestamp(&metav1.Time{Time: time.Now()})
err = r.Update(ctx, b, &client.UpdateOptions{})
g.Expect(err).To(o.BeNil())
Expand Down
Loading

0 comments on commit 5260204

Please sign in to comment.