Skip to content

Commit

Permalink
[gardenlet] Switch NetworkPolicy controller to controller-runtime (
Browse files Browse the repository at this point in the history
…#6991)

* Add documentation

* Reconcile on NetworkPolicy add events

* Compare the entire NetworkPolicy spec to determine changes

* Add integration tests

* Switch network policy reconciler to native controller-runtime controller

* Address PR review feedback

* Add hostnameresolver to manager and watch events

* Address PR review feedback
  • Loading branch information
oliver-goetz committed Nov 15, 2022
1 parent 6c60ccc commit 0f08132
Show file tree
Hide file tree
Showing 16 changed files with 834 additions and 381 deletions.
8 changes: 8 additions & 0 deletions docs/concepts/gardenlet.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@ If there are no extension resources anymore, its status will be `False`.

This condition is taken into account by the `ControllerRegistration` controller part of `gardener-controller-manager` when it computes which extensions have to deployed to which seed cluster, see [this document](controller-manager.md#controllerregistration-controller) for more details.

### [`NetworkPolicy` Controller](../../pkg/gardenlet/controller/networkpolicy)

The `NetworkPolicy` controller reconciles `NetworkPolicy`s in shoot namespaces in order to ensure access to Kubernetes API server.

The controller resolves the IP address of Kubernetes service in `default` namespace and creates egress `NetworkPolicy`s for it.

For more details about `NetworkPolicy`s in Gardener please see [this document](network_policies.md).

### [`Seed` Controller](../../pkg/gardenlet/controller/seed)

The `Seed` controller in the `gardenlet` reconciles `Seed` objects with the help of the following reconcilers.
Expand Down
8 changes: 8 additions & 0 deletions pkg/gardenlet/controller/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/gardener/gardener/pkg/gardenlet/apis/config"
"github.com/gardener/gardener/pkg/gardenlet/controller/backupbucket"
"github.com/gardener/gardener/pkg/gardenlet/controller/controllerinstallation"
"github.com/gardener/gardener/pkg/gardenlet/controller/networkpolicy"
"github.com/gardener/gardener/pkg/gardenlet/controller/seed"
"github.com/gardener/gardener/pkg/gardenlet/controller/shootstate"
"github.com/gardener/gardener/pkg/healthz"
Expand Down Expand Up @@ -73,6 +74,13 @@ func AddToManager(
return fmt.Errorf("failed adding ControllerInstallation controller: %w", err)
}

if err := (&networkpolicy.Reconciler{
Config: *cfg.Controllers.SeedAPIServerNetworkPolicy,
GardenNamespace: gardenNamespace.Name,
}).AddToManager(mgr, seedCluster); err != nil {
return fmt.Errorf("failed adding NetworkPolicy controller: %w", err)
}

if err := seed.AddToManager(mgr, gardenCluster, seedCluster, seedClientSet, *cfg, identity, healthManager, imageVector, componentImageVectors); err != nil {
return fmt.Errorf("failed adding Seed controller: %w", err)
}
Expand Down
7 changes: 0 additions & 7 deletions pkg/gardenlet/controller/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
backupentrycontroller "github.com/gardener/gardener/pkg/gardenlet/controller/backupentry"
bastioncontroller "github.com/gardener/gardener/pkg/gardenlet/controller/bastion"
managedseedcontroller "github.com/gardener/gardener/pkg/gardenlet/controller/managedseed"
networkpolicycontroller "github.com/gardener/gardener/pkg/gardenlet/controller/networkpolicy"
shootcontroller "github.com/gardener/gardener/pkg/gardenlet/controller/shoot"
"github.com/gardener/gardener/pkg/utils/imagevector"
"k8s.io/utils/clock"
Expand Down Expand Up @@ -76,11 +75,6 @@ func (f *LegacyControllerFactory) Start(ctx context.Context) error {
return fmt.Errorf("failed initializing ManagedSeed controller: %w", err)
}

networkPolicyController, err := networkpolicycontroller.NewController(ctx, log, f.SeedCluster, f.Config.SeedConfig.Name)
if err != nil {
return fmt.Errorf("failed initializing NetworkPolicy controller: %w", err)
}

shootController, err := shootcontroller.NewShootController(ctx, log, f.GardenCluster, f.SeedClientSet, f.ShootClientMap, f.Config, f.Identity, f.GardenClusterIdentity, imageVector)
if err != nil {
return fmt.Errorf("failed initializing Shoot controller: %w", err)
Expand All @@ -92,7 +86,6 @@ func (f *LegacyControllerFactory) Start(ctx context.Context) error {
go backupEntryController.Run(controllerCtx, *f.Config.Controllers.BackupEntry.ConcurrentSyncs, *f.Config.Controllers.BackupEntryMigration.ConcurrentSyncs)
go bastionController.Run(controllerCtx, *f.Config.Controllers.Bastion.ConcurrentSyncs)
go managedSeedController.Run(controllerCtx, *f.Config.Controllers.ManagedSeed.ConcurrentSyncs)
go networkPolicyController.Run(controllerCtx, *f.Config.Controllers.SeedAPIServerNetworkPolicy.ConcurrentSyncs)
go shootController.Run(controllerCtx, *f.Config.Controllers.Shoot.ConcurrentSyncs, *f.Config.Controllers.ShootCare.ConcurrentSyncs, *f.Config.Controllers.ShootMigration.ConcurrentSyncs)

log.Info("gardenlet initialized")
Expand Down
152 changes: 152 additions & 0 deletions pkg/gardenlet/controller/networkpolicy/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// 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 networkpolicy

import (
"context"
"fmt"

"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/cluster"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"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"

v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
"github.com/gardener/gardener/pkg/controllerutils/mapper"
predicateutils "github.com/gardener/gardener/pkg/controllerutils/predicate"
"github.com/gardener/gardener/pkg/gardenlet/controller/networkpolicy/helper"
"github.com/gardener/gardener/pkg/gardenlet/controller/networkpolicy/hostnameresolver"
)

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

// AddToManager adds Reconciler to the given manager.
func (r *Reconciler) AddToManager(mgr manager.Manager, seedCluster cluster.Cluster) error {
if r.SeedClient == nil {
r.SeedClient = seedCluster.GetClient()
}
if r.Resolver == nil {
resolver, err := hostnameresolver.CreateForCluster(seedCluster.GetConfig(), mgr.GetLogger())
if err != nil {
return fmt.Errorf("failed to get hostnameresolver: %w", err)
}
resolverUpdate := make(chan event.GenericEvent)
resolver.WithCallback(func() {
resolverUpdate <- event.GenericEvent{}
})
if err := mgr.Add(resolver); err != nil {
return fmt.Errorf("failed to add hostnameresolver to manager: %w", err)
}
r.Resolver = resolver
r.ResolverUpdate = resolverUpdate
}
if r.ResolverUpdate == nil {
r.ResolverUpdate = make(chan event.GenericEvent)
}
if r.GardenNamespace == "" {
r.GardenNamespace = v1beta1constants.GardenNamespace
}
if r.IstioSystemNamespace == "" {
r.IstioSystemNamespace = v1beta1constants.IstioSystemNamespace
}

// It's not possible to overwrite the event handler when using the controller builder. Hence, we have to build up
// the controller manually.
c, err := controller.New(
ControllerName,
mgr,
controller.Options{
Reconciler: r,
MaxConcurrentReconciles: pointer.IntDeref(r.Config.ConcurrentSyncs, 0),
RecoverPanic: true,
},
)
if err != nil {
return err
}

if err := c.Watch(
source.NewKindWithCache(&corev1.Namespace{}, seedCluster.GetCache()),
&handler.EnqueueRequestForObject{},
predicateutils.ForEventTypes(predicateutils.Create, predicateutils.Update),
); err != nil {
return err
}

if err := c.Watch(
source.NewKindWithCache(&corev1.Endpoints{}, seedCluster.GetCache()),
mapper.EnqueueRequestsFrom(mapper.MapFunc(r.MapToNamespaces), mapper.UpdateWithNew, mgr.GetLogger()),
r.IsKubernetesEndpoint(),
); err != nil {
return err
}

if err := c.Watch(
source.NewKindWithCache(&networkingv1.NetworkPolicy{}, seedCluster.GetCache()),
mapper.EnqueueRequestsFrom(mapper.MapFunc(r.MapObjectToNamespace), mapper.UpdateWithNew, mgr.GetLogger()),
predicateutils.HasName(helper.AllowToSeedAPIServer),
); err != nil {
return err
}

return c.Watch(
&source.Channel{Source: r.ResolverUpdate},
mapper.EnqueueRequestsFrom(mapper.MapFunc(r.MapToNamespaces), mapper.UpdateWithNew, mgr.GetLogger()),
)
}

// MapToNamespaces is a mapper function which returns requests for all shoot namespaces + garden namespace + istio-system namespace.
func (r *Reconciler) MapToNamespaces(ctx context.Context, log logr.Logger, _ client.Reader, _ client.Object) []reconcile.Request {
namespaces := &corev1.NamespaceList{}
if err := r.SeedClient.List(ctx, namespaces, &client.ListOptions{
LabelSelector: shootNamespaceSelector,
}); err != nil {
log.Error(err, "Unable to list Shoot namespace for updating NetworkPolicy", "networkPolicyName", helper.AllowToSeedAPIServer)
return []reconcile.Request{}
}

requests := []reconcile.Request{
{NamespacedName: types.NamespacedName{Name: r.GardenNamespace}},
{NamespacedName: types.NamespacedName{Name: r.IstioSystemNamespace}},
}
for _, namespace := range namespaces.Items {
requests = append(requests, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&namespace)})
}

return requests
}

// MapObjectToNamespace is a mapper function which maps an object to its namespace.
func (r *Reconciler) MapObjectToNamespace(_ context.Context, _ logr.Logger, _ client.Reader, obj client.Object) []reconcile.Request {
return []reconcile.Request{{NamespacedName: types.NamespacedName{Name: obj.GetNamespace()}}}
}

// IsKubernetesEndpoint returns a predicate which evaluates if the object is the kubernetes endpoint.
func (r *Reconciler) IsKubernetesEndpoint() predicate.Predicate {
return predicate.NewPredicateFuncs(func(obj client.Object) bool {
return obj.GetNamespace() == corev1.NamespaceDefault && obj.GetName() == "kubernetes"
})
}
140 changes: 140 additions & 0 deletions pkg/gardenlet/controller/networkpolicy/add_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// 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 networkpolicy_test

import (
"context"

"github.com/go-logr/logr"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"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/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
. "github.com/gardener/gardener/pkg/gardenlet/controller/networkpolicy"
)

var _ = Describe("Add", func() {
var (
reconciler *Reconciler
)

BeforeEach(func() {
reconciler = &Reconciler{
GardenNamespace: v1beta1constants.GardenNamespace,
IstioSystemNamespace: v1beta1constants.IstioSystemNamespace,
}
})

Describe("#MapToNamespaces", func() {
var (
ctx = context.TODO()
log = logr.Discard()
fakeClient client.Client
shootNamespace *corev1.Namespace
fooNamespace *corev1.Namespace
)

BeforeEach(func() {
fakeClient = fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
reconciler.SeedClient = fakeClient

shootNamespace = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
Name: "shoot--bar",
Labels: map[string]string{v1beta1constants.GardenRole: v1beta1constants.GardenRoleShoot}},
}
fooNamespace = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}
})

It("should return a request with the shoot, gardener and istio-system namespaces' names", func() {
Expect(fakeClient.Create(ctx, shootNamespace)).To(Succeed())
Expect(fakeClient.Create(ctx, fooNamespace)).To(Succeed())
Expect(reconciler.MapToNamespaces(ctx, log, nil, nil)).To(ConsistOf(
reconcile.Request{NamespacedName: types.NamespacedName{Name: reconciler.GardenNamespace}},
reconcile.Request{NamespacedName: types.NamespacedName{Name: reconciler.IstioSystemNamespace}},
reconcile.Request{NamespacedName: types.NamespacedName{Name: "shoot--bar"}},
))
})
})

Describe("#MapObjectToNamespace", func() {
var (
ctx = context.TODO()
log = logr.Discard()
networkpolicy *networkingv1.NetworkPolicy
)

BeforeEach(func() {
networkpolicy = &networkingv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "bar",
},
}
})

It("should return a request with the namespace's name", func() {
Expect(reconciler.MapObjectToNamespace(ctx, log, nil, networkpolicy)).To(ConsistOf(
reconcile.Request{NamespacedName: types.NamespacedName{Name: networkpolicy.Namespace}},
))
})
})

Describe("#IsKubernetesEndpoint", func() {
var (
p predicate.Predicate
endpoint *corev1.Endpoints
)

BeforeEach(func() {
p = reconciler.IsKubernetesEndpoint()
endpoint = &corev1.Endpoints{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "kubernetes"}}
})

It("should return true because the endpoint is the Kubernetes endpoint", func() {
Expect(p.Create(event.CreateEvent{Object: endpoint})).To(BeTrue())
Expect(p.Update(event.UpdateEvent{ObjectNew: endpoint})).To(BeTrue())
Expect(p.Delete(event.DeleteEvent{Object: endpoint})).To(BeTrue())
Expect(p.Generic(event.GenericEvent{Object: endpoint})).To(BeTrue())
})

It("should return false because the endpoint is not the Kubernetes endpoint", func() {
endpoint.Name = "foo"

Expect(p.Create(event.CreateEvent{Object: endpoint})).To(BeFalse())
Expect(p.Update(event.UpdateEvent{ObjectNew: endpoint})).To(BeFalse())
Expect(p.Delete(event.DeleteEvent{Object: endpoint})).To(BeFalse())
Expect(p.Generic(event.GenericEvent{Object: endpoint})).To(BeFalse())
})

It("should return false because the endpoint is a Kubernetes endpoint in a different namespace", func() {
endpoint.Namespace = "bar"

Expect(p.Create(event.CreateEvent{Object: endpoint})).To(BeFalse())
Expect(p.Update(event.UpdateEvent{ObjectNew: endpoint})).To(BeFalse())
Expect(p.Delete(event.DeleteEvent{Object: endpoint})).To(BeFalse())
Expect(p.Generic(event.GenericEvent{Object: endpoint})).To(BeFalse())
})
})
})
Loading

0 comments on commit 0f08132

Please sign in to comment.