Skip to content

Commit

Permalink
gateway: controller implementation (#1063)
Browse files Browse the repository at this point in the history
Add controllers for reconciling Gateway API objects. This consists of
watching all object kinds that may participate in the dependency graph,
validating the API specification requirements so to update the object
status, and then building a model object with all relevant configuration
information.
  • Loading branch information
kenjenkins authored Nov 11, 2024
1 parent 29f64f8 commit cb28452
Show file tree
Hide file tree
Showing 9 changed files with 1,184 additions and 0 deletions.
43 changes: 43 additions & 0 deletions controllers/gateway/conditions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package gateway

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

func upsertConditions(
conditions *[]metav1.Condition,
observedGeneration int64,
condition ...metav1.Condition,
) (modified bool) {
for _, c := range condition {
if upsertCondition(conditions, observedGeneration, c) {
modified = true
}
}
return modified
}

func upsertCondition(
conditions *[]metav1.Condition,
observedGeneration int64,
condition metav1.Condition,
) (modified bool) {
condition.ObservedGeneration = observedGeneration
condition.LastTransitionTime = metav1.Now()

conds := *conditions
for i := range conds {
if conds[i].Type == condition.Type {
// Existing condition found.
if conds[i].ObservedGeneration == condition.ObservedGeneration &&
conds[i].Status == condition.Status &&
conds[i].Reason == condition.Reason &&
conds[i].Message == condition.Message {
return false
}
conds[i] = condition
return true
}
}
// No existing condition found, so add it.
*conditions = append(*conditions, condition)
return true
}
109 changes: 109 additions & 0 deletions controllers/gateway/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Package gateway contains controllers for Gateway API objects.
package gateway

import (
context "context"
"fmt"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
gateway_v1 "sigs.k8s.io/gateway-api/apis/v1"
gateway_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"

"github.com/pomerium/ingress-controller/pomerium"
)

// DefaultClassControllerName is the default GatewayClass ControllerName.
const DefaultClassControllerName = "pomerium.io/gateway-controller"

// ControllerConfig contains configuration options for the Gateway controller.
type ControllerConfig struct {
// ControllerName associates this controller with a GatewayClass.
ControllerName string
// Gateway addresses are determined from this service.
ServiceName types.NamespacedName
}

type gatewayController struct {
client.Client
pomerium.GatewayReconciler
ControllerConfig
}

// NewGatewayController creates and registers a new controller for Gateway objects.
func NewGatewayController(
ctx context.Context,
mgr ctrl.Manager,
pgr pomerium.GatewayReconciler,
config ControllerConfig,
) error {
gtc := &gatewayController{
Client: mgr.GetClient(),
GatewayReconciler: pgr,
ControllerConfig: config,
}

err := mgr.GetFieldIndexer().IndexField(ctx, &corev1.Secret{}, "type",
func(o client.Object) []string { return []string{string(o.(*corev1.Secret).Type)} })
if err != nil {
return fmt.Errorf("couldn't create index on Secret type: %w", err)
}

// All updates will trigger the same reconcile request.
enqueueRequest := handler.EnqueueRequestsFromMapFunc(
func(_ context.Context, _ client.Object) []reconcile.Request {
return []reconcile.Request{{
NamespacedName: types.NamespacedName{
Name: config.ControllerName,
},
}}
})

err = ctrl.NewControllerManagedBy(mgr).
Named("gateway").
Watches(
&gateway_v1.Gateway{},
enqueueRequest,
builder.WithPredicates(predicate.GenerationChangedPredicate{}),
).
Watches(
&gateway_v1.HTTPRoute{},
enqueueRequest,
builder.WithPredicates(predicate.GenerationChangedPredicate{}),
).
Watches(&corev1.Secret{}, enqueueRequest).
Watches(&corev1.Namespace{}, enqueueRequest).
Watches(&corev1.Service{}, enqueueRequest).
Watches(&gateway_v1beta1.ReferenceGrant{}, enqueueRequest).
Complete(gtc)
if err != nil {
return fmt.Errorf("build controller: %w", err)
}

return nil
}

func (c *gatewayController) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) {
o, err := c.fetchObjects(ctx)
if err != nil {
return ctrl.Result{}, err
}

config, err := c.processGateways(ctx, o)
if err != nil {
return ctrl.Result{}, err
}

_, err = c.SetGatewayConfig(ctx, config)
if err != nil {
return ctrl.Result{}, err
}

return ctrl.Result{}, nil
}
130 changes: 130 additions & 0 deletions controllers/gateway/fetch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package gateway

import (
context "context"

"github.com/hashicorp/go-set/v3"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
gateway_v1 "sigs.k8s.io/gateway-api/apis/v1"
gateway_v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"

"github.com/pomerium/ingress-controller/util"
)

// objects holds all relevant Gateway objects and their dependencies.
type objects struct {
Gateways map[refKey]*gateway_v1.Gateway
HTTPRoutesByGateway map[refKey][]httpRouteInfo
OriginalHTTPRouteStatus []httpRouteAndOriginalStatus
Namespaces map[string]*corev1.Namespace
ReferenceGrants referenceGrantMap
TLSSecrets map[refKey]*corev1.Secret
Services map[types.NamespacedName]*corev1.Service
}

type httpRouteAndOriginalStatus struct {
route *gateway_v1.HTTPRoute
originalStatus *gateway_v1.HTTPRouteStatus
}

// fetchObjects fetches all relevant Gateway objects.
func (c *gatewayController) fetchObjects(ctx context.Context) (*objects, error) {
var o objects

// Fetch all GatewayClasses and filter by controller name.
var gcl gateway_v1.GatewayClassList
if err := c.List(ctx, &gcl); err != nil {
return nil, err
}
gcNames := set.New[string](0)
for i := range gcl.Items {
gc := &gcl.Items[i]
if gc.Spec.ControllerName == gateway_v1.GatewayController(c.ControllerName) {
gcNames.Insert(gc.Name)
}
}

// Fetch all Gateways and filter by GatewayClass name.
var gl gateway_v1.GatewayList
if err := c.List(ctx, &gl); err != nil {
return nil, err
}
o.Gateways = make(map[refKey]*gateway_v1.Gateway)
for i := range gl.Items {
g := &gl.Items[i]
if gcNames.Contains(string(g.Spec.GatewayClassName)) {
o.Gateways[refKeyForObject(g)] = g
}
}

// Fetch all HTTPRoutes and filter by Gateway parentRef.
var hrl gateway_v1.HTTPRouteList
if err := c.List(ctx, &hrl); err != nil {
return nil, err
}
o.HTTPRoutesByGateway = make(map[refKey][]httpRouteInfo)
for i := range hrl.Items {
hr := &hrl.Items[i]
o.OriginalHTTPRouteStatus = append(o.OriginalHTTPRouteStatus,
httpRouteAndOriginalStatus{route: hr, originalStatus: hr.Status.DeepCopy()})
ensureRouteParentStatusExists(hr, c.ControllerName)
for j := range hr.Spec.ParentRefs {
pr := &hr.Spec.ParentRefs[j]
key := refKeyForParentRef(hr, pr)
if _, ok := o.Gateways[key]; ok {
o.HTTPRoutesByGateway[key] = append(o.HTTPRoutesByGateway[key],
httpRouteInfo{hr, pr, &hr.Status.Parents[j]})
}
}
}

// Fetch all Namespaces (the labels may be needed for the allowedRoutes restrictions).
var nl corev1.NamespaceList
if err := c.List(ctx, &nl); err != nil {
return nil, err
}
o.Namespaces = make(map[string]*corev1.Namespace)
for i := range nl.Items {
n := &nl.Items[i]
o.Namespaces[n.Name] = n
}

// Fetch all ReferenceGrants.
var rgl gateway_v1beta1.ReferenceGrantList
if err := c.List(ctx, &rgl); err != nil {
return nil, err
}
o.ReferenceGrants = buildReferenceGrantMap(rgl.Items)

// Fetch all TLS secrets.
var sl corev1.SecretList
if err := c.List(ctx, &sl, client.MatchingFields{"type": string(corev1.SecretTypeTLS)}); err != nil {
return nil, err
}
o.TLSSecrets = make(map[refKey]*corev1.Secret)
for i := range sl.Items {
s := &sl.Items[i]
o.TLSSecrets[refKeyForObject(s)] = s
}

// Fetch all Services.
var servicesList corev1.ServiceList
if err := c.List(ctx, &servicesList); err != nil {
return nil, err
}
o.Services = make(map[types.NamespacedName]*corev1.Service)
for i := range servicesList.Items {
s := &servicesList.Items[i]
o.Services[util.GetNamespacedName(s)] = s
}

return &o, nil
}

type httpRouteInfo struct {
route *gateway_v1.HTTPRoute
parent *gateway_v1.ParentReference
status *gateway_v1.RouteParentStatus
}
Loading

0 comments on commit cb28452

Please sign in to comment.