Skip to content

Commit eea2a72

Browse files
authored
add support for TLS route (#2211)
Problem: TLSRoute was not supported by NGF. Solution: Watched for changes to TLSRoutes, added validation and tests, added TLSRoute to graph, and converted Kubernetes TLSRoute spec to nginx config.
1 parent 6756a2f commit eea2a72

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+3785
-397
lines changed

.github/workflows/conformance.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ jobs:
7575
run: |
7676
ngf_prefix=ghcr.io/nginxinc/nginx-gateway-fabric
7777
ngf_tag=${{ steps.ngf-meta.outputs.version }}
78+
if [ ${{ inputs.enable-experimental }} == "true" ]; then export ENABLE_EXPERIMENTAL=true; fi
7879
make generate-static-deployment PLUS_ENABLED=${{ inputs.image == 'plus' && 'true' || 'false' }} PREFIX=${ngf_prefix} TAG=${ngf_tag}
7980
working-directory: ./tests
8081

@@ -146,6 +147,7 @@ jobs:
146147

147148
- name: Run conformance tests
148149
run: |
150+
if [ ${{ inputs.enable-experimental }} == "true" ]; then export ENABLE_EXPERIMENTAL=true; fi
149151
make run-conformance-tests CONFORMANCE_TAG=${{ github.sha }} NGF_VERSION=${{ github.ref_name }} CLUSTER_NAME=${{ github.run_id }}
150152
core_result=$(cat conformance-profile.yaml | yq '.profiles[0].core.result')
151153
extended_result=$(cat conformance-profile.yaml | yq '.profiles[0].extended.result')

charts/nginx-gateway-fabric/templates/clusterrole.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ rules:
7272
- grpcroutes
7373
{{- if .Values.nginxGateway.gwAPIExperimentalFeatures.enable }}
7474
- backendtlspolicies
75+
- tlsroutes
7576
{{- end }}
7677
verbs:
7778
- list
@@ -85,6 +86,7 @@ rules:
8586
- grpcroutes/status
8687
{{- if .Values.nginxGateway.gwAPIExperimentalFeatures.enable }}
8788
- backendtlspolicies/status
89+
- tlsroutes/status
8890
{{- end }}
8991
verbs:
9092
- update

deploy/experimental-nginx-plus/deploy.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ rules:
8282
- referencegrants
8383
- grpcroutes
8484
- backendtlspolicies
85+
- tlsroutes
8586
verbs:
8687
- list
8788
- watch
@@ -93,6 +94,7 @@ rules:
9394
- gatewayclasses/status
9495
- grpcroutes/status
9596
- backendtlspolicies/status
97+
- tlsroutes/status
9698
verbs:
9799
- update
98100
- apiGroups:

deploy/experimental/deploy.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ rules:
7474
- referencegrants
7575
- grpcroutes
7676
- backendtlspolicies
77+
- tlsroutes
7778
verbs:
7879
- list
7980
- watch
@@ -85,6 +86,7 @@ rules:
8586
- gatewayclasses/status
8687
- grpcroutes/status
8788
- backendtlspolicies/status
89+
- tlsroutes/status
8890
verbs:
8991
- update
9092
- apiGroups:

internal/framework/gatewayclass/validate.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var gatewayCRDs = map[string]apiVersion{
2323
"referencegrants.gateway.networking.k8s.io": {},
2424
"backendtlspolicies.gateway.networking.k8s.io": {},
2525
"grpcroutes.gateway.networking.k8s.io": {},
26+
"tlsroutes.gateway.networking.k8s.io": {},
2627
}
2728

2829
type apiVersion struct {

internal/framework/kinds/kinds.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const (
1919
HTTPRoute = "HTTPRoute"
2020
// GRPCRoute is the GRPCRoute kind.
2121
GRPCRoute = "GRPCRoute"
22+
// TLSRoute is the TLSRoute kind.
23+
TLSRoute = "TLSRoute"
2224
)
2325

2426
// NGINX Gateway Fabric kinds.

internal/mode/static/handler.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ func (h *eventHandlerImpl) updateStatuses(ctx context.Context, logger logr.Logge
246246
gcReqs = status.PrepareGatewayClassRequests(graph.GatewayClass, graph.IgnoredGatewayClasses, transitionTime)
247247
}
248248
routeReqs := status.PrepareRouteRequests(
249+
graph.L4Routes,
249250
graph.Routes,
250251
transitionTime,
251252
h.latestReloadResult,

internal/mode/static/manager.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
3232
k8spredicate "sigs.k8s.io/controller-runtime/pkg/predicate"
3333
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
34+
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
3435
gatewayv1alpha3 "sigs.k8s.io/gateway-api/apis/v1alpha3"
3536
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
3637

@@ -73,6 +74,7 @@ func init() {
7374
utilruntime.Must(gatewayv1beta1.Install(scheme))
7475
utilruntime.Must(gatewayv1.Install(scheme))
7576
utilruntime.Must(gatewayv1alpha3.Install(scheme))
77+
utilruntime.Must(gatewayv1alpha2.Install(scheme))
7678
utilruntime.Must(apiv1.AddToScheme(scheme))
7779
utilruntime.Must(discoveryV1.AddToScheme(scheme))
7880
utilruntime.Must(ngfAPI.AddToScheme(scheme))
@@ -489,6 +491,12 @@ func registerControllers(
489491
// https://github.com/nginxinc/nginx-gateway-fabric/issues/1545
490492
objectType: &apiv1.ConfigMap{},
491493
},
494+
{
495+
objectType: &gatewayv1alpha2.TLSRoute{},
496+
options: []controller.Option{
497+
controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}),
498+
},
499+
},
492500
}
493501
controllerRegCfgs = append(controllerRegCfgs, gwExpFeatures...)
494502
}
@@ -663,6 +671,7 @@ func prepareFirstEventBatchPreparerArgs(
663671
objectLists,
664672
&gatewayv1alpha3.BackendTLSPolicyList{},
665673
&apiv1.ConfigMapList{},
674+
&gatewayv1alpha2.TLSRouteList{},
666675
)
667676
}
668677

internal/mode/static/manager_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"sigs.k8s.io/controller-runtime/pkg/client"
1414
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
1515
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
16+
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
1617
gatewayv1alpha3 "sigs.k8s.io/gateway-api/apis/v1alpha3"
1718
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
1819

@@ -105,6 +106,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
105106
&ngfAPI.NginxProxyList{},
106107
partialObjectMetadataList,
107108
&gatewayv1alpha3.BackendTLSPolicyList{},
109+
&gatewayv1alpha2.TLSRouteList{},
108110
&gatewayv1.GRPCRouteList{},
109111
&ngfAPI.ClientSettingsPolicyList{},
110112
&ngfAPI.ObservabilityPolicyList{},

internal/mode/static/nginx/config/base_http_config_template.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,9 @@ map $http_upgrade $connection_upgrade {
1818
default upgrade;
1919
'' close;
2020
}
21+
22+
## Returns just the path from the original request URI.
23+
map $request_uri $request_uri_path {
24+
"~^(?P<path>[^?]*)(\?.*)?$" $path;
25+
}
2126
`

internal/mode/static/nginx/config/base_http_config_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,6 @@ func TestExecuteBaseHttp(t *testing.T) {
4949
g.Expect(test.expCount).To(Equal(strings.Count(string(res[0].data), expSubStr)))
5050
g.Expect(strings.Count(string(res[0].data), "map $http_host $gw_api_compliant_host {")).To(Equal(1))
5151
g.Expect(strings.Count(string(res[0].data), "map $http_upgrade $connection_upgrade {")).To(Equal(1))
52+
g.Expect(strings.Count(string(res[0].data), "map $request_uri $request_uri_path {")).To(Equal(1))
5253
}
5354
}

internal/mode/static/nginx/config/generator_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config"
1212
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/file"
1313
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane"
14+
"github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver"
1415
)
1516

1617
func TestGenerate(t *testing.T) {
@@ -62,8 +63,13 @@ func TestGenerate(t *testing.T) {
6263
},
6364
StreamUpstreams: []dataplane.Upstream{
6465
{
65-
Name: "stream_up",
66-
Endpoints: nil,
66+
Name: "stream_up",
67+
Endpoints: []resolver.Endpoint{
68+
{
69+
Address: "1.1.1.1",
70+
Port: 80,
71+
},
72+
},
6773
},
6874
},
6975
BackendGroups: []dataplane.BackendGroup{bg},

internal/mode/static/nginx/config/http/config.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package http
22

3+
import "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/shared"
4+
35
const InternalRoutePathPrefix = "/_ngf-internal"
46

57
// Server holds all configuration for an HTTP server.
@@ -12,12 +14,7 @@ type Server struct {
1214
IsDefaultHTTP bool
1315
IsDefaultSSL bool
1416
GRPC bool
15-
}
16-
17-
// IPFamily holds the IP family configuration to be used by NGINX.
18-
type IPFamily struct {
19-
IPv4 bool
20-
IPv6 bool
17+
IsSocket bool
2118
}
2219

2320
type LocationType string
@@ -113,7 +110,7 @@ type ProxySSLVerify struct {
113110
// ServerConfig holds configuration for an HTTP server and IP family to be used by NGINX.
114111
type ServerConfig struct {
115112
Servers []Server
116-
IPFamily IPFamily
113+
IPFamily shared.IPFamily
117114
}
118115

119116
// Include defines a file that's included via the include directive.

internal/mode/static/nginx/config/maps.go

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ import (
1111

1212
var mapsTemplate = gotemplate.Must(gotemplate.New("maps").Parse(mapsTemplateText))
1313

14-
// emptyStringSocket is used when the stream server has an invalid upstream. In this case, we pass the connection
15-
// to the empty socket so that NGINX will close the connection with an error in the error log --
16-
// no host in pass "" -- and set $status variable to 500 (logged by stream access log),
17-
// which will indicate the problem to the user.
18-
// https://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_status
19-
const emptyStringSocket = `""`
14+
const (
15+
// emptyStringSocket is used when the stream server has an invalid upstream. In this case, we pass the connection
16+
// to the empty socket so that NGINX will close the connection with an error in the error log --
17+
// no host in pass "" -- and set $status variable to 500 (logged by stream access log),
18+
// which will indicate the problem to the user.
19+
// https://nginx.org/en/docs/stream/ngx_stream_core_module.html#var_status
20+
emptyStringSocket = `""`
21+
22+
// connectionClosedStreamServerSocket is used when we want to listen on a port but have no service configured,
23+
// so we pass to this server that just returns an empty string to tell users that we are listening.
24+
connectionClosedStreamServerSocket = "unix:/var/run/nginx/connection-closed-server.sock"
25+
)
2026

2127
func executeMaps(conf dataplane.Configuration) []executeResult {
2228
maps := buildAddHeaderMaps(append(conf.HTTPServers, conf.SSLServers...))
@@ -44,32 +50,43 @@ func createStreamMaps(conf dataplane.Configuration) []shared.Map {
4450
return nil
4551
}
4652
portsToMap := make(map[int32]shared.Map)
53+
portHasDefault := make(map[int32]struct{})
54+
upstreams := make(map[string]dataplane.Upstream)
55+
56+
for _, u := range conf.StreamUpstreams {
57+
upstreams[u.Name] = u
58+
}
4759

4860
for _, server := range conf.TLSPassthroughServers {
4961
streamMap, portInUse := portsToMap[server.Port]
5062

5163
socket := emptyStringSocket
5264

53-
if server.UpstreamName != "" {
65+
if u, ok := upstreams[server.UpstreamName]; ok && server.UpstreamName != "" && len(u.Endpoints) > 0 {
5466
socket = getSocketNameTLS(server.Port, server.Hostname)
5567
}
5668

57-
mapParam := shared.MapParameter{
58-
Value: server.Hostname,
59-
Result: socket,
69+
if server.IsDefault {
70+
socket = connectionClosedStreamServerSocket
6071
}
6172

6273
if !portInUse {
63-
m := shared.Map{
64-
Source: "$ssl_preread_server_name",
65-
Variable: getTLSPassthroughVarName(server.Port),
66-
Parameters: []shared.MapParameter{
67-
mapParam,
68-
},
74+
streamMap = shared.Map{
75+
Source: "$ssl_preread_server_name",
76+
Variable: getTLSPassthroughVarName(server.Port),
77+
Parameters: make([]shared.MapParameter, 0),
6978
UseHostnames: true,
7079
}
71-
portsToMap[server.Port] = m
72-
} else {
80+
portsToMap[server.Port] = streamMap
81+
}
82+
83+
// If the hostname is empty, we don't want to add an entry to the map. This case occurs when
84+
// the gateway listener hostname is not specified
85+
if server.Hostname != "" {
86+
mapParam := shared.MapParameter{
87+
Value: server.Hostname,
88+
Result: socket,
89+
}
7390
streamMap.Parameters = append(streamMap.Parameters, mapParam)
7491
portsToMap[server.Port] = streamMap
7592
}
@@ -82,6 +99,7 @@ func createStreamMaps(conf dataplane.Configuration) []shared.Map {
8299

83100
if server.IsDefault {
84101
hostname = "default"
102+
portHasDefault[server.Port] = struct{}{}
85103
}
86104

87105
if portInUse {
@@ -95,7 +113,13 @@ func createStreamMaps(conf dataplane.Configuration) []shared.Map {
95113

96114
maps := make([]shared.Map, 0, len(portsToMap))
97115

98-
for _, m := range portsToMap {
116+
for p, m := range portsToMap {
117+
if _, ok := portHasDefault[p]; !ok {
118+
m.Parameters = append(m.Parameters, shared.MapParameter{
119+
Value: "default",
120+
Result: connectionClosedStreamServerSocket,
121+
})
122+
}
99123
maps = append(maps, m)
100124
}
101125

internal/mode/static/nginx/config/maps_template.go

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,12 @@ package config
33
const mapsTemplateText = `
44
{{ range $m := . }}
55
map {{ $m.Source }} {{ $m.Variable }} {
6-
{{- if $m.UseHostnames -}}
6+
{{- if $m.UseHostnames }}
77
hostnames;
88
{{ end }}
9-
109
{{ range $p := $m.Parameters }}
1110
{{ $p.Value }} {{ $p.Result }};
1211
{{ end }}
1312
}
1413
{{- end }}
15-
16-
# Set $gw_api_compliant_host variable to the value of $http_host unless $http_host is empty, then set it to the value
17-
# of $host. We prefer $http_host because it contains the original value of the host header, which is required by the
18-
# Gateway API. However, in an HTTP/1.0 request, it's possible that $http_host can be empty. In this case, we will use
19-
# the value of $host. See http://nginx.org/en/docs/http/ngx_http_core_module.html#var_host.
20-
map $http_host $gw_api_compliant_host {
21-
'' $host;
22-
default $http_host;
23-
}
24-
25-
# Set $connection_header variable to upgrade when the $http_upgrade header is set, otherwise, set it to close. This
26-
# allows support for websocket connections. See https://nginx.org/en/docs/http/websocket.html.
27-
map $http_upgrade $connection_upgrade {
28-
default upgrade;
29-
'' close;
30-
}
31-
32-
## Returns just the path from the original request URI.
33-
map $request_uri $request_uri_path {
34-
"~^(?P<path>[^?]*)(\?.*)?$" $path;
35-
}
3614
`

0 commit comments

Comments
 (0)