Skip to content

Commit 4d689ad

Browse files
committed
add httproutes
Change-Id: I25ceffb1871e0ca73106236fa0eb4770228f949d
1 parent b67e2e9 commit 4d689ad

File tree

8 files changed

+704
-25
lines changed

8 files changed

+704
-25
lines changed

pkg/controller/controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ func startCloudControllerManager(ctx context.Context, clusterName string, config
280280
clusterName,
281281
gwClient,
282282
sharedInformers.Core().V1().Namespaces(),
283+
sharedInformers.Core().V1().Services(),
283284
sharedGwInformers.Gateway().V1().Gateways(),
284285
sharedGwInformers.Gateway().V1().HTTPRoutes(),
285286
sharedGwInformers.Gateway().V1().GRPCRoutes(),

pkg/gateway/backendref.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package gateway

pkg/gateway/controller.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ type Controller struct {
6464
namespaceLister corev1listers.NamespaceLister
6565
namespaceListerSynced cache.InformerSynced
6666

67+
serviceLister corev1listers.ServiceLister
68+
serviceListerSynced cache.InformerSynced
69+
6770
gatewayLister gatewaylisters.GatewayLister
6871
gatewayListerSynced cache.InformerSynced
6972
gatewayqueue workqueue.TypedRateLimitingInterface[string]
@@ -88,6 +91,7 @@ func New(
8891
clusterName string,
8992
gwClient *gatewayclient.Clientset,
9093
namespaceInformer corev1informers.NamespaceInformer,
94+
serviceInformer corev1informers.ServiceInformer,
9195
gatewayInformer gatewayinformers.GatewayInformer,
9296
httprouteInformer gatewayinformers.HTTPRouteInformer,
9397
grpcrouteInformer gatewayinformers.GRPCRouteInformer,
@@ -96,6 +100,8 @@ func New(
96100
clusterName: clusterName,
97101
namespaceLister: namespaceInformer.Lister(),
98102
namespaceListerSynced: namespaceInformer.Informer().HasSynced,
103+
serviceLister: serviceInformer.Lister(),
104+
serviceListerSynced: serviceInformer.Informer().HasSynced,
99105
gwClient: gwClient,
100106
gatewayLister: gatewayInformer.Lister(),
101107
gatewayListerSynced: gatewayInformer.Informer().HasSynced,
@@ -326,7 +332,13 @@ func (c *Controller) Run(ctx context.Context) error {
326332
klog.Info("Starting Gateway API controller")
327333

328334
// Wait for all involved caches to be synced, before processing items from the queue is started
329-
if !cache.WaitForNamedCacheSync(controllerName, ctx.Done(), c.gatewayListerSynced, c.httprouteListerSynced, c.grpcrouteListerSynced, c.namespaceListerSynced) {
335+
if !cache.WaitForNamedCacheSync(controllerName, ctx.Done(),
336+
c.gatewayListerSynced,
337+
c.httprouteListerSynced,
338+
c.grpcrouteListerSynced,
339+
c.namespaceListerSynced,
340+
c.serviceListerSynced,
341+
) {
330342
return fmt.Errorf("timed out waiting for caches to sync")
331343
}
332344

pkg/gateway/gateway.go

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"sigs.k8s.io/cloud-provider-kind/pkg/config"
2323
"sigs.k8s.io/cloud-provider-kind/pkg/container"
2424

25+
"strings"
26+
2527
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
2628
)
2729

@@ -117,33 +119,20 @@ func (c *Controller) syncGateway(key string) error {
117119
for _, route := range c.getHTTPRoutesForListener(gw, listener) {
118120
klog.V(2).Infof("Processing http route %s/%s for gw %s/%s", route.Namespace, route.Name, gw.Namespace, gw.Name)
119121
// Check hostnames between listener and route
120-
for _, hostname := range route.Spec.Hostnames {
121-
if hostname != *listener.Hostname {
122-
123-
}
122+
if !hostnamesIntersect(listener.Hostname, route.Spec.Hostnames) {
123+
klog.V(4).Infof("HTTPRoute %s/%s hostnames do not intersect with Gateway %s/%s Listener %s hostname, skipping", route.Namespace, route.Name, gw.Namespace, gw.Name, listener.Name)
124+
continue // Skip this route for this listener if hostnames don't intersect
124125
}
125-
// Process rules
126-
for _, rule := range route.Spec.Rules {
127-
for _, match := range rule.Matches {
128-
klog.Infof("match %#v", match)
129-
}
130-
for _, filter := range rule.Filters {
131-
klog.Infof("match %#v", filter)
132-
}
133-
for _, backend := range rule.BackendRefs {
134-
klog.Infof("match %#v", backend)
135-
for _, filter := range backend.Filters {
136-
klog.Infof("match %#v", filter)
137-
}
138-
}
139-
140-
// TODO Timeouts
141-
// TODO Retry
142-
// TODO SessionPersistence
126+
127+
routeEnvoyResources, err := translateHTTPRouteToEnvoyResources(c.serviceLister, route)
128+
if err != nil {
129+
klog.Errorf("Error translating HTTPRoute %s/%s: %v", route.Namespace, route.Name, err)
130+
continue // Skip this route if translation fails
143131
}
144132

145-
resources[resourcev3.RouteType] = append(resources[resourcev3.RouteType], &routev3.Route{})
146-
resources[resourcev3.ClusterType] = append(resources[resourcev3.ClusterType], &clusterv3.Cluster{})
133+
// Merge the translated resources into the main map
134+
mergeEnvoyResources(resources, routeEnvoyResources)
135+
147136
}
148137

149138
for _, route := range c.getGRPCRoutesForListener(gw, listener) {
@@ -277,3 +266,77 @@ func (c *Controller) getGRPCRoutesForListener(gw *gatewayv1.Gateway, listener ga
277266

278267
return matchingRoutes
279268
}
269+
270+
// mergeEnvoyResources merges resources generated for a specific route (source)
271+
// into the main resource map (target).
272+
func mergeEnvoyResources(target map[resourcev3.Type][]envoyproxytypes.Resource, source map[resourcev3.Type][]interface{}) {
273+
for resType, resList := range source {
274+
if _, ok := target[resType]; !ok {
275+
target[resType] = []envoyproxytypes.Resource{}
276+
}
277+
// Convert []interface{} to []envoyproxytypes.Resource
278+
for _, res := range resList {
279+
if envoyRes, ok := res.(envoyproxytypes.Resource); ok {
280+
target[resType] = append(target[resType], envoyRes)
281+
} else {
282+
klog.Warningf("Translated resource is not of type envoyproxytypes.Resource: %T", res)
283+
}
284+
}
285+
}
286+
}
287+
288+
// hostnameMatches checks if a route hostname matches a listener hostname according to Gateway API rules.
289+
func hostnameMatches(listenerHostname, routeHostname gatewayv1.Hostname) bool {
290+
lh := string(listenerHostname)
291+
rh := string(routeHostname)
292+
293+
// Exact match
294+
if lh == rh {
295+
return true
296+
}
297+
298+
// Wildcard listener
299+
if strings.HasPrefix(lh, "*.") {
300+
listenerDomain := lh[1:] // .example.com
301+
if strings.HasPrefix(rh, "*.") {
302+
// Both are wildcards, check if domains suffix match each other
303+
routeDomain := rh[1:]
304+
return strings.HasSuffix(listenerDomain, routeDomain) || strings.HasSuffix(routeDomain, listenerDomain)
305+
}
306+
// Route is not wildcard, check if it's a suffix and not the domain itself (e.g., *.com does not match com)
307+
return strings.HasSuffix(rh, listenerDomain) && rh != listenerDomain[1:]
308+
}
309+
310+
// Wildcard route
311+
if strings.HasPrefix(rh, "*.") {
312+
// Listener is not wildcard, check if it's a suffix and not the domain itself
313+
routeDomain := rh[1:] // .example.com
314+
return strings.HasSuffix(lh, routeDomain) && lh != routeDomain[1:]
315+
}
316+
317+
// No wildcards involved, and not an exact match
318+
return false
319+
}
320+
321+
// hostnamesIntersect checks if any route hostname intersects with the listener hostname.
322+
func hostnamesIntersect(listenerHostname *gatewayv1.Hostname, routeHostnames []gatewayv1.Hostname) bool {
323+
// If the route specifies no hostnames, it implicitly matches any listener hostname.
324+
if len(routeHostnames) == 0 {
325+
return true
326+
}
327+
328+
// If the listener specifies no hostname, it implicitly matches any route hostname.
329+
if listenerHostname == nil || *listenerHostname == "" {
330+
return true
331+
}
332+
333+
lh := *listenerHostname
334+
for _, rh := range routeHostnames {
335+
if hostnameMatches(lh, rh) {
336+
return true // Found an intersection
337+
}
338+
}
339+
340+
// No intersection found
341+
return false
342+
}

pkg/gateway/gateway_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package gateway
2+
3+
import (
4+
"testing"
5+
6+
"k8s.io/utils/ptr"
7+
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
8+
)
9+
10+
func TestHostnameMatches(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
listenerHostname gatewayv1.Hostname
14+
routeHostname gatewayv1.Hostname
15+
want bool
16+
}{
17+
{"exact match", "foo.example.com", "foo.example.com", true},
18+
{"listener wildcard, route exact match", "*.example.com", "foo.example.com", true},
19+
{"listener wildcard, route exact non-match (diff domain)", "*.example.com", "foo.example.net", false},
20+
{"listener wildcard, route exact non-match (domain itself)", "*.example.com", "example.com", false},
21+
{"listener wildcard, route wildcard match (subdomain)", "*.foo.example.com", "*.example.com", true},
22+
{"listener wildcard, route wildcard match (same)", "*.example.com", "*.example.com", true},
23+
{"listener wildcard, route wildcard non-match", "*.example.com", "*.example.net", false},
24+
{"listener exact, route wildcard match", "foo.example.com", "*.example.com", true},
25+
{"listener exact, route wildcard non-match (diff domain)", "foo.example.net", "*.example.com", false},
26+
{"listener exact, route wildcard non-match (domain itself)", "example.com", "*.example.com", false},
27+
{"no wildcard, non-match", "foo.example.com", "bar.example.com", false},
28+
{"no wildcard, non-match diff domain", "foo.example.com", "foo.example.net", false},
29+
{"empty listener", "", "foo.example.com", false}, // Exact match fails
30+
{"empty route", "foo.example.com", "", false}, // Exact match fails
31+
{"both empty", "", "", true}, // Exact match
32+
{"listener wildcard, route empty", "*.example.com", "", false},
33+
{"listener empty, route wildcard", "", "*.example.com", false},
34+
{"complex wildcard listener match", "*.apps.example.com", "foo.bar.apps.example.com", true},
35+
{"complex wildcard route match", "foo.bar.apps.example.com", "*.apps.example.com", true},
36+
{"wildcard vs non-wildcard TLD", "*.com", "example.com", true}, // *.com matches example.com
37+
{"non-wildcard TLD vs wildcard", "example.com", "*.com", true}, // *.com matches example.com
38+
{"wildcard TLD vs TLD", "*.com", "com", false}, // *.com does not match com
39+
{"TLD vs wildcard TLD", "com", "*.com", false}, // *.com does not match com
40+
}
41+
42+
for _, tt := range tests {
43+
t.Run(tt.name, func(t *testing.T) {
44+
if got := hostnameMatches(tt.listenerHostname, tt.routeHostname); got != tt.want {
45+
t.Errorf("hostnameMatches(%q, %q) = %v, want %v", tt.listenerHostname, tt.routeHostname, got, tt.want)
46+
}
47+
})
48+
}
49+
}
50+
51+
func TestHostnamesIntersect(t *testing.T) {
52+
tests := []struct {
53+
name string
54+
listenerHostname *gatewayv1.Hostname
55+
routeHostnames []gatewayv1.Hostname
56+
want bool
57+
}{
58+
{
59+
name: "empty route hostnames",
60+
listenerHostname: ptr.To(gatewayv1.Hostname("foo.example.com")),
61+
routeHostnames: []gatewayv1.Hostname{},
62+
want: true,
63+
},
64+
{
65+
name: "nil listener hostname",
66+
listenerHostname: nil,
67+
routeHostnames: []gatewayv1.Hostname{"foo.example.com"},
68+
want: true,
69+
},
70+
{
71+
name: "empty listener hostname",
72+
listenerHostname: ptr.To(gatewayv1.Hostname("")),
73+
routeHostnames: []gatewayv1.Hostname{"foo.example.com"},
74+
want: true,
75+
},
76+
{
77+
name: "exact match intersection",
78+
listenerHostname: ptr.To(gatewayv1.Hostname("foo.example.com")),
79+
routeHostnames: []gatewayv1.Hostname{"bar.example.com", "foo.example.com"},
80+
want: true,
81+
},
82+
{
83+
name: "wildcard listener intersection",
84+
listenerHostname: ptr.To(gatewayv1.Hostname("*.example.com")),
85+
routeHostnames: []gatewayv1.Hostname{"bar.example.net", "foo.example.com"},
86+
want: true,
87+
},
88+
{
89+
name: "wildcard route intersection",
90+
listenerHostname: ptr.To(gatewayv1.Hostname("foo.example.com")),
91+
routeHostnames: []gatewayv1.Hostname{"bar.example.net", "*.example.com"},
92+
want: true,
93+
},
94+
{
95+
name: "wildcard both intersection",
96+
listenerHostname: ptr.To(gatewayv1.Hostname("*.example.com")),
97+
routeHostnames: []gatewayv1.Hostname{"bar.example.net", "*.apps.example.com"},
98+
want: true,
99+
},
100+
{
101+
name: "no intersection",
102+
listenerHostname: ptr.To(gatewayv1.Hostname("foo.example.com")),
103+
routeHostnames: []gatewayv1.Hostname{"bar.example.com", "baz.example.com"},
104+
want: false,
105+
},
106+
{
107+
name: "no intersection with wildcards",
108+
listenerHostname: ptr.To(gatewayv1.Hostname("*.example.net")),
109+
routeHostnames: []gatewayv1.Hostname{"bar.example.com", "*.apps.example.com"},
110+
want: false,
111+
},
112+
{
113+
name: "listener wildcard, route domain itself (no intersection)",
114+
listenerHostname: ptr.To(gatewayv1.Hostname("*.example.com")),
115+
routeHostnames: []gatewayv1.Hostname{"example.com", "foo.example.net"},
116+
want: false,
117+
},
118+
{
119+
name: "listener exact, route wildcard domain itself (no intersection)",
120+
listenerHostname: ptr.To(gatewayv1.Hostname("example.com")),
121+
routeHostnames: []gatewayv1.Hostname{"*.example.com", "foo.example.net"},
122+
want: false,
123+
},
124+
{
125+
name: "both nil/empty",
126+
listenerHostname: nil,
127+
routeHostnames: []gatewayv1.Hostname{},
128+
want: true,
129+
},
130+
}
131+
132+
for _, tt := range tests {
133+
t.Run(tt.name, func(t *testing.T) {
134+
if got := hostnamesIntersect(tt.listenerHostname, tt.routeHostnames); got != tt.want {
135+
t.Errorf("hostnamesIntersect() = %v, want %v", got, tt.want)
136+
}
137+
})
138+
}
139+
}

0 commit comments

Comments
 (0)