Skip to content

Commit

Permalink
Merge pull request #96 from vshn/namespace-controller
Browse files Browse the repository at this point in the history
Limit scope of reconciles with dedicated namespace controller
  • Loading branch information
ccremer authored Apr 13, 2021
2 parents ab76687 + 8af1588 commit ca4a61a
Show file tree
Hide file tree
Showing 12 changed files with 318 additions and 89 deletions.
6 changes: 6 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ rules:
- get
- list
- watch
- apiGroups:
- ""
resources:
- namespaces/status
verbs:
- get
- apiGroups:
- sync.appuio.ch
resources:
Expand Down
90 changes: 90 additions & 0 deletions controllers/namespace_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package controllers

import (
"context"

"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

syncv1alpha1 "github.com/vshn/espejo/api/v1alpha1"
)

type (
// NamespaceReconciler reconciles SyncConfigs from namespace events
NamespaceReconciler struct {
Client client.Client
Log logr.Logger
Scheme *runtime.Scheme
WatchNamespace string
NewSyncConfigReconciler func() *SyncConfigReconciler
}
// NamespaceReconciliationContext holds parameters relevant for a single reconcile
NamespaceReconciliationContext struct {
namespace *corev1.Namespace
ctx context.Context
}
)

// SetupWithManager configures this reconciler with the given manager
func (r *NamespaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.Namespace{}).
Complete(r)
}

// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch
// +kubebuilder:rbac:groups=core,resources=namespaces/status,verbs=get

// Reconcile processes the given namespace event request.
func (r *NamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
ns := &corev1.Namespace{}
rc := &NamespaceReconciliationContext{
namespace: ns,
ctx: ctx,
}
name := req.Name
err := r.Client.Get(ctx, types.NamespacedName{Name: name}, ns)
if err != nil {
if apierrors.IsNotFound(err) {
r.Log.Info("Namespace does not exist, ignoring reconcile.", "namespace", name)
return ctrl.Result{}, nil
}
r.Log.Info("Could not fetch namespace", "namespace", name, "error", err.Error())
return ctrl.Result{}, err
}
if ns.Status.Phase != corev1.NamespaceActive {
r.Log.V(1).Info("Namespace is not active, ignoring reconcile.", "namespace", ns.Name, "phase", ns.Status.Phase)
return ctrl.Result{}, nil
}

configList := &syncv1alpha1.SyncConfigList{}

r.Log.Info("Reconciling from Namespace event", "namespace", name)
var options []client.ListOption
if r.WatchNamespace != "" {
options = append(options, client.InNamespace(r.WatchNamespace))
}
err = r.Client.List(ctx, configList, options...)
if err != nil {
r.Log.Error(err, "Could not get list of SyncConfig")
return ctrl.Result{}, err
}

return r.reconcileSyncConfigsForNamespace(rc, configList)
}

func (r *NamespaceReconciler) reconcileSyncConfigsForNamespace(rc *NamespaceReconciliationContext, configList *syncv1alpha1.SyncConfigList) (ctrl.Result, error) {
scr := r.NewSyncConfigReconciler()
scr.NamespaceScope = rc.namespace.Name
for _, cfg := range configList.Items {
if result, err := scr.DoReconcile(rc.ctx, &cfg); err != nil {
return result, err
}
}
return ctrl.Result{}, nil
}
124 changes: 124 additions & 0 deletions controllers/namespace_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// +build integration

package controllers

import (
"testing"
"time"

"github.com/stretchr/testify/suite"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
ctrl "sigs.k8s.io/controller-runtime"

. "github.com/vshn/espejo/api/v1alpha1"
// +kubebuilder:scaffold:imports
)

type NamespaceControllerTestSuite struct {
EnvTestSuite
scopedNs string
reconciler *NamespaceReconciler
}

func Test_Namespace(t *testing.T) {
suite.Run(t, new(NamespaceControllerTestSuite))
}

func (ts *NamespaceControllerTestSuite) BeforeTest(suiteName, testName string) {
ts.scopedNs = "scoped-" + rand.String(5)
ts.reconciler = &NamespaceReconciler{
Client: ts.Client,
Log: ts.Logger.WithName(suiteName + "_" + testName),
Scheme: ts.Scheme,
NewSyncConfigReconciler: ts.newSupplier(ts.scopedNs),
}
ns := namespaceFromString(ts.scopedNs)
ts.EnsureResources(&ns)
}

func (ts *NamespaceControllerTestSuite) Test_GivenNamespaceReconciler_WhenNamespaceUpdates_ThenLimitSyncToNamespaceOnly() {
templateCm, _ := ts.givenSyncConfig("*")

ts.whenReconciling()

ts.thenAssertSyncHappenedOnlyInScopedNamespace(templateCm)
}

func (ts *NamespaceControllerTestSuite) Test_GivenNamespaceReconciler_WhenAnIgnoredNamespaceUpdates_ThenDontSyncThatNamespace() {
templateCm, _ := ts.givenSyncConfig("shouldn't match")

ts.whenReconciling()

ts.thenAssertResourceDoesNotExist(templateCm)
}

func (ts *NamespaceControllerTestSuite) whenReconciling() {
result, err := ts.reconciler.Reconcile(ts.Ctx, ts.mapToNamespaceRequest(ts.scopedNs))

ts.Assert().NoError(err)
ts.Assert().False(result.Requeue)
ts.Assert().Equal(time.Duration(0), result.RequeueAfter)
}

func (ts *NamespaceControllerTestSuite) newSupplier(scopedNS string) func() *SyncConfigReconciler {
return func() *SyncConfigReconciler {
return &SyncConfigReconciler{
NamespaceScope: scopedNS,
Log: ts.Logger.WithName(scopedNS),
Scheme: ts.Scheme,
Client: ts.Client,
}
}
}

func (ts *NamespaceControllerTestSuite) mapToNamespaceRequest(namespace string) ctrl.Request {
return ctrl.Request{
NamespacedName: types.NamespacedName{Name: namespace},
}
}

func (ts *NamespaceControllerTestSuite) givenSyncConfig(matchNames string) (*corev1.ConfigMap, *SyncConfig) {
cm := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{
Name: "scoped-configmap-" + rand.String(5),
},
}
sc := &SyncConfig{
ObjectMeta: toObjectMeta("test-syncconfig", ts.NS),
Spec: SyncConfigSpec{
SyncItems: []unstructured.Unstructured{toUnstructured(ts.T(), cm)},
NamespaceSelector: &NamespaceSelector{MatchNames: []string{matchNames}},
},
}
ts.EnsureResources(sc)
return cm, sc
}

func (ts *NamespaceControllerTestSuite) thenAssertSyncHappenedOnlyInScopedNamespace(templateCm *corev1.ConfigMap) {
cmList := &corev1.ConfigMapList{}
ts.FetchResources(cmList)
ts.Assert().NotEmpty(cmList.Items)
count := 0
for _, cm := range cmList.Items {
if cm.Name == templateCm.Name {
count++
ts.Assert().Equal(cm.Namespace, ts.scopedNs)
}
}
ts.Assert().Equal(1, count)
}

func (ts *NamespaceControllerTestSuite) thenAssertResourceDoesNotExist(cm *corev1.ConfigMap) {
nonExistingCm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: cm.Name,
Namespace: ts.scopedNs,
},
}
ts.Assert().False(ts.IsResourceExisting(ts.Ctx, nonExistingCm))
}
Loading

0 comments on commit ca4a61a

Please sign in to comment.