Skip to content

Commit

Permalink
Switch controller to native controller-runtime controller
Browse files Browse the repository at this point in the history
Co-Authored-By: Tim Ebert <tim.ebert@sap.com>
  • Loading branch information
rfranzke and timebertt committed Jul 19, 2022
1 parent 2ae7606 commit 7325298
Show file tree
Hide file tree
Showing 8 changed files with 390 additions and 266 deletions.
9 changes: 9 additions & 0 deletions pkg/controllermanager/controller/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,23 @@ import (

"github.com/gardener/gardener/pkg/api/indexer"
"github.com/gardener/gardener/pkg/controllermanager/apis/config"
"github.com/gardener/gardener/pkg/controllermanager/controller/bastion"
"github.com/gardener/gardener/pkg/controllermanager/controller/cloudprofile"

"k8s.io/utils/clock"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
)

// AddControllersToManager adds all controller-manager controllers to the given manager.
func AddControllersToManager(mgr manager.Manager, cfg *config.ControllerManagerConfiguration) error {
if err := (&bastion.Reconciler{
Config: cfg.Controllers.Bastion,
Clock: clock.RealClock{},
}).AddToManager(mgr); err != nil {
return fmt.Errorf("failed adding Bastion controller: %w", err)
}

if err := (&cloudprofile.Reconciler{
Config: cfg.Controllers.CloudProfile,
}).AddToManager(mgr); err != nil {
Expand Down
109 changes: 109 additions & 0 deletions pkg/controllermanager/controller/bastion/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) 2022 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package bastion

import (
"context"

gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
"github.com/gardener/gardener/pkg/apis/operations"
operationsv1alpha1 "github.com/gardener/gardener/pkg/apis/operations/v1alpha1"
"github.com/gardener/gardener/pkg/controllerutils/mapper"
predicateutils "github.com/gardener/gardener/pkg/controllerutils/predicate"

"github.com/go-logr/logr"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)

// ControllerName is the name of this controller.
const ControllerName = "bastion"

// AddToManager adds Reconciler to the given manager.
func (r *Reconciler) AddToManager(mgr manager.Manager) error {
if r.Client == nil {
r.Client = mgr.GetClient()
}

c, err := builder.
ControllerManagedBy(mgr).
Named(ControllerName).
For(&operationsv1alpha1.Bastion{}, builder.WithPredicates(&predicate.GenerationChangedPredicate{})).
WithOptions(controller.Options{
MaxConcurrentReconciles: *r.Config.ConcurrentSyncs,
RecoverPanic: true,
}).
Build(r)
if err != nil {
return err
}

return c.Watch(
&source.Kind{Type: &gardencorev1beta1.Shoot{}},
mapper.EnqueueRequestsFrom(mapper.MapFunc(mapShootToBastions), mapper.UpdateWithNew, c.GetLogger()),
shootPredicate,
)
}

var (
// react on Shoot events that indicate that we need to garbage collect the Bastion
shootPredicate = predicate.Or(predicateutils.IsDeleting(), shootSeedNameChanged)

// detect update events where shoot.spec.seedName is changed, as we need to delete Bastions, if the corresponding
// Shoot is migrated to a different Seed
shootSeedNameChanged = predicate.Funcs{
CreateFunc: func(_ event.CreateEvent) bool { return false },
DeleteFunc: func(_ event.DeleteEvent) bool { return false },
GenericFunc: func(_ event.GenericEvent) bool { return false },
UpdateFunc: func(e event.UpdateEvent) bool {
oldShoot, ok := e.ObjectOld.(*gardencorev1beta1.Shoot)
if !ok {
return false
}
newShoot, ok := e.ObjectNew.(*gardencorev1beta1.Shoot)
if !ok {
return false
}

if oldShoot.Spec.SeedName == nil {
return false
}

return !apiequality.Semantic.DeepEqual(oldShoot.Spec.SeedName, newShoot.Spec.SeedName)
},
}
)

func mapShootToBastions(ctx context.Context, log logr.Logger, reader client.Reader, obj client.Object) []reconcile.Request {
shoot, ok := obj.(*gardencorev1beta1.Shoot)
if !ok {
return nil
}

bastionList := &operationsv1alpha1.BastionList{}
if err := reader.List(ctx, bastionList, client.InNamespace(shoot.Namespace), client.MatchingFields{operations.BastionShootName: shoot.Name}); err != nil {
log.Error(err, "Failed to list Bastions for shoot", "shoot", client.ObjectKeyFromObject(shoot))
return nil
}

return mapper.ObjectListToRequests(bastionList)
}
221 changes: 221 additions & 0 deletions pkg/controllermanager/controller/bastion/add_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// Copyright (c) 2022 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package bastion

import (
"context"

gardencorev1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
operationsv1alpha1 "github.com/gardener/gardener/pkg/apis/operations/v1alpha1"
"github.com/gardener/gardener/pkg/client/kubernetes"

"github.com/go-logr/logr"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

var _ = Describe("Add", func() {
Describe("shootPredicate", func() {
var obj *gardencorev1beta1.Shoot

BeforeEach(func() {
obj = &gardencorev1beta1.Shoot{}
})

Describe("#Create", func() {
var e event.CreateEvent

JustBeforeEach(func() {
e = event.CreateEvent{Object: obj}
})

It("should return false if the object is not deleting", func() {
Expect(shootPredicate.Create(e)).To(BeFalse())
})

Context("when object is deleting", func() {
BeforeEach(func() {
obj.DeletionTimestamp = &metav1.Time{}
})

It("should return true", func() {
Expect(shootPredicate.Create(e)).To(BeTrue())
})
})
})

Describe("#Delete", func() {
var e event.DeleteEvent

JustBeforeEach(func() {
e = event.DeleteEvent{Object: obj}
})

It("should return false if the object is not deleting", func() {
Expect(shootPredicate.Delete(e)).To(BeFalse())
})

Context("when object is deleting", func() {
BeforeEach(func() {
obj.DeletionTimestamp = &metav1.Time{}
})

It("should return true", func() {
Expect(shootPredicate.Delete(e)).To(BeTrue())
})
})
})

Describe("#Generic", func() {
var e event.GenericEvent

JustBeforeEach(func() {
e = event.GenericEvent{Object: obj}
})

It("should return false if the object is not deleting", func() {
Expect(shootPredicate.Generic(e)).To(BeFalse())
})

Context("when object is deleting", func() {
BeforeEach(func() {
obj.DeletionTimestamp = &metav1.Time{}
})

It("should return true", func() {
Expect(shootPredicate.Generic(e)).To(BeTrue())
})
})
})

Describe("#Update", func() {
var (
e event.UpdateEvent
objNew *gardencorev1beta1.Shoot
)

BeforeEach(func() {
objNew = obj.DeepCopy()
})

JustBeforeEach(func() {
e = event.UpdateEvent{ObjectOld: obj, ObjectNew: objNew}
})

It("should return false if the object is not deleting and seed name did not change", func() {
Expect(shootPredicate.Update(e)).To(BeFalse())
})

Context("when shoot is scheduled for the first time", func() {
BeforeEach(func() {
obj.Spec.SeedName = nil
objNew.Spec.SeedName = pointer.String("some-seed-name")
})

It("should return false", func() {
Expect(shootPredicate.Update(e)).To(BeFalse())
})
})

Context("when seed name changed", func() {
BeforeEach(func() {
obj.Spec.SeedName = pointer.String("old-seed")
objNew.Spec.SeedName = pointer.String("new-seed")
})

It("should return true", func() {
Expect(shootPredicate.Update(e)).To(BeTrue())
})
})

Context("when object is deleting", func() {
BeforeEach(func() {
objNew.DeletionTimestamp = &metav1.Time{}
})

It("should return true", func() {
Expect(shootPredicate.Update(e)).To(BeTrue())
})
})
})
})

Describe("mapShootToBastions", func() {
var (
ctx = context.TODO()
log logr.Logger
fakeClient client.Client
)

BeforeEach(func() {
log = logr.Discard()
fakeClient = fakeclient.NewClientBuilder().WithScheme(kubernetes.GardenScheme).Build()
})

It("should do nothing if the object is no shoot", func() {
Expect(mapShootToBastions(ctx, log, fakeClient, &corev1.Secret{})).To(BeEmpty())
})

It("should map the shoot to bastions", func() {
var (
shoot = &gardencorev1beta1.Shoot{
ObjectMeta: metav1.ObjectMeta{
Namespace: "some-namespace",
},
}
bastion1 = &operationsv1alpha1.Bastion{
ObjectMeta: metav1.ObjectMeta{
Name: "bastion1",
Namespace: shoot.Namespace,
},
Spec: operationsv1alpha1.BastionSpec{
ShootRef: corev1.LocalObjectReference{
Name: shoot.Name,
},
},
}
bastion2 = &operationsv1alpha1.Bastion{
ObjectMeta: metav1.ObjectMeta{
Name: "bastion2",
Namespace: shoot.Namespace,
},
Spec: operationsv1alpha1.BastionSpec{
ShootRef: corev1.LocalObjectReference{
// the fake client does not implement the field selector options, so we should better use
// the same shoot name here (otherwise, we could have tested with a different shoot name)
Name: shoot.Name,
},
},
}
)

Expect(fakeClient.Create(ctx, bastion1)).To(Succeed())
Expect(fakeClient.Create(ctx, bastion2)).To(Succeed())

Expect(mapShootToBastions(ctx, log, fakeClient, shoot)).To(ConsistOf(
reconcile.Request{NamespacedName: types.NamespacedName{Name: bastion1.Name, Namespace: bastion1.Namespace}},
reconcile.Request{NamespacedName: types.NamespacedName{Name: bastion2.Name, Namespace: bastion2.Namespace}},
))
})
})
})
Loading

0 comments on commit 7325298

Please sign in to comment.