-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
gateway: controller implementation (#1063)
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
1 parent
29f64f8
commit cb28452
Showing
9 changed files
with
1,184 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.