Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,29 @@ rules:
- get
- patch
- update
- apiGroups:
- batch
resources:
- jobs
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- batch
resources:
- jobs/finalizers
verbs:
- update
- apiGroups:
- batch
resources:
- jobs/status
verbs:
- get
- patch
- update
69 changes: 63 additions & 6 deletions internal/controller/appdeployment_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,33 @@ package controller
import (
"context"

batchv1 "k8s.io/api/batch/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/log"

appv1 "github.com/Azure/operation-cache-controller/api/v1"
appsv1 "github.com/Azure/operation-cache-controller/api/v1"
apdutil "github.com/Azure/operation-cache-controller/internal/utils/controller/appdeployment"
"github.com/Azure/operation-cache-controller/internal/utils/reconciler"
)

// AppDeploymentReconciler reconciles a AppDeployment object
type AppDeploymentReconciler struct {
client.Client
Scheme *runtime.Scheme
Scheme *runtime.Scheme
recorder record.EventRecorder
}

// +kubebuilder:rbac:groups=app.github.com,resources=appdeployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=app.github.com,resources=appdeployments/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=app.github.com,resources=appdeployments/finalizers,verbs=update
// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=batch,resources=jobs/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=batch,resources=jobs/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
Expand All @@ -47,17 +57,64 @@ type AppDeploymentReconciler struct {
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.4/pkg/reconcile
func (r *AppDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = logf.FromContext(ctx)
logger := log.FromContext(ctx).WithValues(apdutil.LogKeyAppDeploymentName, req.NamespacedName)
appdeployment := &appsv1.AppDeployment{}
if err := r.Get(ctx, req.NamespacedName, appdeployment); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}

// TODO(user): your logic here
adapter := NewAppDeploymentAdapter(ctx, appdeployment, logger, r.Client, r.recorder)
return r.ReconcileHandler(ctx, adapter)
}
func (r *AppDeploymentReconciler) ReconcileHandler(ctx context.Context, adapter AppDeploymentAdapterInterface) (ctrl.Result, error) {
operations := []reconciler.ReconcileOperation{
adapter.EnsureApplicationValid,
adapter.EnsureFinalizer,
adapter.EnsureFinalizerDeleted,
adapter.EnsureDependenciesReady,
adapter.EnsureDeployingFinished,
adapter.EnsureTeardownFinished,
}

for _, operation := range operations {
operationResult, err := operation(ctx)
if err != nil || operationResult.RequeueRequest {
return ctrl.Result{RequeueAfter: operationResult.RequeueDelay}, err
}
if operationResult.CancelRequest {
return ctrl.Result{}, nil
}
}
return ctrl.Result{}, nil
}

var appDeploymentOwnerKey = ".appDeployment.metadata.controller"

// SetupWithManager sets up the controller with the Manager.
func (r *AppDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &batchv1.Job{}, appDeploymentOwnerKey,
func(rawObj client.Object) []string {
job := rawObj.(*batchv1.Job)
owner := metav1.GetControllerOf(job)
if owner == nil {
return nil
}
if owner.APIVersion != appsv1.GroupVersion.String() || owner.Kind != "AppDeployment" {
return nil
}
return []string{owner.Name}
}); err != nil {
return err
}

r.recorder = mgr.GetEventRecorderFor("AppDeployment")

return ctrl.NewControllerManagedBy(mgr).
For(&appv1.AppDeployment{}).
For(&appsv1.AppDeployment{}).
Owns(&batchv1.Job{}).
WithOptions(controller.Options{
MaxConcurrentReconciles: 100,
}).
Named("appdeployment").
Complete(r)
}
86 changes: 83 additions & 3 deletions internal/controller/appdeployment_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,21 @@ import (

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"go.uber.org/mock/gomock"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

appv1 "github.com/Azure/operation-cache-controller/api/v1"
ctrlmocks "github.com/Azure/operation-cache-controller/internal/controller/mocks"
mockpkg "github.com/Azure/operation-cache-controller/internal/mocks"
"github.com/Azure/operation-cache-controller/internal/utils/reconciler"
)

func newTestJobSpec() batchv1.JobSpec {
Expand All @@ -55,9 +61,35 @@ func newTestJobSpec() batchv1.JobSpec {
}

var _ = Describe("AppDeployment Controller", func() {
Context("When setupWithManager is called", func() {
It("Should setup the controller with the manager", func() {

// Create a new mock controller
mockCtrl := gomock.NewController(GinkgoT())
defer mockCtrl.Finish()

k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
})
Expect(err).NotTo(HaveOccurred())

err = (&AppDeploymentReconciler{
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
recorder: k8sManager.GetEventRecorderFor("appdeployment-controller"),
}).SetupWithManager(k8sManager)
Expect(err).NotTo(HaveOccurred())
})
})

Context("When reconciling a resource", func() {
const resourceName = "test-resource"

var (
mockRecorderCtrl *gomock.Controller
mockRecorder *mockpkg.MockEventRecorder
mockAdapterCtrl *gomock.Controller
mockAdapter *ctrlmocks.MockAppDeploymentAdapterInterface
)
ctx := context.Background()

typeNamespacedName := types.NamespacedName{
Expand Down Expand Up @@ -87,6 +119,10 @@ var _ = Describe("AppDeployment Controller", func() {
}}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
}
mockRecorderCtrl = gomock.NewController(GinkgoT())
mockRecorder = mockpkg.NewMockEventRecorder(mockRecorderCtrl)
mockAdapterCtrl = gomock.NewController(GinkgoT())
mockAdapter = ctrlmocks.NewMockAppDeploymentAdapterInterface(mockAdapterCtrl)
})

AfterEach(func() {
Expand All @@ -101,9 +137,18 @@ var _ = Describe("AppDeployment Controller", func() {
It("should successfully reconcile the resource", func() {
By("Reconciling the created resource")
controllerReconciler := &AppDeploymentReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
Client: k8sClient,
Scheme: k8sClient.Scheme(),
recorder: mockRecorder,
}
ctx = context.WithValue(ctx, appdeploymentAdapterContextKey{}, mockAdapter)

mockAdapter.EXPECT().EnsureApplicationValid(gomock.Any()).Return(reconciler.OperationResult{}, nil)
mockAdapter.EXPECT().EnsureFinalizer(gomock.Any()).Return(reconciler.OperationResult{}, nil)
mockAdapter.EXPECT().EnsureFinalizerDeleted(gomock.Any()).Return(reconciler.OperationResult{}, nil)
mockAdapter.EXPECT().EnsureDependenciesReady(gomock.Any()).Return(reconciler.OperationResult{}, nil)
mockAdapter.EXPECT().EnsureDeployingFinished(gomock.Any()).Return(reconciler.OperationResult{}, nil)
mockAdapter.EXPECT().EnsureTeardownFinished(gomock.Any()).Return(reconciler.OperationResult{}, nil)

_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
Expand All @@ -112,5 +157,40 @@ var _ = Describe("AppDeployment Controller", func() {
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
})
It("should cancel the reconcile loop", func() {
By("Reconciling the created resource")
controllerReconciler := &AppDeploymentReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
recorder: mockRecorder,
}
ctx = context.WithValue(ctx, appdeploymentAdapterContextKey{}, mockAdapter)

mockAdapter.EXPECT().EnsureApplicationValid(gomock.Any()).Return(reconciler.OperationResult{
CancelRequest: true,
}, nil)

_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
})

It("should fail to reconcile the resource", func() {
By("Reconciling the created resource")
controllerReconciler := &AppDeploymentReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
recorder: mockRecorder,
}
ctx = context.WithValue(ctx, appdeploymentAdapterContextKey{}, mockAdapter)

mockAdapter.EXPECT().EnsureApplicationValid(gomock.Any()).Return(reconciler.OperationResult{}, errors.NewServiceUnavailable("test error"))

_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(errors.IsServiceUnavailable(err)).To(BeTrue(), "expected error is ServiceUnavailable")
})
})
})
5 changes: 5 additions & 0 deletions test/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,15 @@ import (

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

"github.com/Azure/operation-cache-controller/test/utils"
)

func init() {
format.MaxLength = 20000
}

// namespace where the project is deployed in
const namespace = "operation-cache-controller-system"

Expand Down
Loading