Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Shipwright Target Namespace
Browse files Browse the repository at this point in the history
CRD changes, adding `.spec.namespace` attribute.

Deploying Shipwright Build Controller, and dependencies, on a target
namespace informed on the CRD spec. Additionally, implementing
Kubernetes Finalizer workflow to remove objects with actions centered
on the Reconciler directly.

Co-authored-by: Adam Kaplan <adam.kaplan@redhat.com>
otaviof and adambkaplan committed Jul 29, 2021
1 parent 45bd730 commit 15400d2
Showing 18 changed files with 508 additions and 124 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -4,4 +4,26 @@ An operator to install and configure [Shipwright](https://shipwright.io) on Kube

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for more information on how to build, test, and submit contributions to the operator.
See [CONTRIBUTING.md](CONTRIBUTING.md) for more information on how to build, test, and submit
contributions to the operator.

## Usage

To deploy and manage instances of [Shipwright Build-Controller][build-controller], make sure this
operator is up-and-running, and then create the following:

```yml
---
apiVersion: operator.shipwright.io/v1alpha1
kind: ShipwrightBuild
metadata:
name: shipwright-operator
spec:
namespace: default
```
It will deploy the Build-Controller in `default` namespace. When `.spec.namespace` is not set,
it will use the `shipwright-build` namespace instead, this namespace needs to be created before the
actual deployment takes place.

[build-controller]: https://github.com/shipwright-io/build
4 changes: 4 additions & 0 deletions api/v1alpha1/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// This package contains the CRD code, describing how the operator API will work in Kubernetes. When
// the contents of this package are modified, you must run `make` command to make sure files with
// `zz_generated.` prefix are updated, the additional code is generated as expected.
package v1alpha1
25 changes: 9 additions & 16 deletions api/v1alpha1/shipwrightbuild_types.go
Original file line number Diff line number Diff line change
@@ -8,29 +8,20 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.

// ShipwrightBuildSpec defines the desired state of ShipwrightBuild
// ShipwrightBuildSpec defines the configuration of a Shipwright Build deployment.
type ShipwrightBuildSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file

// Foo is an example field of ShipwrightBuild. Edit ShipwrightBuild_types.go to remove/update
Foo string `json:"foo,omitempty"`
// TargetNamespace is the target namespace where Shipwright's build controller will be deployed.
TargetNamespace string `json:"targetNamespace,omitempty"`
}

// ShipwrightBuildStatus defines the observed state of ShipwrightBuild
type ShipwrightBuildStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
// ShipwrightBuildStatus defines the observed state of Shipwright-Build
type ShipwrightBuildStatus struct{}

// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:subresource:status

// ShipwrightBuild is the Schema for the shipwrightbuilds API
// ShipwrightBuild represents the deployment of Shipwright's build controller on a Kubernetes cluster.
type ShipwrightBuild struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
@@ -45,9 +36,11 @@ type ShipwrightBuild struct {
type ShipwrightBuildList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ShipwrightBuild `json:"items"`

Items []ShipwrightBuild `json:"items"`
}

// init registers the current Schema on the Scheme Builder during initialization.
func init() {
SchemeBuilder.Register(&ShipwrightBuild{}, &ShipwrightBuildList{})
}
2 changes: 1 addition & 1 deletion bundle/manifests/operator.clusterserviceversion.yaml
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ spec:
apiservicedefinitions: {}
customresourcedefinitions:
owned:
- description: ShipwrightBuild is the Schema for the shipwrightbuilds API
- description: ShipwrightBuild represents the deployment of Shipwright's build controller on a Kubernetes cluster.
displayName: Shipwright Build
kind: ShipwrightBuild
name: shipwrightbuilds.operator.shipwright.io
10 changes: 5 additions & 5 deletions bundle/manifests/operator.shipwright.io_shipwrightbuilds.yaml
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ spec:
- name: v1alpha1
schema:
openAPIV3Schema:
description: ShipwrightBuild is the Schema for the shipwrightbuilds API
description: ShipwrightBuild represents the deployment of Shipwright's build controller on a Kubernetes cluster.
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
@@ -28,14 +28,14 @@ spec:
metadata:
type: object
spec:
description: ShipwrightBuildSpec defines the desired state of ShipwrightBuild
description: ShipwrightBuildSpec defines the configuration of a Shipwright Build deployment.
properties:
foo:
description: Foo is an example field of ShipwrightBuild. Edit ShipwrightBuild_types.go to remove/update
targetNamespace:
description: TargetNamespace is the target namespace where Shipwright's build controller will be deployed.
type: string
type: object
status:
description: ShipwrightBuildStatus defines the observed state of ShipwrightBuild
description: ShipwrightBuildStatus defines the observed state of Shipwright-Build
type: object
type: object
served: true
2 changes: 1 addition & 1 deletion cmd/operator/main.go
Original file line number Diff line number Diff line change
@@ -81,8 +81,8 @@ func main() {

if err = (&controllers.ShipwrightBuildReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("ShipwrightBuild"),
Scheme: mgr.GetScheme(),
Logger: ctrl.Log.WithName("controllers").WithName("ShipwrightBuild"),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ShipwrightBuild")
os.Exit(1)
14 changes: 8 additions & 6 deletions config/crd/bases/operator.shipwright.io_shipwrightbuilds.yaml
Original file line number Diff line number Diff line change
@@ -19,7 +19,8 @@ spec:
- name: v1alpha1
schema:
openAPIV3Schema:
description: ShipwrightBuild is the Schema for the shipwrightbuilds API
description: ShipwrightBuild represents the deployment of Shipwright's build
controller on a Kubernetes cluster.
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
@@ -34,15 +35,16 @@ spec:
metadata:
type: object
spec:
description: ShipwrightBuildSpec defines the desired state of ShipwrightBuild
description: ShipwrightBuildSpec defines the configuration of a Shipwright
Build deployment.
properties:
foo:
description: Foo is an example field of ShipwrightBuild. Edit ShipwrightBuild_types.go
to remove/update
targetNamespace:
description: TargetNamespace is the target namespace where Shipwright's
build controller will be deployed.
type: string
type: object
status:
description: ShipwrightBuildStatus defines the observed state of ShipwrightBuild
description: ShipwrightBuildStatus defines the observed state of Shipwright-Build
type: object
type: object
served: true
2 changes: 1 addition & 1 deletion config/manifests/bases/operator.clusterserviceversion.yaml
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ spec:
apiservicedefinitions: {}
customresourcedefinitions:
owned:
- description: ShipwrightBuild is the Schema for the shipwrightbuilds API
- description: ShipwrightBuild represents the deployment of Shipwright's build controller on a Kubernetes cluster.
displayName: Shipwright Build
kind: ShipwrightBuild
name: shipwrightbuilds.operator.shipwright.io
51 changes: 34 additions & 17 deletions controllers/default_test.go
Original file line number Diff line number Diff line change
@@ -18,29 +18,39 @@ import (

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

// namespace where ShipwrightBuild instance will be located
const namespace = "default"
// namespace where shipwright Controller and dependencies will be located
const targetNamespace = "namespace"

var build *v1alpha1.ShipwrightBuild

g.BeforeEach(func() {
namespace := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "shipwright-build",
},
}
err := k8sClient.Get(ctx, types.NamespacedName{Name: namespace.Name}, namespace)
// setting up the target namespace, where Shipwright Controller will be deployed
ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: targetNamespace}}
err := k8sClient.Get(ctx, types.NamespacedName{Name: ns.Name}, ns)
if errors.IsNotFound(err) {
err = k8sClient.Create(ctx, namespace, &client.CreateOptions{})
err = k8sClient.Create(ctx, ns, &client.CreateOptions{})
}
o.Expect(err).NotTo(o.HaveOccurred())

g.By("creating a ShipwrightBuild instance")
build = &v1alpha1.ShipwrightBuild{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster",
Namespace: namespace,
Name: "cluster",
},
Spec: v1alpha1.ShipwrightBuildSpec{
TargetNamespace: targetNamespace,
},
Spec: v1alpha1.ShipwrightBuildSpec{},
}
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)
})

g.AfterEach(func() {
@@ -50,13 +60,18 @@ var _ = g.Describe("Reconcile default ShipwrightBuild installation", func() {
return
}
o.Expect(err).NotTo(o.HaveOccurred())

err = k8sClient.Delete(ctx, build, &client.DeleteOptions{})
o.Expect(err).NotTo(o.HaveOccurred())
g.By("checking that the shipwright-build deployment has been removed")

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

g.By("checking that the shipwright-build-controller deployment has been removed")
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: targetNamespace,
Name: "shipwright-build-controller",
Namespace: "shipwright-build",
},
}
test.EventuallyRemoved(ctx, k8sClient, deployment)
@@ -81,7 +96,7 @@ var _ = g.Describe("Reconcile default ShipwrightBuild installation", func() {

expectedServiceAccount := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Namespace: "shipwright-build",
Namespace: targetNamespace,
Name: "shipwright-build-controller",
},
}
@@ -91,7 +106,7 @@ var _ = g.Describe("Reconcile default ShipwrightBuild installation", func() {
g.It("creates a deployment for the Shipwright build controller", func() {
expectedDeployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "shipwright-build",
Namespace: targetNamespace,
Name: "shipwright-build-controller",
},
}
@@ -111,17 +126,19 @@ var _ = g.Describe("Reconcile default ShipwrightBuild installation", func() {
g.It("deletes the RBAC for the Shipwright build controller", func() {
expectedClusterRole := &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: "shipwright-build-controller",
Namespace: targetNamespace,
Name: "shipwright-build-controller",
},
}
expectedClusterRoleBinding := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "shipwright-build-controller",
Namespace: targetNamespace,
Name: "shipwright-build-controller",
},
}
expectedServiceAccount := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Namespace: "shipwright-build",
Namespace: targetNamespace,
Name: "shipwright-build-controller",
},
}
@@ -144,7 +161,7 @@ var _ = g.Describe("Reconcile default ShipwrightBuild installation", func() {
g.It("deletes the deployment for the Shipwright build controller", func() {
expectedDeployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: "shipwright-build",
Namespace: targetNamespace,
Name: "shipwright-build-controller",
},
}
25 changes: 25 additions & 0 deletions controllers/result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package controllers

import (
ctrl "sigs.k8s.io/controller-runtime"
)

// Requeue triggers a object requeue.
func Requeue() (ctrl.Result, error) {
return ctrl.Result{Requeue: true}, nil
}

// RequeueOnError triggers requeue when error is not nil.
func RequeueOnError(err error) (ctrl.Result, error) {
return ctrl.Result{}, err
}

// RequeueWithError triggers a object requeue because the informed error happend.
func RequeueWithError(err error) (ctrl.Result, error) {
return ctrl.Result{Requeue: true}, err
}

// NoRequeue all done, the object does not need reconciliation anymore.
func NoRequeue() (ctrl.Result, error) {
return ctrl.Result{Requeue: false}, nil
}
205 changes: 140 additions & 65 deletions controllers/shipwrightbuild_controller.go
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@ package controllers
import (
"context"
"fmt"
"os"
"path/filepath"

mfc "github.com/manifestival/controller-runtime-client"
@@ -17,93 +16,169 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
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/predicate"

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

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"
)

// ShipwrightBuildReconciler reconciles a ShipwrightBuild object
type ShipwrightBuildReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Manifest manifestival.Manifest
client.Client // controller kubernetes client

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

// Declare RBAC needed to reconcile the release manifest YAML
// To minimize the risk of privilege escalation or destructive behavior, the controller is only
// allowed to modify named resources that deploy Shipwright Build.
// This is especially true for the cluster roles and custom resource definitions included in the
// release manifest.

// +kubebuilder:rbac:groups=operator.shipwright.io,resources=shipwrightbuilds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=operator.shipwright.io,resources=shipwrightbuilds/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=operator.shipwright.io,resources=shipwrightbuilds/finalizers,verbs=update
// +kubebuilder:rbac:groups=shipwright.io,resources=*,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups=core,resources=pods;services;services/finalizers;endpoints;persistentvolumeclaims;events;configmaps;secrets;serviceaccounts,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups=apps,resources=deployments;daemonsets;replicasets;statefulsets,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups=apps,resourceNames=shipwright-build,resources=deployments/finalizers,verbs=update
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles,verbs=get;list;watch;create
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles,resourceNames=shipwright-build-controller,verbs=update;patch;delete
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,verbs=get;list;watch;create
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,resourceNames=shipwright-build-controller,verbs=update;patch;delete
// +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch;create
// +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,resourceNames=builds.shipwright.io;buildruns.shipwright.io;buildstrategies.shipwright.io;clusterbuildstrategies.shipwright.io,verbs=update;patch;delete
// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;create
// +kubebuilder:rbac:groups=tekton.dev,resources=tasks;taskruns,verbs=create;delete;get;list;patch;update;watch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the ShipwrightBuild object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
// setFinalizer append finalizer on the resource, and uses local client to update it immediately.
func (r *ShipwrightBuildReconciler) setFinalizer(ctx context.Context, b *v1alpha1.ShipwrightBuild) error {
if contains(b.GetFinalizers(), FinalizerAnnotation) {
return nil
}
b.SetFinalizers(append(b.GetFinalizers(), FinalizerAnnotation))
return r.Update(ctx, b, &client.UpdateOptions{})
}

// unsetFinalizer remove all instances of local finalizer string, updating the resource immediately.
func (r *ShipwrightBuildReconciler) unsetFinalizer(ctx context.Context, b *v1alpha1.ShipwrightBuild) error {
finalizers := []string{}
for _, f := range b.GetFinalizers() {
if f == FinalizerAnnotation {
continue
}
finalizers = append(finalizers, f)
}

b.SetFinalizers(finalizers)
return r.Update(ctx, b, &client.UpdateOptions{})
}

// 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 *ShipwrightBuildReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("shipwrightbuild", req.NamespacedName)

build := &v1alpha1.ShipwrightBuild{}
// Remove Namespaces from the manifest - cluster admins must provision the shipwright-build namespace
manifest := r.Manifest.Filter(manifestival.Not(manifestival.ByKind("Namespace")))
err := r.Client.Get(ctx, req.NamespacedName, build)
if errors.IsNotFound(err) {
log.Info("object not found, deleting Shipwright Build from the cluster")
err = manifest.Delete()
if err != nil {
return ctrl.Result{}, err
logger := r.Logger.WithValues("namespace", req.Namespace, "name", req.Name)
logger.Info("Starting resource reconciliation...")

// retrieving the ShipwrightBuild instance requested for reconciliation
b := &v1alpha1.ShipwrightBuild{}
if err := r.Get(ctx, req.NamespacedName, b); err != nil {
if errors.IsNotFound(err) {
logger.Info("Resource is not found!")
return NoRequeue()
}
return ctrl.Result{}, nil
logger.Error(err, "Retrieving ShipwrightBuild object from cache")
return RequeueOnError(err)
}
if err != nil {
return ctrl.Result{}, 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, using default namespace instead")
targetNamespace = defaultTargetNamespace
}
log.Info("reconciling ShipwrightBuild with manifest")
err = manifest.Apply()
logger = logger.WithValues("targetNamespace", targetNamespace)

// filtering out namespace resource, so it does not create new namespaces accidentally, and
// transforming object to target the namespace informed on the CRD (.spec.namespace)
manifest, err := r.Manifest.
Filter(manifestival.Not(manifestival.ByKind("Namespace"))).
Transform(manifestival.InjectNamespace(targetNamespace))
if err != nil {
return ctrl.Result{}, err
logger.Error(err, "Transforming manifests, injecting namespace")
return RequeueWithError(err)
}

return ctrl.Result{}, nil
// when deletion-timestamp is set, the reconciliation process is in fact deleting the resources
// previously deployed. To mark the deletion process as done, it needs to clean up the
// finalizers, and thus the ShipwrightBuild is removed from cache
if !b.GetDeletionTimestamp().IsZero() {
logger.Info("DeletionTimestamp is set...")
if !contains(b.GetFinalizers(), FinalizerAnnotation) {
logger.Info("Finalizers removed, deletion of manifests completed!")
return NoRequeue()
}

logger.Info("Deleting manifests...")
if err := manifest.Delete(); err != nil {
logger.Error(err, "Deleting manifest's resources")
return RequeueWithError(err)
}
logger.Info("Removing finalizers...")
if err := r.unsetFinalizer(ctx, b); err != nil {
logger.Error(err, "Removing the finalizer")
return RequeueWithError(err)
}
logger.Info("All removed!")
return NoRequeue()
}

// rolling out the resources described on the manifests, it should create a new Shipwright Build
// instance with required dependencies
logger.Info("Applying manifest's resources...")
if err := manifest.Apply(); err != nil {
logger.Error(err, "Rolling out manifest's resources")
return RequeueWithError(err)
}
if err := r.setFinalizer(ctx, b); err != nil {
logger.Info(fmt.Sprintf("%#v", b))
logger.Error(err, "Setting the finalizer")
return RequeueWithError(err)
}
logger.Info("All done!")
return NoRequeue()
}

// SetupWithManager sets up the controller with the Manager.
func (r *ShipwrightBuildReconciler) SetupWithManager(mgr ctrl.Manager) error {
mfclient := mfc.NewClient(mgr.GetClient())
mflogger := mgr.GetLogger().WithName("manifestival")
dataPath, exists := os.LookupEnv("KO_DATA_PATH")
if !exists {
return fmt.Errorf("KO_DATA_PATH is not set - cannot set up reconciler")
// setupManifestival instantiate manifestival with local controller attributes.
func (r *ShipwrightBuildReconciler) setupManifestival(managerLogger logr.Logger) error {
client := mfc.NewClient(r.Client)
logger := managerLogger.WithName("manifestival")

dataPath, err := koDataPath()
if err != nil {
return err
}
buildManifest := filepath.Join(dataPath, "release.yaml")

mf, err := manifestival.NewManifest(buildManifest, manifestival.UseClient(mfclient), manifestival.UseLogger(mflogger))
if err != nil {
r.Manifest, err = manifestival.NewManifest(
buildManifest,
manifestival.UseClient(client),
manifestival.UseLogger(logger),
)
return err
}

// SetupWithManager sets up the controller with the Manager, by instantiating Manifestival and
// setting up watch and predicate rules for ShipwrightBuild objects.
func (r *ShipwrightBuildReconciler) SetupWithManager(mgr ctrl.Manager) error {
if err := r.setupManifestival(mgr.GetLogger()); err != nil {
return err
}
r.Manifest = mf
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.ShipwrightBuild{}).
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)
}
173 changes: 173 additions & 0 deletions controllers/shipwrightbuild_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package controllers

import (
"context"
"os"
"testing"
"time"

o "github.com/onsi/gomega"
"github.com/shipwright-io/operator/api/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
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"
)

func init() {
// exporting the environment variable which points Manifestival to the release.yaml file,
// containing all resources managed by it
_ = os.Setenv("KO_DATA_PATH", "../cmd/operator/kodata")
}

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

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

logger := zap.New()

c := fake.NewFakeClientWithScheme(s, b)
r := &ShipwrightBuildReconciler{Client: c, Scheme: s, Logger: logger}

// creating targetNamespace on which Shipwright-Build will be deployed against, before the other
// tests takes place
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())
})
}

// manifestival instance is setup as part of controller-=runtime's SetupWithManager, thus calling
// the setup before all other methods
t.Run("setupManifestival", func(t *testing.T) {
err := r.setupManifestival(logger)
g.Expect(err).To(o.BeNil())
})

return c, r
}

// TestShipwrightBuildReconciler_Finalizers testing adding and removing finalizers on the resource.
func TestShipwrightBuildReconciler_Finalizers(t *testing.T) {
g := o.NewGomegaWithT(t)

b := &v1alpha1.ShipwrightBuild{ObjectMeta: metav1.ObjectMeta{Name: "name", Namespace: "default"}}
_, r := bootstrapShipwrightBuildReconciler(t, b)

// adding one entry on finalizers slice, making sure it's registered
t.Run("setFinalizer", func(t *testing.T) {
err := r.setFinalizer(context.TODO(), b)

g.Expect(err).To(o.BeNil())
g.Expect(b.GetFinalizers()).To(o.Equal([]string{FinalizerAnnotation}))
})

// removing previously added finalizer entry, making sure slice it's empty afterwards
t.Run("unsetFinalizer", func(t *testing.T) {
err := r.unsetFinalizer(context.TODO(), b)

g.Expect(err).To(o.BeNil())
g.Expect(b.GetFinalizers()).To(o.Equal([]string{}))
})
}

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

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

b := &v1alpha1.ShipwrightBuild{
ObjectMeta: metav1.ObjectMeta{
Name: namespacedName.Name,
Namespace: namespacedName.Namespace,
},
Spec: v1alpha1.ShipwrightBuildSpec{
TargetNamespace: targetNamespace,
},
}
c, r := bootstrapShipwrightBuildReconciler(t, b)

t.Logf("Deploying Shipwright Controller against '%s' namespace", targetNamespace)

// rolling out all manifests on the desired namespace, making sure the deployment for Shipwright
// Build Controller is created accordingly
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 = c.Get(ctx, deploymentName, &appsv1.Deployment{})
g.Expect(err).To(o.BeNil())
})

// rolling back all changes, making sure the main deployment 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())

// setting a deletion timestemp on the build object, it triggers the rollback logic so the
// reconciliation should remove the objects previously deployed
b.SetDeletionTimestamp(&metav1.Time{Time: time.Now()})
err = r.Update(ctx, b, &client.UpdateOptions{})
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 = c.Get(ctx, deploymentName, &appsv1.Deployment{})
g.Expect(errors.IsNotFound(err)).To(o.BeTrue())
})
}

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

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

// To minimize the risk of privilege escalation or destructive behavior, the controller is only
// allowed to modify named resources that deploy Shipwright Build. This is especially true for the
// cluster roles and custom resource definitions included in the release manifest.

// +kubebuilder:rbac:groups=operator.shipwright.io,resources=shipwrightbuilds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=operator.shipwright.io,resources=shipwrightbuilds/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=operator.shipwright.io,resources=shipwrightbuilds/finalizers,verbs=update
// +kubebuilder:rbac:groups=shipwright.io,resources=*,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups=core,resources=pods;services;services/finalizers;endpoints;persistentvolumeclaims;events;configmaps;secrets;serviceaccounts,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups=apps,resources=deployments;daemonsets;replicasets;statefulsets,verbs=create;delete;get;list;patch;update;watch
// +kubebuilder:rbac:groups=apps,resourceNames=shipwright-build,resources=deployments/finalizers,verbs=update
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles,verbs=get;list;watch;create
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles,resourceNames=shipwright-build-controller,verbs=update;patch;delete
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,verbs=get;list;watch;create
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,resourceNames=shipwright-build-controller,verbs=update;patch;delete
// +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch;create
// +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,resourceNames=builds.shipwright.io;buildruns.shipwright.io;buildstrategies.shipwright.io;clusterbuildstrategies.shipwright.io,verbs=update;patch;delete
// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;create
// +kubebuilder:rbac:groups=tekton.dev,resources=tasks;taskruns,verbs=create;delete;get;list;patch;update;watch
2 changes: 1 addition & 1 deletion controllers/suite_test.go
Original file line number Diff line number Diff line change
@@ -73,7 +73,7 @@ var _ = BeforeSuite(func() {
err = (&ShipwrightBuildReconciler{
Client: mgr.GetClient(),
Scheme: scheme.Scheme,
Log: ctrl.Log.WithName("controllers").WithName("shipwrightbuild"),
Logger: ctrl.Log.WithName("controllers").WithName("shipwrightbuild"),
}).SetupWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

28 changes: 28 additions & 0 deletions controllers/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package controllers

import (
"fmt"
"os"
)

// koDataPathEnv ko data-path environment variable.
const koDataPathEnv = "KO_DATA_PATH"

// koDataPath retrieve the data path environment variable, returning error when not found.
func koDataPath() (string, error) {
dataPath, exists := os.LookupEnv(koDataPathEnv)
if !exists {
return "", fmt.Errorf("'%s' is not set", koDataPathEnv)
}
return dataPath, nil
}

// contains returns true if the string if found in the slice.
func contains(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
1 change: 1 addition & 0 deletions hack/check-git-status.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#! /bin/bash

set -x
set -e

fixCommand="$*"
2 changes: 1 addition & 1 deletion hack/test-with-envtest.sh
Original file line number Diff line number Diff line change
@@ -11,4 +11,4 @@ source "${ENVTEST_ASSETS_DIR}/setup-envtest.sh"
fetch_envtest_tools "${ENVTEST_ASSETS_DIR}"
setup_envtest_env "${ENVTEST_ASSETS_DIR}"
# Run tests sequentially - the controller integration tests cannot be run concurrently
go test ./... -coverprofile cover.out -p 1
go test ./... -coverprofile cover.out -p 1 -failfast -ginkgo.v -ginkgo.failFast
41 changes: 32 additions & 9 deletions test/common.go
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ package test

import (
"context"
"fmt"
"time"

apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
@@ -13,6 +13,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
)

// timeout amount of time to wait for Eventually methods
var timeout = 30 * time.Second

// EventuallyExists checks if an object with the given namespace+name and type eventually exists.
func EventuallyExists(ctx context.Context, k8sClient client.Client, obj client.Object) {
o.Eventually(func() bool {
@@ -26,7 +29,32 @@ func EventuallyExists(ctx context.Context, k8sClient client.Client, obj client.O
}
o.Expect(err).NotTo(o.HaveOccurred())
return true
}).Should(o.BeTrue())
}, timeout).Should(o.BeTrue())
}

// EventuallyContainFinalizer retrieves and inspect the object to assert if the informed finalizer
// string is in the object.
func EventuallyContainFinalizer(
ctx context.Context,
k8sClient client.Client,
obj client.Object,
finalizer string,
) {
o.Eventually(func() bool {
key := types.NamespacedName{
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
}
if err := k8sClient.Get(ctx, key, obj); err != nil {
return false
}
for _, s := range obj.GetFinalizers() {
if s == finalizer {
return true
}
}
return false
}, timeout).Should(o.BeTrue())
}

// CRDEventuallyExists checks if a custom resource definition with the given name eventually exists.
@@ -44,13 +72,8 @@ func EventuallyRemoved(ctx context.Context, k8sClient client.Client, obj client.
o.Eventually(func() bool {
key := types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()}
err := k8sClient.Get(ctx, key, obj)
if errors.IsNotFound(err) {
return true
}
o.Expect(err).NotTo(o.HaveOccurred())
fmt.Printf("found object %s: %s\n", obj.GetObjectKind(), key)
return false
}).Should(o.BeTrue())
return errors.IsNotFound(err)
}, timeout).Should(o.BeTrue())
}

// CRDEventuallyRemoved checks if a custom resource definition has been eventually removed

0 comments on commit 15400d2

Please sign in to comment.