Skip to content

Commit 7f10a8a

Browse files
authored
Merge pull request #4669 from shuqz/i2g-annotation
[feat i2g]support ssl-redirect translate
2 parents 01c5abc + 7af53db commit 7f10a8a

File tree

11 files changed

+272
-33
lines changed

11 files changed

+272
-33
lines changed

pkg/ingress2gateway/translate/annotation_coverage_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ func TestAllIngressAnnotationsCovered(t *testing.T) {
7777
},
7878
HTTPRouteConfig: {
7979
annotations.IngressSuffixUseRegexPathMatch,
80+
annotations.IngressSuffixSSLRedirect,
8081
},
8182
}
8283

@@ -87,15 +88,13 @@ func TestAllIngressAnnotationsCovered(t *testing.T) {
8788
annotations.IngressSuffixGroupOrder,
8889
},
8990
Routing: {
90-
annotations.IngressSuffixSSLRedirect,
9191
// NOTE: "use-annotation" action backends (alb.ingress.kubernetes.io/actions.{name}),
9292
// condition annotations (alb.ingress.kubernetes.io/conditions.{name}), and
9393
// transform annotations (alb.ingress.kubernetes.io/transforms.{name}) are dynamically
9494
// named and cannot be tracked as static suffixes here.
9595
// actions.* translation is implemented in translate_action_helper.go (forward, redirect, fixed-response).
9696
// conditions.* translation is implemented in translate_condition_helper.go.
9797
// transforms.* translation is implemented in translate_transform_helper.go.
98-
// ssl-redirect is planned for a subsequent PR.
9998
},
10099
Authentication: {
101100
annotations.IngressSuffixAuthType,

pkg/ingress2gateway/translate/gateway.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package translate
22

33
import (
4+
"strings"
5+
46
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
57
gatewayv1beta1 "sigs.k8s.io/aws-load-balancer-controller/apis/gateway/v1beta1"
68
gwconstants "sigs.k8s.io/aws-load-balancer-controller/pkg/gateway/constants"
@@ -30,11 +32,22 @@ func buildGatewayClass() gwv1.GatewayClass {
3032
func buildGateway(name, namespace string, lbConfig *gatewayv1beta1.LoadBalancerConfiguration, listenPorts []listenPortEntry) gwv1.Gateway {
3133
var listeners []gwv1.Listener
3234
for _, lp := range listenPorts {
33-
listeners = append(listeners, gwv1.Listener{
34-
Name: gwv1.SectionName(utils.GetSectionName(lp.Protocol, lp.Port)),
35+
listener := gwv1.Listener{
36+
Name: gwv1.SectionName(utils.GenerateSectionName(lp.Protocol, lp.Port)),
3537
Port: gwv1.PortNumber(lp.Port),
3638
Protocol: toALBProtocol(lp.Protocol),
37-
})
39+
}
40+
// Gateway API requires a tls section for HTTPS listeners.
41+
// The actual certificate is configured via LoadBalancerConfiguration,
42+
// but the webhook rejects HTTPS listeners without tls set.
43+
if strings.EqualFold(lp.Protocol, utils.ProtocolHTTPS) {
44+
listener.TLS = &gwv1.ListenerTLSConfig{
45+
CertificateRefs: []gwv1.SecretObjectReference{
46+
{Name: utils.PlaceholderTLSSecretName},
47+
},
48+
}
49+
}
50+
listeners = append(listeners, listener)
3851
}
3952

4053
gw := gwv1.Gateway{

pkg/ingress2gateway/translate/gateway_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ func TestBuildGateway(t *testing.T) {
6262
}
6363

6464
func TestListenerName(t *testing.T) {
65-
assert.Equal(t, "http-80", utils.GetSectionName("HTTP", 80))
66-
assert.Equal(t, "https-443", utils.GetSectionName("HTTPS", 443))
65+
assert.Equal(t, "http-80", utils.GenerateSectionName("HTTP", 80))
66+
assert.Equal(t, "https-443", utils.GenerateSectionName("HTTPS", 443))
6767
}
6868

6969
func TestToALBProtocol(t *testing.T) {

pkg/ingress2gateway/translate/httproute.go

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,17 @@ func (t *httpRouteTranslator) trackBackend(svcName string, resolvedPort int32) {
4747
}
4848

4949
// buildHTTPRoutes builds one or more HTTPRoutes from an Ingress resource and collects
50-
// unique service references (with resolved ports) encountered during the iteration.
51-
func buildHTTPRoutes(ing networking.Ingress, namespace, gatewayName string, listenPorts []listenPortEntry, servicesByKey map[string]corev1.Service) ([]gwv1.HTTPRoute, []serviceRef, []gatewayv1beta1.ListenerRuleConfiguration, error) {
52-
parentRefs := buildParentRefs(gatewayName)
50+
func buildHTTPRoutes(ing networking.Ingress, namespace, gatewayName string, listenPorts []listenPortEntry, servicesByKey map[string]corev1.Service, sslRedirectPort *int32) ([]gwv1.HTTPRoute, []serviceRef, []gatewayv1beta1.ListenerRuleConfiguration, error) {
51+
// Determine parentRefs based on ssl-redirect.
52+
// When ssl-redirect is set, the rules route attaches only to the HTTPS listener.
53+
// When not set, the route attaches to all listeners (no sectionName).
54+
var parentRefs []gwv1.ParentReference
55+
if sslRedirectPort != nil {
56+
sectionName := utils.GenerateSectionName(utils.ProtocolHTTPS, *sslRedirectPort)
57+
parentRefs = buildParentRefs(gatewayName, &sectionName)
58+
} else {
59+
parentRefs = buildParentRefs(gatewayName, nil)
60+
}
5361

5462
t := &httpRouteTranslator{
5563
namespace: namespace,
@@ -98,18 +106,35 @@ func buildHTTPRoutes(ing networking.Ingress, namespace, gatewayName string, list
98106
return nil, nil, nil, err
99107
}
100108

109+
// When ssl-redirect is set, generate a redirect route for each HTTP listener.
110+
if sslRedirectPort != nil {
111+
for _, lp := range listenPorts {
112+
if strings.EqualFold(lp.Protocol, utils.ProtocolHTTP) {
113+
redirectRoute := buildSSLRedirectRoute(
114+
namespace, ing.Name, gatewayName,
115+
utils.GenerateSectionName(lp.Protocol, lp.Port),
116+
*sslRedirectPort,
117+
)
118+
routes = append(routes, redirectRoute)
119+
}
120+
}
121+
}
122+
101123
return routes, t.svcRefs, t.listenerRuleConfigs, nil
102124
}
103125

104-
// buildParentRefs creates a single ParentReference to the gateway.
105-
// No sectionName is needed because Ingress applies all rules to all listen-ports,
106-
// and a parentRef without sectionName attaches the route to all listeners on the gateway.
107-
func buildParentRefs(gatewayName string) []gwv1.ParentReference {
108-
return []gwv1.ParentReference{
109-
{
110-
Name: gwv1.ObjectName(gatewayName),
111-
},
126+
// buildParentRefs creates a ParentReference to the gateway.
127+
// When sectionName is nil, the route attaches to all listeners.
128+
// When sectionName is set, the route attaches only to that specific listener.
129+
func buildParentRefs(gatewayName string, sectionName *string) []gwv1.ParentReference {
130+
ref := gwv1.ParentReference{
131+
Name: gwv1.ObjectName(gatewayName),
132+
}
133+
if sectionName != nil {
134+
sn := gwv1.SectionName(*sectionName)
135+
ref.SectionName = &sn
112136
}
137+
return []gwv1.ParentReference{ref}
113138
}
114139

115140
// buildRouteRule builds a single HTTPRouteRule from an Ingress path, including backend resolution,
@@ -339,6 +364,29 @@ func assembleRoutes(namespace, ingName string, parentRefs []gwv1.ParentReference
339364
return routes, nil
340365
}
341366

367+
// buildSSLRedirectRoute creates an HTTPRoute that redirects all HTTP traffic to HTTPS.
368+
// It attaches only to the specified HTTP listener via sectionName.
369+
func buildSSLRedirectRoute(namespace, ingName, gatewayName, httpSectionName string, sslPort int32) gwv1.HTTPRoute {
370+
parentRefs := buildParentRefs(gatewayName, &httpSectionName)
371+
httpsScheme := "https"
372+
port := gwv1.PortNumber(sslPort)
373+
statusCode := 301
374+
return newHTTPRoute(
375+
utils.GetRedirectHTTPRouteName(namespace, ingName),
376+
namespace, parentRefs, nil,
377+
[]gwv1.HTTPRouteRule{{
378+
Filters: []gwv1.HTTPRouteFilter{{
379+
Type: gwv1.HTTPRouteFilterRequestRedirect,
380+
RequestRedirect: &gwv1.HTTPRequestRedirectFilter{
381+
Scheme: &httpsScheme,
382+
Port: &port,
383+
StatusCode: &statusCode,
384+
},
385+
}},
386+
}},
387+
)
388+
}
389+
342390
// resolveServicePort resolves a ServiceBackendPort to a numeric port.
343391
func resolveServicePort(sbp networking.ServiceBackendPort, namespace, svcName string, servicesByKey map[string]corev1.Service) (int32, error) {
344392
if sbp.Number != 0 {

pkg/ingress2gateway/translate/httproute_test.go

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ func TestBuildHTTPRoutes(t *testing.T) {
1616
pathExact := networking.PathTypeExact
1717

1818
tests := []struct {
19-
name string
20-
ing networking.Ingress
21-
namespace string
22-
gwName string
23-
ports []listenPortEntry
24-
wantRoutes int
25-
check func(t *testing.T, routes []gwv1.HTTPRoute)
19+
name string
20+
ing networking.Ingress
21+
namespace string
22+
gwName string
23+
ports []listenPortEntry
24+
sslRedirectPort *int32
25+
wantRoutes int
26+
check func(t *testing.T, routes []gwv1.HTTPRoute)
2627
}{
2728
{
2829
name: "single rule with host and prefix path",
@@ -439,11 +440,65 @@ func TestBuildHTTPRoutes(t *testing.T) {
439440
assert.Equal(t, "/src", *r.Spec.Rules[0].Matches[0].Path.Value)
440441
},
441442
},
443+
{
444+
name: "ssl-redirect produces redirect route on HTTP and rules route on HTTPS",
445+
ing: networking.Ingress{
446+
ObjectMeta: metav1.ObjectMeta{
447+
Name: "ssl",
448+
Annotations: map[string]string{
449+
"alb.ingress.kubernetes.io/ssl-redirect": "443",
450+
},
451+
},
452+
Spec: networking.IngressSpec{
453+
Rules: []networking.IngressRule{{
454+
Host: "app.example.com",
455+
IngressRuleValue: networking.IngressRuleValue{
456+
HTTP: &networking.HTTPIngressRuleValue{
457+
Paths: []networking.HTTPIngressPath{{
458+
Path: "/api", PathType: &pathPrefix,
459+
Backend: networking.IngressBackend{
460+
Service: &networking.IngressServiceBackend{
461+
Name: "api-svc", Port: networking.ServiceBackendPort{Number: 80},
462+
},
463+
},
464+
}},
465+
},
466+
},
467+
}},
468+
},
469+
},
470+
namespace: "default", gwName: "gw",
471+
ports: []listenPortEntry{
472+
{Protocol: "HTTP", Port: 80},
473+
{Protocol: "HTTPS", Port: 443},
474+
},
475+
sslRedirectPort: int32Ptr(443),
476+
wantRoutes: 2, // rules route + redirect route
477+
check: func(t *testing.T, routes []gwv1.HTTPRoute) {
478+
// First route: rules, attached to HTTPS listener only
479+
rulesRoute := routes[0]
480+
require.NotNil(t, rulesRoute.Spec.ParentRefs[0].SectionName)
481+
assert.Equal(t, gwv1.SectionName("https-443"), *rulesRoute.Spec.ParentRefs[0].SectionName)
482+
assert.Len(t, rulesRoute.Spec.Rules, 1)
483+
assert.Equal(t, "/api", *rulesRoute.Spec.Rules[0].Matches[0].Path.Value)
484+
485+
// Second route: redirect, attached to HTTP listener only
486+
redirectRoute := routes[1]
487+
require.NotNil(t, redirectRoute.Spec.ParentRefs[0].SectionName)
488+
assert.Equal(t, gwv1.SectionName("http-80"), *redirectRoute.Spec.ParentRefs[0].SectionName)
489+
require.Len(t, redirectRoute.Spec.Rules, 1)
490+
require.Len(t, redirectRoute.Spec.Rules[0].Filters, 1)
491+
assert.Equal(t, gwv1.HTTPRouteFilterRequestRedirect, redirectRoute.Spec.Rules[0].Filters[0].Type)
492+
assert.Equal(t, "https", *redirectRoute.Spec.Rules[0].Filters[0].RequestRedirect.Scheme)
493+
assert.Equal(t, gwv1.PortNumber(443), *redirectRoute.Spec.Rules[0].Filters[0].RequestRedirect.Port)
494+
assert.Equal(t, 301, *redirectRoute.Spec.Rules[0].Filters[0].RequestRedirect.StatusCode)
495+
},
496+
},
442497
}
443498

444499
for _, tt := range tests {
445500
t.Run(tt.name, func(t *testing.T) {
446-
routes, _, _, err := buildHTTPRoutes(tt.ing, tt.namespace, tt.gwName, tt.ports, nil)
501+
routes, _, _, err := buildHTTPRoutes(tt.ing, tt.namespace, tt.gwName, tt.ports, nil, tt.sslRedirectPort)
447502
require.NoError(t, err)
448503
require.Len(t, routes, tt.wantRoutes)
449504
if tt.check != nil {
@@ -525,3 +580,39 @@ func TestResolveServicePort(t *testing.T) {
525580
_, err = resolveServicePort(networking.ServiceBackendPort{}, "default", "my-svc", svcMap)
526581
assert.Error(t, err)
527582
}
583+
584+
func int32Ptr(v int32) *int32 { return &v }
585+
586+
func TestBuildHTTPRoutes_DefaultBackendError(t *testing.T) {
587+
pathPrefix := networking.PathTypePrefix
588+
ing := networking.Ingress{
589+
ObjectMeta: metav1.ObjectMeta{Name: "app"},
590+
Spec: networking.IngressSpec{
591+
DefaultBackend: &networking.IngressBackend{
592+
Service: &networking.IngressServiceBackend{
593+
Name: "missing-svc",
594+
Port: networking.ServiceBackendPort{Name: "http"},
595+
},
596+
},
597+
Rules: []networking.IngressRule{{
598+
IngressRuleValue: networking.IngressRuleValue{
599+
HTTP: &networking.HTTPIngressRuleValue{
600+
Paths: []networking.HTTPIngressPath{{
601+
Path: "/", PathType: &pathPrefix,
602+
Backend: networking.IngressBackend{
603+
Service: &networking.IngressServiceBackend{
604+
Name: "svc", Port: networking.ServiceBackendPort{Number: 80},
605+
},
606+
},
607+
}},
608+
},
609+
},
610+
}},
611+
},
612+
}
613+
// No services provided — named port resolution for defaultBackend will fail
614+
_, _, _, err := buildHTTPRoutes(ing, "default", "gw",
615+
[]listenPortEntry{{Protocol: "HTTP", Port: 80}}, nil, nil)
616+
assert.Error(t, err)
617+
assert.Contains(t, err.Error(), "defaultBackend")
618+
}

pkg/ingress2gateway/translate/icp_overrides.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package translate
22

33
import (
44
"fmt"
5+
"strconv"
56
"strings"
67

78
elbv2api "sigs.k8s.io/aws-load-balancer-controller/apis/elbv2/v1beta1"
89
gatewayv1beta1 "sigs.k8s.io/aws-load-balancer-controller/apis/gateway/v1beta1"
10+
annotations "sigs.k8s.io/aws-load-balancer-controller/pkg/annotations"
911
"sigs.k8s.io/aws-load-balancer-controller/pkg/ingress2gateway/utils"
1012
)
1113

@@ -14,9 +16,8 @@ import (
1416
// Fields intentionally not mapped to LB config:
1517
// - NamespaceSelector: cluster policy, not a LB setting
1618
// - TargetType: TG-level, handled in applyIngressClassParamsToTGProps
17-
// TO-DO
18-
// - Group: handled at Ingress grouping level task
19-
// - SSLRedirectPort: handled at routing level task
19+
// - Group: handled at Ingress grouping level task (TO-DO)
20+
// - SSLRedirectPort: handled in buildHTTPRoutes via resolveSSLRedirectPort
2021
func applyIngressClassParamsToLBConfig(spec *gatewayv1beta1.LoadBalancerConfigurationSpec, icp *elbv2api.IngressClassParams) {
2122
if icp == nil {
2223
return
@@ -167,3 +168,16 @@ func applyIngressClassParamsToTGProps(props *gatewayv1beta1.TargetGroupProps, ic
167168
props.TargetType = &tt
168169
}
169170
}
171+
172+
// resolveSSLRedirectPort returns the SSL redirect port if configured.
173+
// IngressClassParams.SSLRedirectPort takes priority over the Ingress annotation.
174+
func resolveSSLRedirectPort(ingAnnotations map[string]string, icp *elbv2api.IngressClassParams) *int32 {
175+
if icp != nil && icp.Spec.SSLRedirectPort != "" {
176+
port, err := strconv.ParseInt(icp.Spec.SSLRedirectPort, 10, 32)
177+
if err == nil {
178+
p := int32(port)
179+
return &p
180+
}
181+
}
182+
return getInt32(ingAnnotations, annotations.IngressSuffixSSLRedirect)
183+
}

0 commit comments

Comments
 (0)