Skip to content

Commit

Permalink
refactor manager and controllers to split shipwright build controller…
Browse files Browse the repository at this point in the history
… from certificates controller

shipwright build controller to reconcile shipwright build manifest
certificates controller to reconcile webhook certificate
  • Loading branch information
jkhelil committed Oct 25, 2023
1 parent fc72088 commit 82d44ab
Show file tree
Hide file tree
Showing 14 changed files with 500 additions and 139 deletions.
4 changes: 2 additions & 2 deletions bundle/manifests/operator.shipwright.io_shipwrightbuilds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ spec:
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
type FooStatus struct{ // Represents the observations of a foo's
current state. // Known .status.conditions.type are: \"Available\",
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
Expand Down
14 changes: 14 additions & 0 deletions controllers/add_certificates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright The Shipwright Contributors
//
// SPDX-License-Identifier: Apache-2.0

package controllers

import (
"github.com/shipwright-io/operator/controllers/certificates"
)

func init() {
// AddToManagerFuncs is a list of functions to create controllers and add them to a manager.
AddToManagerFuncs = append(AddToManagerFuncs, certificates.Add)
}
14 changes: 14 additions & 0 deletions controllers/add_shipwrightbuild.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright The Shipwright Contributors
//
// SPDX-License-Identifier: Apache-2.0

package controllers

import (
"github.com/shipwright-io/operator/controllers/shipwrightbuild"
)

func init() {
// AddToManagerFuncs is a list of functions to create controllers and add them to a manager.
AddToManagerFuncs = append(AddToManagerFuncs, shipwrightbuild.Add)
}
165 changes: 165 additions & 0 deletions controllers/certificates/certificates_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright The Shipwright Contributors
//
// SPDX-License-Identifier: Apache-2.0

package certificates

import (
"context"

"github.com/go-logr/logr"
"github.com/manifestival/manifestival"
corev1 "k8s.io/api/core/v1"
crdclientv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"

"github.com/shipwright-io/operator/api/v1alpha1"
commonctrl "github.com/shipwright-io/operator/controllers/common"
"github.com/shipwright-io/operator/pkg/certmanager"
"github.com/shipwright-io/operator/pkg/common"
)

// CertificatesReconciler reconciles a ShipwrightBuild object
type CertificatesReconciler struct {
client.Client // controller kubernetes client
CRDClient crdclientv1.ApiextensionsV1Interface

Logger logr.Logger // decorated logger
Scheme *runtime.Scheme // runtime scheme
Manifest manifestival.Manifest // release manifests render
}

// Add creates a new certificates Controller and adds it to the Manager. The Manager will set fields on the Controller
// and Start it when the Manager is Started.
func Add(mgr manager.Manager) error {
r, err := newReconciler(mgr)
if err != nil {
return err
}
return add(mgr, r)
}

// newReconciler returns a new reconcile.Reconciler
func newReconciler(mgr manager.Manager) (*CertificatesReconciler, error) {
c := mgr.GetClient()
scheme := mgr.GetScheme()
logger := ctrl.Log.WithName("controllers").WithName("certificates")

crdClient, err := crdclientv1.NewForConfig(mgr.GetConfig())
if err != nil {
logger.Error(err, "unable to get crd client")
return nil, err
}

return &CertificatesReconciler{
CRDClient: crdClient,
Client: c,
Scheme: scheme,
Logger: logger,
}, nil
}

// add adds a new Controller to mgr with r as the reconcile.Reconciler
func add(mgr manager.Manager, r *CertificatesReconciler) error {

return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.ShipwrightBuild{}, builder.WithPredicates(predicate.Funcs{
CreateFunc: func(ce event.CreateEvent) bool {
// all new objects must be subject to reconciliation
return true
},
DeleteFunc: func(e event.DeleteEvent) bool {
// objects that haven't been confirmed deleted must be subject to reconciliation
return !e.DeleteStateUnknown
},
UpdateFunc: func(e event.UpdateEvent) bool {
// objects that have updated generation must be subject to reconciliation
return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration()
},
})).
Complete(r)
}

// Reconcile performs the resource reconciliation steps to deploy or remove Shipwright Build
// instances. When deletion-timestamp is found, the removal of the previously deploy resources is
// executed, otherwise the regular deploy workflow takes place.
func (r *CertificatesReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := r.Logger.WithValues("namespace", req.Namespace, "name", req.Name)
logger.Info("Starting resource reconciliation...")

// retrieving the ShipwrightBuild instance requested for reconcile
b := &v1alpha1.ShipwrightBuild{}
if err := r.Get(ctx, req.NamespacedName, b); err != nil {
if errors.IsNotFound(err) {
logger.Info("Resource is not found!")
return commonctrl.NoRequeue()
}
logger.Error(err, "retrieving ShipwrightBuild object from cache")
return commonctrl.RequeueOnError(err)
}
init := b.Status.Conditions == nil
if init {
b.Status.Conditions = make([]metav1.Condition, 0)
apimeta.SetStatusCondition(&b.Status.Conditions, metav1.Condition{
Type: commonctrl.ConditionReady,
Status: metav1.ConditionUnknown, // we just started trying to reconcile
Reason: "Init",
Message: "Initializing Shipwright Operator",
})
if err := r.Client.Status().Update(ctx, b); err != nil {
return commonctrl.RequeueWithError(err)
}
}

// selecting the target namespace based on the CRD information, when not informed using the
// default namespace instead
targetNamespace := b.Spec.TargetNamespace
if targetNamespace == "" {
logger.Info(
"Namespace is not informed! Target namespace is selected from default settings instead",
"defaultTargetNamespace", commonctrl.DefaultTargetNamespace,
)
targetNamespace = commonctrl.DefaultTargetNamespace
}
logger = logger.WithValues("targetNamespace", targetNamespace)
// create if it does not exist
ns := &corev1.Namespace{}
if err := r.Get(ctx, types.NamespacedName{Name: targetNamespace}, ns); err != nil {
if !errors.IsNotFound(err) {
logger.Info("retrieving target namespace %s error: %s", targetNamespace, err.Error())
return commonctrl.RequeueOnError(err)
}
ns.Name = targetNamespace

if err = r.Create(ctx, ns, &client.CreateOptions{Raw: &metav1.CreateOptions{}}); err != nil {
if !errors.IsAlreadyExists(err) {
logger.Info("creating target namespace %s error: %s", targetNamespace, err.Error())
return commonctrl.RequeueOnError(err)
}
}
logger.Info("created target namespace")
}

// ReconcileCertManager
if common.BoolFromEnvVar(commonctrl.UseManagedWebhookCerts) {
requeue, err := certmanager.ReconcileCertManager(ctx, r.CRDClient, r.Client, r.Logger, targetNamespace)
if err != nil {
return ctrl.Result{Requeue: requeue}, err
}
if requeue {
return commonctrl.Requeue()
}
}
logger.Info("All done!")
return commonctrl.NoRequeue()
}
149 changes: 149 additions & 0 deletions controllers/certificates/certificates_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package certificates

import (
"context"
"testing"

certmanager "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
o "github.com/onsi/gomega"
"github.com/shipwright-io/operator/api/v1alpha1"
commonctrl "github.com/shipwright-io/operator/controllers/common"
corev1 "k8s.io/api/core/v1"
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
crdclientv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

// bootstrapShipwrightBuildReconciler start up a new instance of ShipwrightBuildReconciler which is
// ready to interact with Manifestival, returning the Manifestival instance and the client.
func bootstrapCertificatesReconciler(
t *testing.T,
b *v1alpha1.ShipwrightBuild,
tcrds []*crdv1.CustomResourceDefinition,
) (client.Client, *crdclientv1.Clientset, *CertificatesReconciler) {
g := o.NewGomegaWithT(t)

s := runtime.NewScheme()
s.AddKnownTypes(corev1.SchemeGroupVersion, &corev1.Namespace{})
s.AddKnownTypes(certmanager.SchemeGroupVersion, &certmanager.Certificate{})
s.AddKnownTypes(v1alpha1.GroupVersion, &v1alpha1.ShipwrightBuild{})

logger := zap.New()

c := fake.NewClientBuilder().WithScheme(s).WithObjects(b).WithStatusSubresource(b).Build()

var crdClient *crdclientv1.Clientset
if len(tcrds) > 0 {
objs := []runtime.Object{}
for _, obj := range tcrds {
objs = append(objs, obj)
}
crdClient = crdclientv1.NewSimpleClientset(objs...)
} else {
crdClient = crdclientv1.NewSimpleClientset()
}

r := &CertificatesReconciler{CRDClient: crdClient.ApiextensionsV1(), Client: c, Scheme: s, Logger: logger}

if b.Spec.TargetNamespace != "" {
t.Logf("Creating test namespace '%s'", b.Spec.TargetNamespace)
t.Run("create-test-namespace", func(t *testing.T) {
err := c.Create(
context.TODO(),
&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: b.Spec.TargetNamespace}},
&client.CreateOptions{},
)
g.Expect(err).To(o.BeNil())
})
}

return c, crdClient, r
}

// testCertificatesReconcilerReconcile simulates the reconciliation process for rolling out and
// rolling back manifests in the informed target namespace name.
func testCertificatesReconcilerReconcile(t *testing.T, targetNamespace string) {
g := o.NewGomegaWithT(t)

namespacedName := types.NamespacedName{Namespace: "default", Name: "name"}
certificateName := types.NamespacedName{
Namespace: targetNamespace,
Name: "shipwright-build-webhook-cert",
}
req := reconcile.Request{NamespacedName: namespacedName}

b := &v1alpha1.ShipwrightBuild{
ObjectMeta: metav1.ObjectMeta{
Name: namespacedName.Name,
Namespace: namespacedName.Namespace,
},
Spec: v1alpha1.ShipwrightBuildSpec{
TargetNamespace: targetNamespace,
},
}
crd1 := &crdv1.CustomResourceDefinition{}
crd1.Name = "certificates.cert-manager.io"
crd2 := &crdv1.CustomResourceDefinition{}
crd2.Name = "certmanagers.operator.openshift.io"
crds := []*crdv1.CustomResourceDefinition{crd1, crd2}
_, _, r := bootstrapCertificatesReconciler(t, b, crds)

t.Logf("Deploying Certificates Controller against '%s' namespace", targetNamespace)
t.Setenv(commonctrl.UseManagedWebhookCerts, "true")

// rolling out all manifests on the desired namespace, making sure the webhook certificate is created
t.Run("rollout-manifests", func(t *testing.T) {
ctx := context.TODO()
res, err := r.Reconcile(ctx, req)
g.Expect(err).To(o.BeNil())
g.Expect(res.Requeue).To(o.BeFalse())
err = r.Get(ctx, certificateName, &certmanager.Certificate{})
g.Expect(err).To(o.BeNil())
})

// rolling back all changes, making sure the webhook certificate is also not found afterwards
t.Run("rollback-manifests", func(t *testing.T) {
ctx := context.TODO()

err := r.Get(ctx, namespacedName, b)
g.Expect(err).To(o.BeNil())

// delete the ShipwrightBuild CR triggers the rollback logic so the
// reconciliation should remove the objects previously deployed
err = r.Delete(ctx, b, &client.DeleteOptions{})
g.Expect(err).To(o.BeNil())

res, err := r.Reconcile(ctx, req)
g.Expect(err).To(o.BeNil())
g.Expect(res.Requeue).To(o.BeFalse())

err = r.Get(ctx, certificateName, &certmanager.Certificate{})
g.Expect(err).To(o.BeNil())
})
}

// TestShipwrightBuildReconciler_Reconcile runs rollout/rollback tests against different namespaces.
func TestCertificateseconciler_Reconcile(t *testing.T) {
tests := []struct {
testName string
targetNamespace string
}{{
testName: "target namespace is informed",
targetNamespace: "namespace",
}, {
testName: "target namespace is not informed",
targetNamespace: commonctrl.DefaultTargetNamespace,
}}

for _, tt := range tests {
t.Run(tt.testName, func(t *testing.T) {
testCertificatesReconcilerReconcile(t, tt.targetNamespace)
})
}
}
17 changes: 17 additions & 0 deletions controllers/common/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package common

const (
// FinalizerAnnotation annotation string appended on finalizer slice.
FinalizerAnnotation = "finalizer.operator.shipwright.io"
// defaultTargetNamespace fallback namespace when `.spec.namepace` is not informed.
DefaultTargetNamespace = "shipwright-build"

// Ready object is providing service.
ConditionReady = "Ready"

// UseManagedWebhookCerts is an env Var that controls wether we install the webhook certs
UseManagedWebhookCerts = "USE_MANAGED_WEBHOOK_CERTS"

CertManagerInjectAnnotationKey = "cert-manager.io/inject-ca-from"
CertManagerInjectAnnotationValueTemplate = "%s/shipwright-build-webhook-cert"
)
2 changes: 1 addition & 1 deletion controllers/result.go → controllers/common/result.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package controllers
package common

import (
ctrl "sigs.k8s.io/controller-runtime"
Expand Down
Loading

0 comments on commit 82d44ab

Please sign in to comment.