mirror of
https://github.com/clastix/kamaji.git
synced 2026-02-14 10:00:02 +00:00
This change extends Gateway API support to Konnectivity addons. When `spec.controlPlane.gateway` is configured and Konnectivity addon is enabled, Kamaji automatically creates two TLSRoutes: 1. A Control plane TLSRoute (port 6443, sectionName "kube-apiserver") 2. A Konnectivity TLSRoute (port 8132, sectionName "konnectivity-server") Both routes use the hostname specified in `gateway.hostname` and reference the same Gateway resource via `parentRefs`, with `port` and `sectionName` set automatically by Kamaji. This patch also adds CEL validation to prevent users from specifying `port` or `sectionName` in Gateway `parentRefs`, as these fields are now managed automatically by Kamaji. Signed-off-by: Parth Yadav <parth@coredge.io>
242 lines
8.5 KiB
Go
242 lines
8.5 KiB
Go
// Copyright 2022 Clastix Labs
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package resources
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
|
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/fields"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
|
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
|
|
|
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
|
)
|
|
|
|
// fetchGatewayByListener uses the indexer to efficiently find a gateway with a specific listener.
|
|
// This avoids the need to iterate through all listeners in a gateway.
|
|
func fetchGatewayByListener(ctx context.Context, c client.Client, ref gatewayv1.ParentReference) (*gatewayv1.Gateway, error) {
|
|
if ref.SectionName == nil {
|
|
return nil, fmt.Errorf("missing sectionName")
|
|
}
|
|
|
|
// Build the composite key that matches our indexer format: namespace/gatewayName/listenerName
|
|
listenerKey := fmt.Sprintf("%s/%s/%s", *ref.Namespace, ref.Name, *ref.SectionName)
|
|
|
|
// Query gateways using the indexer
|
|
gatewayList := &gatewayv1.GatewayList{}
|
|
if err := c.List(ctx, gatewayList, client.MatchingFieldsSelector{
|
|
Selector: fields.OneTermEqualSelector(kamajiv1alpha1.GatewayListenerNameKey, listenerKey),
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("failed to list gateways by listener: %w", err)
|
|
}
|
|
|
|
if len(gatewayList.Items) == 0 {
|
|
return nil, fmt.Errorf("no gateway found with listener '%s'", *ref.SectionName)
|
|
}
|
|
|
|
// Since we're using a composite key with namespace/name/listener, we should get exactly one result
|
|
if len(gatewayList.Items) > 1 {
|
|
return nil, fmt.Errorf("found multiple gateways with listener '%s', expected exactly one", *ref.SectionName)
|
|
}
|
|
|
|
return &gatewayList.Items[0], nil
|
|
}
|
|
|
|
// FindMatchingListener finds a listener in the given list that matches the parent reference.
|
|
func FindMatchingListener(listeners []gatewayv1.Listener, ref gatewayv1.ParentReference) (gatewayv1.Listener, error) {
|
|
if ref.SectionName == nil {
|
|
return gatewayv1.Listener{}, fmt.Errorf("missing sectionName")
|
|
}
|
|
name := *ref.SectionName
|
|
for _, listener := range listeners {
|
|
if listener.Name == name {
|
|
return listener, nil
|
|
}
|
|
}
|
|
|
|
// TODO: Handle the cases according to the spec:
|
|
// - When both Port (experimental) and SectionName are
|
|
// specified, the name and port of the selected listener
|
|
// must match both specified values.
|
|
// - When unspecified (empty string) this will reference
|
|
// the entire resource [...] an attachment is considered
|
|
// successful if at least one section in the parent resource accepts it
|
|
|
|
return gatewayv1.Listener{}, fmt.Errorf("could not find listener '%s'", name)
|
|
}
|
|
|
|
// IsGatewayRouteStatusChanged checks if the gateway route status has changed compared to the stored status.
|
|
// Returns true if the status has changed (update needed), false if it's the same.
|
|
func IsGatewayRouteStatusChanged(currentStatus *kamajiv1alpha1.KubernetesGatewayStatus, resourceStatus gatewayv1alpha2.RouteStatus) bool {
|
|
if currentStatus == nil {
|
|
return true
|
|
}
|
|
|
|
// Compare RouteStatus - check if number of parents changed
|
|
if len(currentStatus.RouteStatus.Parents) != len(resourceStatus.Parents) {
|
|
return true
|
|
}
|
|
|
|
// Compare individual parent statuses
|
|
// NOTE: Multiple Parent References are assumed.
|
|
for i, currentParent := range currentStatus.RouteStatus.Parents {
|
|
if i >= len(resourceStatus.Parents) {
|
|
return true
|
|
}
|
|
|
|
resourceParent := resourceStatus.Parents[i]
|
|
|
|
// Compare parent references
|
|
if currentParent.ParentRef.Name != resourceParent.ParentRef.Name ||
|
|
(currentParent.ParentRef.Namespace == nil) != (resourceParent.ParentRef.Namespace == nil) ||
|
|
(currentParent.ParentRef.Namespace != nil && resourceParent.ParentRef.Namespace != nil &&
|
|
*currentParent.ParentRef.Namespace != *resourceParent.ParentRef.Namespace) ||
|
|
(currentParent.ParentRef.SectionName == nil) != (resourceParent.ParentRef.SectionName == nil) ||
|
|
(currentParent.ParentRef.SectionName != nil && resourceParent.ParentRef.SectionName != nil &&
|
|
*currentParent.ParentRef.SectionName != *resourceParent.ParentRef.SectionName) {
|
|
return true
|
|
}
|
|
|
|
if len(currentParent.Conditions) != len(resourceParent.Conditions) {
|
|
return true
|
|
}
|
|
|
|
// Compare each condition
|
|
for j, currentCondition := range currentParent.Conditions {
|
|
if j >= len(resourceParent.Conditions) {
|
|
return true
|
|
}
|
|
|
|
resourceCondition := resourceParent.Conditions[j]
|
|
|
|
if currentCondition.Type != resourceCondition.Type ||
|
|
currentCondition.Status != resourceCondition.Status ||
|
|
currentCondition.Reason != resourceCondition.Reason ||
|
|
currentCondition.Message != resourceCondition.Message ||
|
|
!currentCondition.LastTransitionTime.Equal(&resourceCondition.LastTransitionTime) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Since access points are derived from route status and gateway conditions,
|
|
// and we've already compared the route status above, we can assume that
|
|
// if the route status hasn't changed, the access points calculation
|
|
// will produce the same result. This avoids the need for complex
|
|
// gateway fetching in the status comparison.
|
|
//
|
|
// If there are edge cases where gateway state changes but route status doesn't,
|
|
// those will be caught in the next reconciliation cycle anyway.
|
|
return false
|
|
}
|
|
|
|
// CleanupTLSRoute cleans up a TLSRoute resource if it's managed by the given TenantControlPlane.
|
|
func CleanupTLSRoute(ctx context.Context, c client.Client, routeName, routeNamespace string, tcp metav1.Object) (bool, error) {
|
|
route := gatewayv1alpha2.TLSRoute{}
|
|
if err := c.Get(ctx, client.ObjectKey{
|
|
Namespace: routeNamespace,
|
|
Name: routeName,
|
|
}, &route); err != nil {
|
|
if !k8serrors.IsNotFound(err) {
|
|
return false, fmt.Errorf("failed to get TLSRoute before cleanup: %w", err)
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
if !metav1.IsControlledBy(&route, tcp) {
|
|
return false, nil
|
|
}
|
|
|
|
if err := c.Delete(ctx, &route); err != nil {
|
|
if !k8serrors.IsNotFound(err) {
|
|
return false, fmt.Errorf("cannot delete TLSRoute route: %w", err)
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// BuildGatewayAccessPointsStatus builds access points from route statuses.
|
|
func BuildGatewayAccessPointsStatus(ctx context.Context, c client.Client, route *gatewayv1alpha2.TLSRoute, routeStatuses gatewayv1alpha2.RouteStatus) ([]kamajiv1alpha1.GatewayAccessPoint, error) {
|
|
accessPoints := []kamajiv1alpha1.GatewayAccessPoint{}
|
|
routeNamespace := gatewayv1.Namespace(route.Namespace)
|
|
|
|
for _, routeStatus := range routeStatuses.Parents {
|
|
routeAccepted := meta.IsStatusConditionTrue(
|
|
routeStatus.Conditions,
|
|
string(gatewayv1.RouteConditionAccepted),
|
|
)
|
|
if !routeAccepted {
|
|
continue
|
|
}
|
|
|
|
if routeStatus.ParentRef.Namespace == nil {
|
|
// Set the namespace to the route namespace if not set
|
|
routeStatus.ParentRef.Namespace = &routeNamespace
|
|
}
|
|
|
|
// Use the indexer to efficiently find the gateway with the specific listener
|
|
gateway, err := fetchGatewayByListener(ctx, c, routeStatus.ParentRef)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not fetch gateway with listener '%v': %w",
|
|
routeStatus.ParentRef.SectionName, err)
|
|
}
|
|
gatewayProgrammed := meta.IsStatusConditionTrue(
|
|
gateway.Status.Conditions,
|
|
string(gatewayv1.GatewayConditionProgrammed),
|
|
)
|
|
if !gatewayProgrammed {
|
|
continue
|
|
}
|
|
|
|
// Since we fetched the gateway using the indexer, we know the listener exists
|
|
// but we still need to get its details from the gateway spec
|
|
listener, err := FindMatchingListener(
|
|
gateway.Spec.Listeners, routeStatus.ParentRef,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to match listener: %w", err)
|
|
}
|
|
|
|
for _, hostname := range route.Spec.Hostnames {
|
|
rawURL := fmt.Sprintf("https://%s:%d", hostname, listener.Port)
|
|
parsedURL, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid url: %w", err)
|
|
}
|
|
|
|
hostnameAddressType := gatewayv1.HostnameAddressType
|
|
accessPoints = append(accessPoints, kamajiv1alpha1.GatewayAccessPoint{
|
|
Type: &hostnameAddressType,
|
|
Value: parsedURL.String(),
|
|
Port: listener.Port,
|
|
})
|
|
}
|
|
}
|
|
|
|
return accessPoints, nil
|
|
}
|
|
|
|
// NewParentRefsSpecWithPortAndSection creates a copy of parentRefs with port and sectionName set for each reference.
|
|
func NewParentRefsSpecWithPortAndSection(parentRefs []gatewayv1.ParentReference, port int32, sectionName string) []gatewayv1.ParentReference {
|
|
result := make([]gatewayv1.ParentReference, len(parentRefs))
|
|
sectionNamePtr := gatewayv1.SectionName(sectionName)
|
|
for i, parentRef := range parentRefs {
|
|
result[i] = *parentRef.DeepCopy()
|
|
result[i].Port = &port
|
|
result[i].SectionName = §ionNamePtr
|
|
}
|
|
|
|
return result
|
|
}
|