Skip to content

Commit

Permalink
Implements Gateway Controller
Browse files Browse the repository at this point in the history
Signed-off-by: Daneyon Hansen <daneyonhansen@gmail.com>
  • Loading branch information
danehans committed Jun 15, 2021
1 parent bfbe34f commit eafb94c
Show file tree
Hide file tree
Showing 17 changed files with 692 additions and 39 deletions.
13 changes: 6 additions & 7 deletions cmd/contour/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,16 +425,15 @@ func doServe(log logrus.FieldLogger, ctx *serveContext) error {
}

// Create and register the gatewayclass controller with the manager.
if ctx.Config.GatewayConfig.ControllerName != nil {
gcController := *ctx.Config.GatewayConfig.ControllerName
if _, err := controller.NewGatewayClassController(mgr, &dynamicHandler,
log.WithField("context", "gatewayclass-controller"), gcController); err != nil {
log.WithError(err).Fatal("failed to create gatewayclass-controller")
}
gcController := *ctx.Config.GatewayConfig.ControllerName
if _, err := controller.NewGatewayClassController(mgr, &dynamicHandler,
log.WithField("context", "gatewayclass-controller"), gcController); err != nil {
log.WithError(err).Fatal("failed to create gatewayclass-controller")
}

// Create and register the NewGatewayController controller with the manager.
if _, err := controller.NewGatewayController(mgr, &dynamicHandler, log.WithField("context", "gateway-controller")); err != nil {
if _, err := controller.NewGatewayController(mgr, &dynamicHandler,
log.WithField("context", "gateway-controller"), gcController); err != nil {
log.WithError(err).Fatal("failed to create gateway-controller")
}

Expand Down
1 change: 1 addition & 0 deletions examples/contour/02-role-contour.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ rules:
resources:
- backendpolicies/status
- gatewayclasses/status
- gateways/status
- httproutes/status
- tcproutes/status
- tlsroutes/status
Expand Down
15 changes: 15 additions & 0 deletions examples/gateway/02-gateway.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
kind: Gateway
apiVersion: networking.x-k8s.io/v1alpha1
metadata:
name: contour
namespace: projectcontour
spec:
gatewayClassName: example
listeners:
- protocol: HTTP
port: 80
routes:
kind: HTTPRoute
selector:
matchLabels:
app: kuard
2 changes: 2 additions & 0 deletions examples/render/contour-gateway.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# examples/contour/03-envoy.yaml
# examples/gateway/00-crds.yaml
# examples/gateway/01-gatewayclass.yaml
# examples/gateway/02-gateway.yaml
#

---
Expand Down Expand Up @@ -2843,6 +2844,7 @@ rules:
resources:
- backendpolicies/status
- gatewayclasses/status
- gateways/status
- httproutes/status
- tcproutes/status
- tlsroutes/status
Expand Down
1 change: 1 addition & 0 deletions examples/render/contour.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2841,6 +2841,7 @@ rules:
resources:
- backendpolicies/status
- gatewayclasses/status
- gateways/status
- httproutes/status
- tcproutes/status
- tlsroutes/status
Expand Down
152 changes: 131 additions & 21 deletions internal/controller/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ package controller

import (
"context"
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
internal_errors "github.com/projectcontour/contour/internal/errors"
"github.com/projectcontour/contour/internal/slice"
"github.com/projectcontour/contour/internal/status"
"github.com/projectcontour/contour/internal/validation"

"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
Expand All @@ -30,47 +35,152 @@ import (
gatewayapi_v1alpha1 "sigs.k8s.io/gateway-api/apis/v1alpha1"
)

const finalizer = "gateway.projectcontour.io/finalizer"

type gatewayReconciler struct {
client client.Client
eventHandler cache.ResourceEventHandler
logrus.FieldLogger
ctx context.Context
client client.Client
eventHandler cache.ResourceEventHandler
log logrus.FieldLogger
classController string
}

// NewGatewayController creates the gateway controller from mgr. The controller will be pre-configured
// to watch for Gateway objects across all namespaces.
func NewGatewayController(mgr manager.Manager, eventHandler cache.ResourceEventHandler, log logrus.FieldLogger) (controller.Controller, error) {
// to watch for Gateway objects across all namespaces and reconcile those that match class.
func NewGatewayController(mgr manager.Manager, eventHandler cache.ResourceEventHandler, log logrus.FieldLogger, class string) (controller.Controller, error) {
r := &gatewayReconciler{
client: mgr.GetClient(),
eventHandler: eventHandler,
FieldLogger: log,
ctx: context.Background(),
client: mgr.GetClient(),
eventHandler: eventHandler,
log: log,
classController: class,
}
c, err := controller.New("gateway-controller", mgr, controller.Options{Reconciler: r})
if err != nil {
return nil, err
}
if err := c.Watch(&source.Kind{Type: &gatewayapi_v1alpha1.Gateway{}}, &handler.EnqueueRequestForObject{}); err != nil {
if err := c.Watch(&source.Kind{Type: &gatewayapi_v1alpha1.Gateway{}}, r.enqueueRequestForOwnedGateway()); err != nil {
return nil, err
}
return c, nil
}

// enqueueRequestForOwnedGateway returns an event handler that maps events to
// Gateway objects that reference a GatewayClass owned by Contour.
func (r *gatewayReconciler) enqueueRequestForOwnedGateway() handler.EventHandler {
return handler.EnqueueRequestsFromMapFunc(func(a client.Object) []reconcile.Request {
gw, ok := a.(*gatewayapi_v1alpha1.Gateway)
if !ok {
r.log.WithField("name", a.GetName()).WithField("namespace", a.GetNamespace()).Info("invalid object, bypassing reconciliation.")
return []reconcile.Request{}
}
if err := r.classForGateway(gw); err != nil {
r.log.WithField("namespace", gw.Namespace).WithField("name", gw.Name).Info(err, ", bypassing reconciliation")
return []reconcile.Request{}
}
// The gateway references a gatewayclass that exists and is managed
// by Contour, so enqueue it for reconciliation.
r.log.WithField("namespace", gw.Namespace).WithField("name", gw.Name).Info("queueing gateway")
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Namespace: gw.Namespace,
Name: gw.Name,
},
},
}
})
}

// classForGateway returns an error if gw does not exist or is not owned by Contour.
func (r *gatewayReconciler) classForGateway(gw *gatewayapi_v1alpha1.Gateway) error {
gc := &gatewayapi_v1alpha1.GatewayClass{}
if err := r.client.Get(r.ctx, types.NamespacedName{Name: gw.Spec.GatewayClassName}, gc); err != nil {
return fmt.Errorf("failed to get gatewayclass %s: %w", gw.Spec.GatewayClassName, err)
}
if !isController(gc, r.classController) {
return fmt.Errorf("gatewayclass %s not owned by contour", gw.Spec.GatewayClassName)
}
return nil
}

// isController returns true if the controller of the provided gc matches Contour's
// GatewayClass controller string.
func isController(gc *gatewayapi_v1alpha1.GatewayClass, controller string) bool {
return gc.Spec.Controller == controller
}

func (r *gatewayReconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
r.log.WithField("namespace", request.Namespace).WithField("name", request.Name).Info("reconciling gateway")

// Fetch the Gateway from the cache.
gateway := &gatewayapi_v1alpha1.Gateway{}
err := r.client.Get(ctx, request.NamespacedName, gateway)
if errors.IsNotFound(err) {
r.eventHandler.OnDelete(&gatewayapi_v1alpha1.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: request.Name,
Namespace: request.Namespace,
},
})
gw := &gatewayapi_v1alpha1.Gateway{}
if err := r.client.Get(ctx, request.NamespacedName, gw); err != nil {
if errors.IsNotFound(err) {
r.log.WithField("name", request.Name).WithField("namespace", request.Namespace).Info("failed to find gateway")
return reconcile.Result{}, nil
}
// Error reading the object, so requeue the request.
return reconcile.Result{}, fmt.Errorf("failed to get gateway %s/%s: %w", request.Namespace, request.Name, err)
}

// Check if object is deleted.
if !gw.ObjectMeta.DeletionTimestamp.IsZero() {
r.eventHandler.OnDelete(gw)
// TODO: Add a method to remove gateway sub-resources and finalizer.
return reconcile.Result{}, nil
}

// Pass the new changed object off to the eventHandler.
r.eventHandler.OnAdd(gateway)
// The gateway is safe to process, so check if it's valid.
errs := validation.ValidateGateway(ctx, r.client, gw)
if errs != nil {
r.log.WithField("name", request.Name).WithField("namespace", request.Namespace).
Error("invalid gateway: ", internal_errors.ParseFieldErrors(errs))
} else {
// The gw is valid so finalize and ensure it.
if !isFinalized(gw) {
// Before doing anything with the gateway, ensure it has a finalizer so it can cleaned-up later.
if err := ensureFinalizer(ctx, r.client, gw); err != nil {
return reconcile.Result{}, fmt.Errorf("failed to finalize gateway %s/%s: %w", gw.Namespace, gw.Name, err)
}
r.log.WithField("name", request.Name).WithField("namespace", request.Namespace).Info("finalized gateway")
// The gateway has been mutated, so get the latest.
if err := r.client.Get(ctx, request.NamespacedName, gw); err != nil {
return reconcile.Result{}, fmt.Errorf("failed to get gateway %s/%s: %w", request.Namespace, request.Name, err)
}
}
// Pass the gateway off to the eventHandler.
r.eventHandler.OnAdd(gw)

// TODO: Ensure the gateway by creating manage infrastructure, i.e. the Envoy service.
}

if err := status.SyncGateway(ctx, r.client, gw, errs); err != nil {
return reconcile.Result{}, fmt.Errorf("failed to sync gateway %s/%s status: %w", gw.Namespace, gw.Name, err)
}
r.log.WithField("name", gw.Name).WithField("namespace", gw.Namespace).Info("synced gateway status")

return reconcile.Result{}, nil
}

// isFinalized returns true if gw is finalized.
func isFinalized(gw *gatewayapi_v1alpha1.Gateway) bool {
for _, f := range gw.Finalizers {
if f == finalizer {
return true
}
}
return false
}

// ensureFinalizer ensures the finalizer is added to the given gw.
func ensureFinalizer(ctx context.Context, cli client.Client, gw *gatewayapi_v1alpha1.Gateway) error {
if !slice.ContainsString(gw.Finalizers, finalizer) {
updated := gw.DeepCopy()
updated.Finalizers = append(updated.Finalizers, finalizer)
if err := cli.Update(ctx, updated); err != nil {
return fmt.Errorf("failed to add finalizer %s: %w", finalizer, err)
}
}
return nil
}
2 changes: 1 addition & 1 deletion internal/k8s/informers.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func IngressV1Resources() []schema.GroupVersionResource {
}

// +kubebuilder:rbac:groups="networking.x-k8s.io",resources=gatewayclasses;gateways;httproutes;backendpolicies;tlsroutes;tcproutes;udproutes,verbs=get;list;watch
// +kubebuilder:rbac:groups="networking.x-k8s.io",resources=gatewayclasses/status;httproutes/status;backendpolicies/status;tlsroutes/status;tcproutes/status;udproutes/status,verbs=update
// +kubebuilder:rbac:groups="networking.x-k8s.io",resources=gatewayclasses/status;gateways/status;httproutes/status;backendpolicies/status;tlsroutes/status;tcproutes/status;udproutes/status,verbs=update

// GatewayAPIResources returns a list of Gateway API group/version resources.
func GatewayAPIResources() []schema.GroupVersionResource {
Expand Down
42 changes: 42 additions & 0 deletions internal/slice/slice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright Project Contour Authors
// 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 slice

// RemoveString returns a newly created []string that contains all items from slice that
// are not equal to s.
func RemoveString(slice []string, s string) []string {
newSlice := make([]string, 0)
for _, item := range slice {
if item == s {
continue
}
newSlice = append(newSlice, item)
}
if len(newSlice) == 0 {
// Sanitize for unit tests so we don't need to distinguish empty array
// and nil.
newSlice = nil
}
return newSlice
}

// ContainsString checks if a given slice of strings contains the provided string.
func ContainsString(slice []string, s string) bool {
for _, item := range slice {
if item == s {
return true
}
}
return false
}

0 comments on commit eafb94c

Please sign in to comment.