blob: 33da4e0caf0b1ccb6f1b4ae8c2085fa527d4fe05 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 controller
import (
"context"
"fmt"
"path"
"reflect"
"strings"
)
import (
"controllers/internal/utils"
"github.com/go-logr/logr"
"github.com/samber/lo"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
"sigs.k8s.io/gateway-api/apis/v1beta1"
)
const (
KindGateway = "Gateway"
KindSecret = "Secret"
)
var (
enableReferenceGrant bool
)
func SetEnableReferenceGrant(enable bool) {
enableReferenceGrant = enable
}
func GetEnableReferenceGrant() bool {
return enableReferenceGrant
}
func TypePredicate[T client.Object]() func(obj client.Object) bool {
return func(obj client.Object) bool {
_, ok := obj.(T)
return ok
}
}
func IsConditionPresentAndEqual(conditions []metav1.Condition, condition metav1.Condition) bool {
for _, cond := range conditions {
if cond.Type == condition.Type &&
cond.Reason == condition.Reason &&
cond.Status == condition.Status &&
cond.ObservedGeneration == condition.ObservedGeneration {
return true
}
}
return false
}
func ListRequests(ctx context.Context, c client.Client, logger logr.Logger, listObj client.ObjectList, opts ...client.ListOption) []reconcile.Request {
return ListMatchingRequests(ctx, c, logger, listObj, func(obj client.Object) bool { return true }, opts...)
}
func ListMatchingRequests(ctx context.Context, c client.Client, logger logr.Logger, listObj client.ObjectList, matchFunc func(obj client.Object) bool, opts ...client.ListOption) []reconcile.Request {
if err := c.List(ctx, listObj, opts...); err != nil {
logger.Error(err, "failed to list resource")
return nil
}
items, err := meta.ExtractList(listObj)
if err != nil {
logger.Error(err, "failed to extract list items")
return nil
}
var requests []reconcile.Request
for _, item := range items {
obj, ok := item.(client.Object)
if !ok {
continue
}
if matchFunc(obj) {
requests = append(requests, reconcile.Request{
NamespacedName: utils.NamespacedName(obj),
})
}
}
return requests
}
func getListenerStatus(ctx context.Context, mrgc client.Client, gateway *gatewayv1.Gateway) ([]gatewayv1.ListenerStatus, error) {
statuses := make(map[gatewayv1.SectionName]gatewayv1.ListenerStatus, len(gateway.Spec.Listeners))
for i, listener := range gateway.Spec.Listeners {
attachedRoutes, err := getAttachedRoutesForListener(ctx, mrgc, *gateway, listener)
if err != nil {
return nil, err
}
var (
now = metav1.Now()
conditionProgrammed = metav1.Condition{
Type: string(gatewayv1.ListenerConditionProgrammed),
Status: metav1.ConditionTrue,
ObservedGeneration: gateway.GetGeneration(),
LastTransitionTime: now,
Reason: string(gatewayv1.ListenerReasonProgrammed),
}
conditionAccepted = metav1.Condition{
Type: string(gatewayv1.ListenerConditionAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: gateway.GetGeneration(),
LastTransitionTime: now,
Reason: string(gatewayv1.ListenerReasonAccepted),
}
conditionConflicted = metav1.Condition{
Type: string(gatewayv1.ListenerConditionConflicted),
Status: metav1.ConditionTrue,
ObservedGeneration: gateway.GetGeneration(),
LastTransitionTime: now,
Reason: string(gatewayv1.ListenerReasonNoConflicts),
}
conditionResolvedRefs = metav1.Condition{
Type: string(gatewayv1.ListenerConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: gateway.GetGeneration(),
LastTransitionTime: now,
Reason: string(gatewayv1.ListenerReasonResolvedRefs),
}
supportedKinds = []gatewayv1.RouteGroupKind{}
)
status := gatewayv1.ListenerStatus{
Name: listener.Name,
Conditions: []metav1.Condition{
conditionProgrammed,
conditionAccepted,
conditionConflicted,
conditionResolvedRefs,
},
SupportedKinds: supportedKinds,
AttachedRoutes: attachedRoutes,
}
changed := false
if len(gateway.Status.Listeners) > i {
if gateway.Status.Listeners[i].AttachedRoutes != attachedRoutes {
changed = true
}
for _, condition := range status.Conditions {
if !IsConditionPresentAndEqual(gateway.Status.Listeners[i].Conditions, condition) {
changed = true
break
}
}
} else {
changed = true
}
if changed {
statuses[listener.Name] = status
} else {
statuses[listener.Name] = gateway.Status.Listeners[i]
}
}
// check for conflicts
statusArray := []gatewayv1.ListenerStatus{}
for _, status := range statuses {
statusArray = append(statusArray, status)
}
return statusArray, nil
}
func getAttachedRoutesForListener(ctx context.Context, mgrc client.Client, gateway gatewayv1.Gateway, listener gatewayv1.Listener) (int32, error) {
httpRouteList := gatewayv1.HTTPRouteList{}
if err := mgrc.List(ctx, &httpRouteList); err != nil {
return 0, err
}
var attachedRoutes int32
for _, route := range httpRouteList.Items {
route := route
acceptedByGateway := lo.ContainsBy(route.Status.Parents, func(parentStatus gatewayv1.RouteParentStatus) bool {
parentRef := parentStatus.ParentRef
if parentRef.Group != nil && *parentRef.Group != gatewayv1.GroupName {
return false
}
if parentRef.Kind != nil && *parentRef.Kind != KindGateway {
return false
}
gatewayNamespace := route.Namespace
if parentRef.Namespace != nil {
gatewayNamespace = string(*parentRef.Namespace)
}
return gateway.Namespace == gatewayNamespace && gateway.Name == string(parentRef.Name)
})
if !acceptedByGateway {
continue
}
for _, parentRef := range route.Spec.ParentRefs {
ok, _, err := checkRouteAcceptedByListener(
ctx,
mgrc,
&route,
gateway,
listener,
parentRef,
)
if err != nil {
return 0, err
}
if ok {
attachedRoutes++
}
}
}
return attachedRoutes, nil
}
func SetGatewayConditionAccepted(gw *gatewayv1.Gateway, status bool, message string) (ok bool) {
condition := metav1.Condition{
Type: string(gatewayv1.GatewayConditionAccepted),
Status: ConditionStatus(status),
Reason: string(gatewayv1.GatewayReasonAccepted),
ObservedGeneration: gw.GetGeneration(),
Message: message,
LastTransitionTime: metav1.Now(),
}
if !IsConditionPresentAndEqual(gw.Status.Conditions, condition) {
setGatewayCondition(gw, condition)
ok = true
}
return
}
func ConditionStatus(status bool) metav1.ConditionStatus {
if status {
return metav1.ConditionTrue
}
return metav1.ConditionFalse
}
func setGatewayCondition(gw *gatewayv1.Gateway, newCondition metav1.Condition) {
gw.Status.Conditions = MergeCondition(gw.Status.Conditions, newCondition)
}
func acceptedMessage(kind string) string {
return fmt.Sprintf("the %s has been accepted by the pixiu-gateway-controller", kind)
}
func referenceGrantPredicates(kind gatewayv1.Kind) predicate.Funcs {
var filter = func(obj client.Object) bool {
grant, ok := obj.(*v1beta1.ReferenceGrant)
if !ok {
return false
}
for _, from := range grant.Spec.From {
if from.Kind == kind && string(from.Group) == gatewayv1.GroupName {
return true
}
}
return false
}
predicates := predicate.NewPredicateFuncs(filter)
predicates.UpdateFunc = func(e event.UpdateEvent) bool {
return filter(e.ObjectOld) || filter(e.ObjectNew)
}
return predicates
}
func checkRouteAcceptedByListener(ctx context.Context, mgrc client.Client, route client.Object, gateway gatewayv1.Gateway, listener gatewayv1.Listener, parentRef gatewayv1.ParentReference) (bool, gatewayv1.RouteConditionReason, error) {
if parentRef.SectionName != nil {
if *parentRef.SectionName != "" && *parentRef.SectionName != listener.Name {
return false, gatewayv1.RouteReasonNoMatchingParent, nil
}
}
if parentRef.Port != nil {
if *parentRef.Port != listener.Port {
return false, gatewayv1.RouteReasonNoMatchingParent, nil
}
}
if !routeMatchesListenerType(route, listener) {
return false, gatewayv1.RouteReasonNoMatchingParent, nil
}
if !routeHostnamesIntersectsWithListenerHostname(route, listener) {
return false, gatewayv1.RouteReasonNoMatchingListenerHostname, nil
}
if ok, err := routeMatchesListenerAllowedRoutes(ctx, mgrc, route, listener.AllowedRoutes, gateway.Namespace, parentRef.Namespace); err != nil {
return false, gatewayv1.RouteReasonNotAllowedByListeners, fmt.Errorf("failed matching listener %s to a route %s for gateway %s: %w",
listener.Name, route.GetName(), gateway.Name, err,
)
} else if !ok {
return false, gatewayv1.RouteReasonNotAllowedByListeners, nil
}
return true, gatewayv1.RouteReasonAccepted, nil
}
func routeMatchesListenerAllowedRoutes(ctx context.Context, mgrc client.Client, route client.Object, allowedRoutes *gatewayv1.AllowedRoutes, gatewayNamespace string, parentRefNamespace *gatewayv1.Namespace) (bool, error) {
if allowedRoutes == nil {
return true, nil
}
if !isRouteKindAllowed(route, allowedRoutes.Kinds) {
return false, fmt.Errorf("route %s/%s is not allowed in the kind", route.GetNamespace(), route.GetName())
}
if !isRouteNamespaceAllowed(ctx, route, mgrc, gatewayNamespace, parentRefNamespace, allowedRoutes.Namespaces) {
return false, fmt.Errorf("route %s/%s is not allowed in the namespace", route.GetNamespace(), route.GetName())
}
return true, nil
}
func isRouteKindAllowed(route client.Object, kinds []gatewayv1.RouteGroupKind) (ok bool) {
ok = true
if len(kinds) > 0 {
_, ok = lo.Find(kinds, func(rgk gatewayv1.RouteGroupKind) bool {
gvk := route.GetObjectKind().GroupVersionKind()
return (rgk.Group != nil && string(*rgk.Group) == gvk.Group) && string(rgk.Kind) == gvk.Kind
})
}
return
}
func isRouteNamespaceAllowed(ctx context.Context, route client.Object, mgrc client.Client, gatewayNamespace string, parentRefNamespace *gatewayv1.Namespace, routeNamespaces *gatewayv1.RouteNamespaces) bool {
if routeNamespaces == nil || routeNamespaces.From == nil {
return true
}
switch *routeNamespaces.From {
case gatewayv1.NamespacesFromAll:
return true
case gatewayv1.NamespacesFromSame:
if parentRefNamespace == nil {
return gatewayNamespace == route.GetNamespace()
}
return route.GetNamespace() == string(*parentRefNamespace)
case gatewayv1.NamespacesFromSelector:
namespace := corev1.Namespace{}
if err := mgrc.Get(ctx, client.ObjectKey{Name: route.GetNamespace()}, &namespace); err != nil {
return false
}
s, err := metav1.LabelSelectorAsSelector(routeNamespaces.Selector)
if err != nil {
return false
}
return s.Matches(labels.Set(namespace.Labels))
default:
return true
}
}
func routeMatchesListenerType(route client.Object, listener gatewayv1.Listener) bool {
switch route.(type) {
case *gatewayv1.HTTPRoute:
if listener.Protocol != gatewayv1.HTTPProtocolType && listener.Protocol != gatewayv1.HTTPSProtocolType {
return false
}
if listener.Protocol == gatewayv1.HTTPSProtocolType {
if listener.TLS == nil {
return false
}
if listener.TLS.Mode != nil && *listener.TLS.Mode != gatewayv1.TLSModeTerminate {
return false
}
}
default:
return false
}
return true
}
func routeHostnamesIntersectsWithListenerHostname(route client.Object, listener gatewayv1.Listener) bool {
switch r := route.(type) {
case *gatewayv1.HTTPRoute:
return listenerHostnameIntersectWithRouteHostnames(listener, r.Spec.Hostnames)
default:
return false
}
}
func reconcileGatewaysMatchGatewayClass(gatewayClass client.Object, gateways []gatewayv1.Gateway) (recs []reconcile.Request) {
for _, gateway := range gateways {
if string(gateway.Spec.GatewayClassName) == gatewayClass.GetName() {
recs = append(recs, reconcile.Request{
NamespacedName: client.ObjectKey{
Name: gateway.GetName(),
Namespace: gateway.GetNamespace(),
},
})
}
}
return
}
func listenerHostnameIntersectWithRouteHostnames(listener gatewayv1.Listener, hostnames []gatewayv1.Hostname) bool {
if len(hostnames) == 0 {
return true
}
if listener.Hostname == nil || *listener.Hostname == "" {
return true
}
for _, hostname := range hostnames {
if HostnamesIntersect(string(*listener.Hostname), string(hostname)) {
return true
}
}
return false
}
func FullTypeName(a any) string {
typeOf := reflect.TypeOf(a)
pkgPath := typeOf.PkgPath()
name := typeOf.String()
if typeOf.Kind() == reflect.Ptr {
pkgPath = typeOf.Elem().PkgPath()
}
return path.Join(path.Dir(pkgPath), name)
}
func SetGatewayConditionProgrammed(gw *gatewayv1.Gateway, status bool, message string) (ok bool) {
condition := metav1.Condition{
Type: string(gatewayv1.GatewayConditionProgrammed),
Status: ConditionStatus(status),
Reason: string(gatewayv1.GatewayReasonProgrammed),
ObservedGeneration: gw.GetGeneration(),
Message: message,
LastTransitionTime: metav1.Now(),
}
if !IsConditionPresentAndEqual(gw.Status.Conditions, condition) {
setGatewayCondition(gw, condition)
ok = true
}
return
}
func SetGatewayListenerConditionProgrammed(gw *gatewayv1.Gateway, listenerName string, status bool, message string) (ok bool) {
condition := metav1.Condition{
Type: string(gatewayv1.ListenerConditionProgrammed),
Status: ConditionStatus(status),
Reason: string(gatewayv1.ListenerReasonProgrammed),
ObservedGeneration: gw.GetGeneration(),
Message: message,
LastTransitionTime: metav1.Now(),
}
if !IsConditionPresentAndEqual(gw.Status.Conditions, condition) {
setListenerCondition(gw, listenerName, condition)
ok = true
}
return
}
func SetGatewayListenerConditionResolvedRefs(gw *gatewayv1.Gateway, listenerName string, status bool, message string) (ok bool) {
condition := metav1.Condition{
Type: string(gatewayv1.ListenerConditionResolvedRefs),
Status: ConditionStatus(status),
Reason: string(gatewayv1.ListenerReasonResolvedRefs),
ObservedGeneration: gw.GetGeneration(),
Message: message,
LastTransitionTime: metav1.Now(),
}
if !IsConditionPresentAndEqual(gw.Status.Conditions, condition) {
setListenerCondition(gw, listenerName, condition)
ok = true
}
return
}
func setListenerCondition(gw *gatewayv1.Gateway, listenerName string, newCondition metav1.Condition) {
for i, listener := range gw.Status.Listeners {
if listener.Name == gatewayv1.SectionName(listenerName) {
gw.Status.Listeners[i].Conditions = MergeCondition(listener.Conditions, newCondition)
return
}
}
}
func MergeCondition(conditions []metav1.Condition, newCondition metav1.Condition) []metav1.Condition {
if newCondition.LastTransitionTime.IsZero() {
newCondition.LastTransitionTime = metav1.Now()
}
newConditions := []metav1.Condition{}
for _, condition := range conditions {
if condition.Type != newCondition.Type {
newConditions = append(newConditions, condition)
}
}
newConditions = append(newConditions, newCondition)
return newConditions
}
func HostnamesIntersect(a, b string) bool {
return HostnamesMatch(a, b) || HostnamesMatch(b, a)
}
// HostnamesMatch checks that the hostnameB matches the hostnameA. HostnameA is treated as mask
// to be checked against the hostnameB.
func HostnamesMatch(hostnameA, hostnameB string) bool {
// the hostnames are in the form of "foo.bar.com"; split them
// in a slice of substrings
hostnameALabels := strings.Split(hostnameA, ".")
hostnameBLabels := strings.Split(hostnameB, ".")
var a, b int
var wildcard bool
// iterate over the parts of both the hostnames
for a, b = 0, 0; a < len(hostnameALabels) && b < len(hostnameBLabels); a, b = a+1, b+1 {
var matchFound bool
// if the current part of B is a wildcard, we need to find the first
// A part that matches with the following B part
if wildcard {
for ; b < len(hostnameBLabels); b++ {
if hostnameALabels[a] == hostnameBLabels[b] {
matchFound = true
break
}
}
}
// if no match was found, the hostnames don't match
if wildcard && !matchFound {
return false
}
// check if at least on of the current parts are a wildcard; if so, continue
if hostnameALabels[a] == "*" {
wildcard = true
continue
}
// reset the wildcard variables
wildcard = false
// if the current a part is different from the b part, the hostnames are incompatible
if hostnameALabels[a] != hostnameBLabels[b] {
return false
}
}
return len(hostnameBLabels)-b == len(hostnameALabels)-a
}