Support for cross-namespace references / GatewayAPI ReferenceGrants

This commit is contained in:
Pascal Hofmann 2024-01-30 16:44:05 +01:00 committed by GitHub
parent 8b77f0c2dd
commit 9be523d772
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 940 additions and 67 deletions

View file

@ -153,17 +153,16 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
RequiredConsecutiveSuccesses: 0,
},
SupportedFeatures: sets.New[ksuite.SupportedFeature]().
Insert(ksuite.GatewayCoreFeatures.UnsortedList()...),
Insert(ksuite.GatewayCoreFeatures.UnsortedList()...).
Insert(ksuite.ReferenceGrantCoreFeatures.UnsortedList()...),
EnableAllSupportedFeatures: false,
RunTest: *k8sConformanceRunTest,
// Until the feature are all supported, following tests are skipped.
SkipTests: []string{
"HTTPExactPathMatching",
"HTTPRouteHostnameIntersection",
"GatewaySecretReferenceGrantAllInNamespace",
"HTTPRouteListenerHostnameMatching",
"HTTPRouteRequestHeaderModifier",
"GatewaySecretInvalidReferenceGrant",
"GatewayClassObservedGenerationBump",
"HTTPRouteInvalidNonExistentBackendRef",
"GatewayWithAttachedRoutes",
@ -171,14 +170,11 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
"HTTPRouteDisallowedKind",
"HTTPRouteInvalidReferenceGrant",
"HTTPRouteObservedGenerationBump",
"GatewayInvalidRouteKind",
"TLSRouteSimpleSameNamespace",
"TLSRouteInvalidReferenceGrant",
"HTTPRouteInvalidCrossNamespaceParentRef",
"HTTPRouteInvalidParentRefNotMatchingSectionName",
"GatewaySecretReferenceGrantSpecific",
"GatewayModifyListeners",
"GatewaySecretMissingReferenceGrant",
"GatewayInvalidTLSConfiguration",
"HTTPRouteInvalidCrossNamespaceBackendRef",
"HTTPRouteMatchingAcrossRoutes",

View file

@ -19,6 +19,7 @@ import (
"k8s.io/client-go/tools/clientcmd"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
gatev1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
gateclientset "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
gateinformers "sigs.k8s.io/gateway-api/pkg/client/informers/externalversions"
)
@ -34,13 +35,7 @@ func (reh *resourceEventHandler) OnAdd(obj interface{}, isInInitialList bool) {
}
func (reh *resourceEventHandler) OnUpdate(oldObj, newObj interface{}) {
switch oldObj.(type) {
case *gatev1.GatewayClass:
// Skip update for gateway classes. We only manage addition or deletion for this cluster-wide resource.
return
default:
eventHandlerFunc(reh.ev, newObj)
}
eventHandlerFunc(reh.ev, newObj)
}
func (reh *resourceEventHandler) OnDelete(obj interface{}) {
@ -59,6 +54,7 @@ type Client interface {
GetHTTPRoutes(namespaces []string) ([]*gatev1.HTTPRoute, error)
GetTCPRoutes(namespaces []string) ([]*gatev1alpha2.TCPRoute, error)
GetTLSRoutes(namespaces []string) ([]*gatev1alpha2.TLSRoute, error)
GetReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error)
GetService(namespace, name string) (*corev1.Service, bool, error)
GetSecret(namespace, name string) (*corev1.Secret, bool, error)
GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error)
@ -189,9 +185,6 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
return nil, err
}
// TODO manage Reference Policy
// https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.ReferencePolicy
for _, ns := range namespaces {
factoryGateway := gateinformers.NewSharedInformerFactoryWithOptions(c.csGateway, resyncPeriod, gateinformers.WithNamespace(ns))
_, err = factoryGateway.Gateway().V1().Gateways().Informer().AddEventHandler(eventHandler)
@ -210,6 +203,10 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
if err != nil {
return nil, err
}
_, err = factoryGateway.Gateway().V1beta1().ReferenceGrants().Informer().AddEventHandler(eventHandler)
if err != nil {
return nil, err
}
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns))
_, err = factoryKube.Core().V1().Services().Informer().AddEventHandler(eventHandler)
@ -363,6 +360,21 @@ func (c *clientWrapper) GetTLSRoutes(namespaces []string) ([]*gatev1alpha2.TLSRo
return tlsRoutes, nil
}
func (c *clientWrapper) GetReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error) {
if !c.isWatchedNamespace(namespace) {
log.Warn().Msgf("Failed to get ReferenceGrants: %q is not within watched namespaces", namespace)
return nil, fmt.Errorf("failed to get ReferenceGrants: namespace %s is not within watched namespaces", namespace)
}
referenceGrants, err := c.factoriesGateway[c.lookupNamespace(namespace)].Gateway().V1beta1().ReferenceGrants().Lister().ReferenceGrants(namespace).List(labels.Everything())
if err != nil {
return nil, err
}
return referenceGrants, nil
}
func (c *clientWrapper) GetGateways() []*gatev1.Gateway {
var result []*gatev1.Gateway
@ -388,7 +400,7 @@ func (c *clientWrapper) UpdateGatewayClassStatus(gatewayClass *gatev1.GatewayCla
var newConditions []metav1.Condition
for _, cond := range gc.Status.Conditions {
// No update for identical condition.
if cond.Type == condition.Type && cond.Status == condition.Status {
if cond.Type == condition.Type && cond.Status == condition.Status && cond.ObservedGeneration == condition.ObservedGeneration {
return nil
}
@ -470,7 +482,7 @@ func conditionsEquals(conditionsA, conditionsB []metav1.Condition) bool {
for _, conditionA := range conditionsA {
for _, conditionB := range conditionsB {
if conditionA.Type == conditionB.Type {
if conditionA.Reason != conditionB.Reason || conditionA.Status != conditionB.Status || conditionA.Message != conditionB.Message {
if conditionA.Reason != conditionB.Reason || conditionA.Status != conditionB.Status || conditionA.Message != conditionB.Message || conditionA.ObservedGeneration != conditionB.ObservedGeneration {
return false
}
conditionMatches++

View file

@ -12,6 +12,7 @@ import (
kscheme "k8s.io/client-go/kubernetes/scheme"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
gatev1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
)
var _ Client = (*clientMock)(nil)
@ -23,6 +24,11 @@ func init() {
panic(err)
}
err = gatev1beta1.AddToScheme(kscheme.Scheme)
if err != nil {
panic(err)
}
err = gatev1.AddToScheme(kscheme.Scheme)
if err != nil {
panic(err)
@ -39,11 +45,12 @@ type clientMock struct {
apiSecretError error
apiEndpointsError error
gatewayClasses []*gatev1.GatewayClass
gateways []*gatev1.Gateway
httpRoutes []*gatev1.HTTPRoute
tcpRoutes []*gatev1alpha2.TCPRoute
tlsRoutes []*gatev1alpha2.TLSRoute
gatewayClasses []*gatev1.GatewayClass
gateways []*gatev1.Gateway
httpRoutes []*gatev1.HTTPRoute
tcpRoutes []*gatev1alpha2.TCPRoute
tlsRoutes []*gatev1alpha2.TLSRoute
referenceGrants []*gatev1beta1.ReferenceGrant
watchChan chan interface{}
}
@ -78,6 +85,8 @@ func newClientMock(paths ...string) clientMock {
c.tcpRoutes = append(c.tcpRoutes, o)
case *gatev1alpha2.TLSRoute:
c.tlsRoutes = append(c.tlsRoutes, o)
case *gatev1beta1.ReferenceGrant:
c.referenceGrants = append(c.referenceGrants, o)
default:
panic(fmt.Sprintf("Unknown runtime object %+v %T", o, o))
}
@ -190,6 +199,16 @@ func (c clientMock) GetTLSRoutes(namespaces []string) ([]*gatev1alpha2.TLSRoute,
return tlsRoutes, nil
}
func (c clientMock) GetReferenceGrants(namespace string) ([]*gatev1beta1.ReferenceGrant, error) {
var referenceGrants []*gatev1beta1.ReferenceGrant
for _, referenceGrant := range c.referenceGrants {
if inNamespace(referenceGrant.ObjectMeta, namespace) {
referenceGrants = append(referenceGrants, referenceGrant)
}
}
return referenceGrants, nil
}
func (c clientMock) GetService(namespace, name string) (*corev1.Service, bool, error) {
if c.apiServiceError != nil {
return nil, false, c.apiServiceError

View file

@ -0,0 +1,78 @@
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: secret-from-default
namespace: secret-namespace
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: default
to:
- group: ""
kind: Secret
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: secret-namespace
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9000
hostname: foo.example.com
tls:
mode: Terminate # Default mode
certificateRefs:
- kind: Secret
name: supersecret
namespace: secret-namespace
group: ""
allowedRoutes:
kinds:
- kind: TCPRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TCPRoute
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: tcp-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""

View file

@ -0,0 +1,64 @@
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: secret-namespace
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9000
hostname: foo.example.com
tls:
mode: Terminate # Default mode
certificateRefs:
- kind: Secret
name: supersecret
namespace: secret-namespace
group: ""
allowedRoutes:
kinds:
- kind: TCPRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TCPRoute
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: tcp-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""

View file

@ -0,0 +1,78 @@
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: secret-from-default
namespace: secret-namespace
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: differentnamespace
to:
- group: ""
kind: Secret
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: secret-namespace
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9000
hostname: foo.example.com
tls:
mode: Terminate # Default mode
certificateRefs:
- kind: Secret
name: supersecret
namespace: secret-namespace
group: ""
allowedRoutes:
kinds:
- kind: TCPRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TCPRoute
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: tcp-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""

View file

@ -0,0 +1,79 @@
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: secret-from-default
namespace: secret-namespace
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: default
to:
- group: ""
kind: Secret
name: differentsecret
---
apiVersion: v1
kind: Secret
metadata:
name: supersecret
namespace: secret-namespace
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=
---
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway-class
spec:
controllerName: traefik.io/gateway-controller
---
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1
metadata:
name: my-gateway
namespace: default
spec:
gatewayClassName: my-gateway-class
listeners: # Use GatewayClass defaults for listener definition.
- name: tls
protocol: TLS
port: 9000
hostname: foo.example.com
tls:
mode: Terminate # Default mode
certificateRefs:
- kind: Secret
name: supersecret
namespace: secret-namespace
group: ""
allowedRoutes:
kinds:
- kind: TCPRoute
group: gateway.networking.k8s.io
namespaces:
from: Same
---
kind: TCPRoute
apiVersion: gateway.networking.k8s.io/v1alpha2
metadata:
name: tcp-app-1
namespace: default
spec:
parentRefs:
- name: my-gateway
kind: Gateway
group: gateway.networking.k8s.io
rules:
- backendRefs:
- name: whoamitcp
port: 9000
weight: 1
kind: Service
group: ""

View file

@ -34,11 +34,14 @@ import (
"k8s.io/utils/ptr"
"k8s.io/utils/strings/slices"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
gatev1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
)
const (
providerName = "kubernetesgateway"
groupCore = "core"
kindGateway = "Gateway"
kindTraefikService = "TraefikService"
kindHTTPRoute = "HTTPRoute"
@ -348,6 +351,7 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway *
Name: listener.Name,
SupportedKinds: []gatev1.RouteGroupKind{},
Conditions: []metav1.Condition{},
// AttachedRoutes: 0 TODO Set to number of Routes associated with a Listener regardless of Gateway or Route status
}
supportedKinds, conditions := supportedRouteKinds(listener.Protocol)
@ -356,9 +360,8 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway *
continue
}
listenerStatuses[i].SupportedKinds = supportedKinds
routeKinds, conditions := getAllowedRouteKinds(gateway, listener, supportedKinds)
listenerStatuses[i].SupportedKinds = routeKinds
if len(conditions) > 0 {
listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, conditions...)
continue
@ -474,7 +477,7 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway *
certificateRef := listener.TLS.CertificateRefs[0]
if certificateRef.Kind == nil || *certificateRef.Kind != "Secret" ||
certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != "core") {
certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) {
// update "ResolvedRefs" status true with "InvalidCertificateRef" reason
listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
@ -482,43 +485,74 @@ func (p *Provider) fillGatewayConf(ctx context.Context, client Client, gateway *
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonInvalidCertificateRef),
Message: fmt.Sprintf("Unsupported TLS CertificateRef group/kind: %v/%v", certificateRef.Group, certificateRef.Kind),
Message: fmt.Sprintf("Unsupported TLS CertificateRef group/kind: %s/%s", groupToString(certificateRef.Group), kindToString(certificateRef.Kind)),
})
continue
}
// TODO Support ReferencePolicy to support cross namespace references.
certificateNamespace := gateway.Namespace
if certificateRef.Namespace != nil && string(*certificateRef.Namespace) != gateway.Namespace {
listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonInvalidCertificateRef),
Message: "Cross namespace secrets are not supported",
})
continue
certificateNamespace = string(*certificateRef.Namespace)
}
configKey := gateway.Namespace + "/" + string(certificateRef.Name)
if _, tlsExists := tlsConfigs[configKey]; !tlsExists {
tlsConf, err := getTLS(client, certificateRef.Name, gateway.Namespace)
if certificateNamespace != gateway.Namespace {
referenceGrants, err := client.GetReferenceGrants(certificateNamespace)
if err != nil {
// update "ResolvedRefs" status true with "InvalidCertificateRef" reason
listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonInvalidCertificateRef),
Message: fmt.Sprintf("Error while retrieving certificate: %v", err),
Reason: string(gatev1.ListenerReasonRefNotPermitted),
Message: fmt.Sprintf("Cannot find any ReferenceGrant: %v", err),
})
continue
}
referenceGrants = filterReferenceGrantsFrom(referenceGrants, "gateway.networking.k8s.io", "Gateway", gateway.Namespace)
referenceGrants = filterReferenceGrantsTo(referenceGrants, groupCore, "Secret", string(certificateRef.Name))
if len(referenceGrants) == 0 {
listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonRefNotPermitted),
Message: "Required ReferenceGrant for cross namespace secret reference is missing",
})
continue
}
}
configKey := certificateNamespace + "/" + string(certificateRef.Name)
if _, tlsExists := tlsConfigs[configKey]; !tlsExists {
tlsConf, err := getTLS(client, certificateRef.Name, certificateNamespace)
if err != nil {
// update "ResolvedRefs" status false with "InvalidCertificateRef" reason
// update "Programmed" status false with "Invalid" reason
listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions,
metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonInvalidCertificateRef),
Message: fmt.Sprintf("Error while retrieving certificate: %v", err),
},
metav1.Condition{
Type: string(gatev1.ListenerConditionProgrammed),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonInvalid),
Message: fmt.Sprintf("Error while retrieving certificate: %v", err),
},
)
continue
}
tlsConfigs[configKey] = tlsConf
}
}
@ -548,15 +582,32 @@ func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses [
var result error
for i, listener := range listenerStatuses {
if len(listener.Conditions) == 0 {
// GatewayConditionReady "Ready", GatewayConditionReason "ListenerReady"
listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions, metav1.Condition{
Type: string(gatev1.ListenerReasonAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: "ListenerReady",
Message: "No error found",
})
listenerStatuses[i].Conditions = append(listenerStatuses[i].Conditions,
metav1.Condition{
Type: string(gatev1.ListenerConditionAccepted),
Status: metav1.ConditionTrue,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonAccepted),
Message: "No error found",
},
metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionTrue,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonResolvedRefs),
Message: "No error found",
},
metav1.Condition{
Type: string(gatev1.ListenerConditionProgrammed),
Status: metav1.ConditionTrue,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonProgrammed),
Message: "No error found",
},
)
continue
}
@ -565,6 +616,7 @@ func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses [
result = multierror.Append(result, errors.New(condition.Message))
}
}
gatewayStatus.Listeners = listenerStatuses
if result != nil {
// GatewayConditionReady "Ready", GatewayConditionReason "ListenersNotValid"
@ -580,8 +632,6 @@ func (p *Provider) makeGatewayStatus(gateway *gatev1.Gateway, listenerStatuses [
return gatewayStatus, result
}
gatewayStatus.Listeners = listenerStatuses
gatewayStatus.Conditions = append(gatewayStatus.Conditions,
// update "Accepted" status with "Accepted" reason
metav1.Condition{
@ -656,7 +706,7 @@ func getAllowedRouteKinds(gateway *gatev1.Gateway, listener gatev1.Listener, sup
}
var (
routeKinds []gatev1.RouteGroupKind
routeKinds = []gatev1.RouteGroupKind{}
conditions []metav1.Condition
)
@ -672,12 +722,12 @@ func getAllowedRouteKinds(gateway *gatev1.Gateway, listener gatev1.Listener, sup
if !isSupported {
conditions = append(conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionAccepted),
Status: metav1.ConditionTrue,
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(gatev1.ListenerReasonInvalidRouteKinds),
Message: fmt.Sprintf("Listener protocol %q does not support RouteGroupKind %v/%s", listener.Protocol, routeKind.Group, routeKind.Kind),
Message: fmt.Sprintf("Listener protocol %q does not support RouteGroupKind %s/%s", listener.Protocol, groupToString(routeKind.Group), routeKind.Kind),
})
continue
}
@ -712,7 +762,7 @@ func (p *Provider) gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, li
routes, err := client.GetHTTPRoutes(namespaces)
if err != nil {
// update "ResolvedRefs" status true with "InvalidRoutesRef" reason
// update "ResolvedRefs" status true with "RefNotPermitted" reason
return []metav1.Condition{{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
@ -757,7 +807,7 @@ func (p *Provider) gatewayHTTPRouteToHTTPConf(ctx context.Context, ep string, li
for _, routeRule := range route.Spec.Rules {
rule, err := extractRule(routeRule, hostRule)
if err != nil {
// update "ResolvedRefs" status true with "DroppedRoutes" reason
// update "ResolvedRefs" status true with "UnsupportedPathOrHeaderType" reason
conditions = append(conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
@ -1048,7 +1098,7 @@ func gatewayTLSRouteToTCPConf(ctx context.Context, ep string, listener gatev1.Li
Type: string(gatev1.GatewayClassConditionStatusAccepted),
Status: metav1.ConditionFalse,
ObservedGeneration: gateway.Generation,
Reason: string(gatev1.ListenerConditionConflicted),
Reason: string(gatev1.ListenerReasonHostnameConflict),
Message: fmt.Sprintf("No hostname match between listener: %v and route: %v", listener.Hostname, route.Spec.Hostnames),
LastTransitionTime: metav1.Now(),
})
@ -1059,7 +1109,7 @@ func gatewayTLSRouteToTCPConf(ctx context.Context, ep string, listener gatev1.Li
rule, err := hostSNIRule(hostnames)
if err != nil {
// update "ResolvedRefs" status true with "DroppedRoutes" reason
// update "ResolvedRefs" status true with "InvalidHostnames" reason
conditions = append(conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
@ -1111,7 +1161,7 @@ func gatewayTLSRouteToTCPConf(ctx context.Context, ep string, listener gatev1.Li
wrrService, subServices, err := loadTCPServices(client, route.Namespace, routeRule.BackendRefs)
if err != nil {
// update "ResolvedRefs" status true with "DroppedRoutes" reason
// update "ResolvedRefs" status true with "InvalidBackendRefs" reason
conditions = append(conditions, metav1.Condition{
Type: string(gatev1.ListenerConditionResolvedRefs),
Status: metav1.ConditionFalse,
@ -1526,7 +1576,7 @@ func loadServices(client Client, namespace string, backendRefs []gatev1.HTTPBack
continue
}
if *backendRef.Group != "" && *backendRef.Group != "core" && *backendRef.Kind != "Service" {
if *backendRef.Group != "" && *backendRef.Group != groupCore && *backendRef.Kind != "Service" {
return nil, nil, fmt.Errorf("unsupported HTTPBackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name)
}
@ -1649,7 +1699,7 @@ func loadTCPServices(client Client, namespace string, backendRefs []gatev1.Backe
continue
}
if *backendRef.Group != "" && *backendRef.Group != "core" && *backendRef.Kind != "Service" {
if *backendRef.Group != "" && *backendRef.Group != groupCore && *backendRef.Kind != "Service" {
return nil, nil, fmt.Errorf("unsupported BackendRef %s/%s/%s", *backendRef.Group, *backendRef.Kind, backendRef.Name)
}
@ -1880,3 +1930,65 @@ func makeListenerKey(l gatev1.Listener) string {
return fmt.Sprintf("%s|%s|%d", l.Protocol, hostname, l.Port)
}
func filterReferenceGrantsFrom(referenceGrants []*gatev1beta1.ReferenceGrant, group, kind, namespace string) []*gatev1beta1.ReferenceGrant {
var matchingReferenceGrants []*gatev1beta1.ReferenceGrant
for _, referenceGrant := range referenceGrants {
if referenceGrantMatchesFrom(referenceGrant, group, kind, namespace) {
matchingReferenceGrants = append(matchingReferenceGrants, referenceGrant)
}
}
return matchingReferenceGrants
}
func referenceGrantMatchesFrom(referenceGrant *gatev1beta1.ReferenceGrant, group, kind, namespace string) bool {
for _, from := range referenceGrant.Spec.From {
sanitizedGroup := string(from.Group)
if sanitizedGroup == "" {
sanitizedGroup = groupCore
}
if string(from.Namespace) != namespace || string(from.Kind) != kind || sanitizedGroup != group {
continue
}
return true
}
return false
}
func filterReferenceGrantsTo(referenceGrants []*gatev1beta1.ReferenceGrant, group, kind, name string) []*gatev1beta1.ReferenceGrant {
var matchingReferenceGrants []*gatev1beta1.ReferenceGrant
for _, referenceGrant := range referenceGrants {
if referenceGrantMatchesTo(referenceGrant, group, kind, name) {
matchingReferenceGrants = append(matchingReferenceGrants, referenceGrant)
}
}
return matchingReferenceGrants
}
func referenceGrantMatchesTo(referenceGrant *gatev1beta1.ReferenceGrant, group, kind, name string) bool {
for _, to := range referenceGrant.Spec.To {
sanitizedGroup := string(to.Group)
if sanitizedGroup == "" {
sanitizedGroup = groupCore
}
if string(to.Kind) != kind || sanitizedGroup != group || (to.Name != nil && string(*to.Name) != name) {
continue
}
return true
}
return false
}
func groupToString(p *gatev1.Group) string {
if p == nil {
return "<nil>"
}
return string(*p)
}
func kindToString(p *gatev1.Kind) string {
if p == nil {
return "<nil>"
}
return string(*p)
}

View file

@ -15,6 +15,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
gatev1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
)
var _ provider.Provider = (*Provider)(nil)
@ -4628,6 +4629,196 @@ func TestLoadMixedRoutes(t *testing.T) {
}
}
func TestLoadRoutesWithReferenceGrants(t *testing.T) {
testCases := []struct {
desc string
ingressClass string
paths []string
expected *dynamic.Configuration
entryPoints map[string]Entrypoint
}{
{
desc: "Empty",
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Empty because ReferenceGrant for Secret is missing",
paths: []string{"services.yml", "referencegrant/for_secret_missing.yml"},
entryPoints: map[string]Entrypoint{
"tls": {Address: ":9000"},
},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Empty because ReferenceGrant spec.from does not match",
paths: []string{"services.yml", "referencegrant/for_secret_not_matching_from.yml"},
entryPoints: map[string]Entrypoint{
"tls": {Address: ":9000"},
},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Empty because ReferenceGrant spec.to does not match",
paths: []string{"services.yml", "referencegrant/for_secret_not_matching_to.yml"},
entryPoints: map[string]Entrypoint{
"tls": {Address: ":9000"},
},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "For Secret",
paths: []string{"services.yml", "referencegrant/for_secret.yml"},
entryPoints: map[string]Entrypoint{
"tls": {Address: ":9000"},
},
expected: &dynamic.Configuration{
UDP: &dynamic.UDPConfiguration{
Routers: map[string]*dynamic.UDPRouter{},
Services: map[string]*dynamic.UDPService{},
},
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{
"default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb": {
EntryPoints: []string{"tls"},
Service: "default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0",
Rule: "HostSNI(`*`)",
RuleSyntax: "v3",
TLS: &dynamic.RouterTCPTLSConfig{},
},
},
Middlewares: map[string]*dynamic.TCPMiddleware{},
Services: map[string]*dynamic.TCPService{
"default-tcp-app-1-my-gateway-tls-e3b0c44298fc1c149afb-wrr-0": {
Weighted: &dynamic.TCPWeightedRoundRobin{
Services: []dynamic.TCPWRRService{{
Name: "default-whoamitcp-9000",
Weight: func(i int) *int { return &i }(1),
}},
},
},
"default-whoamitcp-9000": {
LoadBalancer: &dynamic.TCPServersLoadBalancer{
Servers: []dynamic.TCPServer{
{
Address: "10.10.0.9:9000",
},
{
Address: "10.10.0.10:9000",
},
},
},
},
},
ServersTransports: map[string]*dynamic.TCPServersTransport{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
ServersTransports: map[string]*dynamic.ServersTransport{},
},
TLS: &dynamic.TLSConfiguration{
Certificates: []*tls.CertAndStores{
{
Certificate: tls.Certificate{
CertFile: types.FileOrContent("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"),
KeyFile: types.FileOrContent("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"),
},
},
},
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
if test.expected == nil {
return
}
p := Provider{EntryPoints: test.entryPoints}
conf := p.loadConfigurationFromGateway(context.Background(), newClientMock(test.paths...))
assert.Equal(t, test.expected, conf)
})
}
}
func Test_hostRule(t *testing.T) {
testCases := []struct {
desc string
@ -5644,3 +5835,247 @@ func kindPtr(kind gatev1.Kind) *gatev1.Kind {
func pathMatchTypePtr(p gatev1.PathMatchType) *gatev1.PathMatchType { return &p }
func headerMatchTypePtr(h gatev1.HeaderMatchType) *gatev1.HeaderMatchType { return &h }
func Test_referenceGrantMatchesFrom(t *testing.T) {
testCases := []struct {
desc string
referenceGrant gatev1beta1.ReferenceGrant
group string
kind string
namespace string
expectedResult bool
}{
{
desc: "matches",
referenceGrant: gatev1beta1.ReferenceGrant{
Spec: gatev1beta1.ReferenceGrantSpec{
From: []gatev1beta1.ReferenceGrantFrom{
{
Group: "correct-group",
Kind: "correct-kind",
Namespace: "correct-namespace",
},
},
},
},
group: "correct-group",
kind: "correct-kind",
namespace: "correct-namespace",
expectedResult: true,
},
{
desc: "empty group matches core",
referenceGrant: gatev1beta1.ReferenceGrant{
Spec: gatev1beta1.ReferenceGrantSpec{
From: []gatev1beta1.ReferenceGrantFrom{
{
Group: "",
Kind: "correct-kind",
Namespace: "correct-namespace",
},
},
},
},
group: "core",
kind: "correct-kind",
namespace: "correct-namespace",
expectedResult: true,
},
{
desc: "wrong group",
referenceGrant: gatev1beta1.ReferenceGrant{
Spec: gatev1beta1.ReferenceGrantSpec{
From: []gatev1beta1.ReferenceGrantFrom{
{
Group: "wrong-group",
Kind: "correct-kind",
Namespace: "correct-namespace",
},
},
},
},
group: "correct-group",
kind: "correct-kind",
namespace: "correct-namespace",
expectedResult: false,
},
{
desc: "wrong kind",
referenceGrant: gatev1beta1.ReferenceGrant{
Spec: gatev1beta1.ReferenceGrantSpec{
From: []gatev1beta1.ReferenceGrantFrom{
{
Group: "correct-group",
Kind: "wrong-kind",
Namespace: "correct-namespace",
},
},
},
},
group: "correct-group",
kind: "correct-kind",
namespace: "correct-namespace",
expectedResult: false,
},
{
desc: "wrong namespace",
referenceGrant: gatev1beta1.ReferenceGrant{
Spec: gatev1beta1.ReferenceGrantSpec{
From: []gatev1beta1.ReferenceGrantFrom{
{
Group: "correct-group",
Kind: "correct-kind",
Namespace: "wrong-namespace",
},
},
},
},
group: "correct-group",
kind: "correct-kind",
namespace: "correct-namespace",
expectedResult: false,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
assert.Equal(t, test.expectedResult, referenceGrantMatchesFrom(&test.referenceGrant, test.group, test.kind, test.namespace))
})
}
}
func Test_referenceGrantMatchesTo(t *testing.T) {
testCases := []struct {
desc string
referenceGrant gatev1beta1.ReferenceGrant
group string
kind string
name string
expectedResult bool
}{
{
desc: "matches",
referenceGrant: gatev1beta1.ReferenceGrant{
Spec: gatev1beta1.ReferenceGrantSpec{
To: []gatev1beta1.ReferenceGrantTo{
{
Group: "correct-group",
Kind: "correct-kind",
Name: objectNamePtr("correct-name"),
},
},
},
},
group: "correct-group",
kind: "correct-kind",
name: "correct-name",
expectedResult: true,
},
{
desc: "matches without name",
referenceGrant: gatev1beta1.ReferenceGrant{
Spec: gatev1beta1.ReferenceGrantSpec{
To: []gatev1beta1.ReferenceGrantTo{
{
Group: "correct-group",
Kind: "correct-kind",
Name: nil,
},
},
},
},
group: "correct-group",
kind: "correct-kind",
name: "some-name",
expectedResult: true,
},
{
desc: "empty group matches core",
referenceGrant: gatev1beta1.ReferenceGrant{
Spec: gatev1beta1.ReferenceGrantSpec{
To: []gatev1beta1.ReferenceGrantTo{
{
Group: "",
Kind: "correct-kind",
Name: objectNamePtr("correct-name"),
},
},
},
},
group: "core",
kind: "correct-kind",
name: "correct-name",
expectedResult: true,
},
{
desc: "wrong group",
referenceGrant: gatev1beta1.ReferenceGrant{
Spec: gatev1beta1.ReferenceGrantSpec{
To: []gatev1beta1.ReferenceGrantTo{
{
Group: "wrong-group",
Kind: "correct-kind",
Name: objectNamePtr("correct-name"),
},
},
},
},
group: "correct-group",
kind: "correct-kind",
name: "correct-namespace",
expectedResult: false,
},
{
desc: "wrong kind",
referenceGrant: gatev1beta1.ReferenceGrant{
Spec: gatev1beta1.ReferenceGrantSpec{
To: []gatev1beta1.ReferenceGrantTo{
{
Group: "correct-group",
Kind: "wrong-kind",
Name: objectNamePtr("correct-name"),
},
},
},
},
group: "correct-group",
kind: "correct-kind",
name: "correct-name",
expectedResult: false,
},
{
desc: "wrong name",
referenceGrant: gatev1beta1.ReferenceGrant{
Spec: gatev1beta1.ReferenceGrantSpec{
To: []gatev1beta1.ReferenceGrantTo{
{
Group: "correct-group",
Kind: "correct-kind",
Name: objectNamePtr("wrong-name"),
},
},
},
},
group: "correct-group",
kind: "correct-kind",
name: "correct-name",
expectedResult: false,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
assert.Equal(t, test.expectedResult, referenceGrantMatchesTo(&test.referenceGrant, test.group, test.kind, test.name))
})
}
}
func objectNamePtr(objectName gatev1.ObjectName) *gatev1.ObjectName {
return &objectName
}

View file

@ -12,7 +12,7 @@ import (
// MustParseYaml parses a YAML to objects.
func MustParseYaml(content []byte) []runtime.Object {
acceptedK8sTypes := regexp.MustCompile(`^(Namespace|Deployment|Endpoints|Service|Ingress|IngressRoute|IngressRouteTCP|IngressRouteUDP|Middleware|MiddlewareTCP|Secret|TLSOption|TLSStore|TraefikService|IngressClass|ServersTransport|ServersTransportTCP|GatewayClass|Gateway|HTTPRoute|TCPRoute|TLSRoute)$`)
acceptedK8sTypes := regexp.MustCompile(`^(Namespace|Deployment|Endpoints|Service|Ingress|IngressRoute|IngressRouteTCP|IngressRouteUDP|Middleware|MiddlewareTCP|Secret|TLSOption|TLSStore|TraefikService|IngressClass|ServersTransport|ServersTransportTCP|GatewayClass|Gateway|HTTPRoute|TCPRoute|TLSRoute|ReferenceGrant)$`)
files := strings.Split(string(content), "---\n")
retVal := make([]runtime.Object, 0, len(files))