diff --git a/internal/nginx/config/generator.go b/internal/nginx/config/generator.go index a3b405baa9..800a0ed134 100644 --- a/internal/nginx/config/generator.go +++ b/internal/nginx/config/generator.go @@ -80,8 +80,22 @@ func generateDefaultHTTPServer() server { func generate(virtualServer state.VirtualServer, serviceStore state.ServiceStore) (server, Warnings) { warnings := newWarnings() - locs := make([]location, 0, len(virtualServer.PathRules)) // FIXME(pleshakov): expand with rule.Routes + s := server{ServerName: virtualServer.Hostname} + + if virtualServer.SSL != nil { + s.SSL = &ssl{ + Certificate: virtualServer.SSL.CertificatePath, + CertificateKey: virtualServer.SSL.CertificatePath, + } + } + + if len(virtualServer.PathRules) == 0 { + // generate default "/" 404 location + s.Locations = []location{{Path: "/", Return: &returnVal{Code: statusNotFound}}} + return s, warnings + } + locs := make([]location, 0, len(virtualServer.PathRules)) // FIXME(pleshakov): expand with rule.Routes for _, rule := range virtualServer.PathRules { matches := make([]httpMatch, 0, len(rule.MatchRules)) @@ -125,16 +139,9 @@ func generate(virtualServer state.VirtualServer, serviceStore state.ServiceStore locs = append(locs, pathLoc) } } - s := server{ - ServerName: virtualServer.Hostname, - Locations: locs, - } - if virtualServer.SSL != nil { - s.SSL = &ssl{ - Certificate: virtualServer.SSL.CertificatePath, - CertificateKey: virtualServer.SSL.CertificatePath, - } - } + + s.Locations = locs + return s, warnings } diff --git a/internal/nginx/config/http.go b/internal/nginx/config/http.go index 50d39b9025..11d2a1762e 100644 --- a/internal/nginx/config/http.go +++ b/internal/nginx/config/http.go @@ -5,21 +5,30 @@ type httpServers struct { } type server struct { - IsDefaultHTTP bool - IsDefaultSSL bool - ServerName string SSL *ssl + ServerName string Locations []location + IsDefaultHTTP bool + IsDefaultSSL bool } type location struct { + Return *returnVal Path string ProxyPass string HTTPMatchVar string Internal bool } +type returnVal struct { + Code statusCode +} + type ssl struct { Certificate string CertificateKey string } + +type statusCode int + +const statusNotFound statusCode = 404 diff --git a/internal/nginx/config/template.go b/internal/nginx/config/template.go index 843a0f5635..b67a162318 100644 --- a/internal/nginx/config/template.go +++ b/internal/nginx/config/template.go @@ -39,8 +39,10 @@ server { {{ if $l.Internal }} internal; {{ end }} - - proxy_set_header Host $host; + + {{ if $l.Return }} + return {{ $l.Return.Code }}; + {{ end }} {{ if $l.HTTPMatchVar }} set $http_matches {{ $l.HTTPMatchVar | printf "%q" }}; @@ -48,6 +50,7 @@ server { {{ end }} {{ if $l.ProxyPass }} + proxy_set_header Host $host; proxy_pass {{ $l.ProxyPass }}$request_uri; {{ end }} } diff --git a/internal/state/change_processor_test.go b/internal/state/change_processor_test.go index 6de1be50b6..bd64f31813 100644 --- a/internal/state/change_processor_test.go +++ b/internal/state/change_processor_test.go @@ -246,6 +246,10 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + { + Hostname: "~^", + SSL: &state.SSL{CertificatePath: certificatePath}, + }, }, } @@ -333,6 +337,10 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + { + Hostname: "~^", + SSL: &state.SSL{CertificatePath: certificatePath}, + }, }, } expectedStatuses := state.Statuses{ @@ -419,6 +427,10 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + { + Hostname: "~^", + SSL: &state.SSL{CertificatePath: certificatePath}, + }, }, } expectedStatuses := state.Statuses{ @@ -505,6 +517,10 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + { + Hostname: "~^", + SSL: &state.SSL{CertificatePath: certificatePath}, + }, }, } expectedStatuses := state.Statuses{ @@ -590,6 +606,10 @@ var _ = Describe("ChangeProcessor", func() { CertificatePath: certificatePath, }, }, + { + Hostname: "~^", + SSL: &state.SSL{CertificatePath: certificatePath}, + }, }, } expectedStatuses := state.Statuses{ @@ -669,6 +689,10 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + { + Hostname: "~^", + SSL: &state.SSL{CertificatePath: certificatePath}, + }, }, } expectedStatuses := state.Statuses{ @@ -754,6 +778,10 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + { + Hostname: "~^", + SSL: &state.SSL{CertificatePath: certificatePath}, + }, }, } expectedStatuses := state.Statuses{ @@ -791,12 +819,17 @@ var _ = Describe("ChangeProcessor", func() { Expect(helpers.Diff(expectedStatuses, statuses)).To(BeEmpty()) }) - It("should return empty configuration and updated statuses after deleting the second HTTPRoute", func() { + It("should return configuration with default ssl server and updated statuses after deleting the second HTTPRoute", func() { processor.CaptureDeleteChange(&v1alpha2.HTTPRoute{}, types.NamespacedName{Namespace: "test", Name: "hr-2"}) expectedConf := state.Configuration{ HTTPServers: []state.VirtualServer{}, - SSLServers: []state.VirtualServer{}, + SSLServers: []state.VirtualServer{ + { + Hostname: "~^", + SSL: &state.SSL{CertificatePath: certificatePath}, + }, + }, } expectedStatuses := state.Statuses{ GatewayClassStatus: &state.GatewayClassStatus{ diff --git a/internal/state/configuration.go b/internal/state/configuration.go index 034909a9d5..47cb2e8ec6 100644 --- a/internal/state/configuration.go +++ b/internal/state/configuration.go @@ -7,6 +7,8 @@ import ( "sigs.k8s.io/gateway-api/apis/v1alpha2" ) +const wildcardHostname = "~^" + // Configuration is an internal representation of Gateway configuration. // We can think of Configuration as an intermediate state between the Gateway API resources and the data plane (NGINX) // configuration. @@ -89,8 +91,8 @@ type configBuilder struct { func newConfigBuilder() *configBuilder { return &configBuilder{ - http: newVirtualServerBuilder(), - ssl: newVirtualServerBuilder(), + http: newVirtualServerBuilder(v1alpha2.HTTPProtocolType), + ssl: newVirtualServerBuilder(v1alpha2.HTTPSProtocolType), } } @@ -113,19 +115,27 @@ func (b *configBuilder) build() Configuration { } type virtualServerBuilder struct { + protocolType v1alpha2.ProtocolType rulesPerHost map[string]map[string]PathRule listenersForHost map[string]*listener + listeners []*listener } -func newVirtualServerBuilder() *virtualServerBuilder { +func newVirtualServerBuilder(protocolType v1alpha2.ProtocolType) *virtualServerBuilder { return &virtualServerBuilder{ + protocolType: protocolType, rulesPerHost: make(map[string]map[string]PathRule), listenersForHost: make(map[string]*listener), + listeners: make([]*listener, 0), } } func (b *virtualServerBuilder) upsertListener(l *listener) { + if b.protocolType == v1alpha2.HTTPSProtocolType { + b.listeners = append(b.listeners, l) + } + for _, r := range l.Routes { var hostnames []string @@ -137,6 +147,7 @@ func (b *virtualServerBuilder) upsertListener(l *listener) { for _, h := range hostnames { b.listenersForHost[h] = l + if _, exist := b.rulesPerHost[h]; !exist { b.rulesPerHost[h] = make(map[string]PathRule) } @@ -144,6 +155,7 @@ func (b *virtualServerBuilder) upsertListener(l *listener) { for i, rule := range r.Source.Spec.Rules { for _, h := range hostnames { + for j, m := range rule.Matches { path := getPath(m.Path) @@ -167,7 +179,7 @@ func (b *virtualServerBuilder) upsertListener(l *listener) { func (b *virtualServerBuilder) build() []VirtualServer { - servers := make([]VirtualServer, 0, len(b.rulesPerHost)) + servers := make([]VirtualServer, 0, len(b.rulesPerHost)+len(b.listeners)) for h, rules := range b.rulesPerHost { s := VirtualServer{ @@ -198,7 +210,18 @@ func (b *virtualServerBuilder) build() []VirtualServer { servers = append(servers, s) } - // sort servers for predictable order + for _, l := range b.listeners { + hostname := getListenerHostname(l.Source.Hostname) + // generate a 404 ssl server block for listeners with no routes or listeners with wildcard (match-all) routes + // FIXME(kate-osborn): when we support regex hostnames (e.g. *.example.com) we will have to modify this check to catch regex hostnames. + if len(l.Routes) == 0 || hostname == wildcardHostname { + servers = append(servers, VirtualServer{ + Hostname: hostname, + SSL: &SSL{CertificatePath: l.SecretPath}, + }) + } + } + sort.Slice(servers, func(i, j int) bool { return servers[i].Hostname < servers[j].Hostname }) @@ -206,6 +229,15 @@ func (b *virtualServerBuilder) build() []VirtualServer { return servers } +func getListenerHostname(h *v1alpha2.Hostname) string { + name := getHostname(h) + if name == "" { + return wildcardHostname + } + + return name +} + func getPath(path *v1alpha2.HTTPPathMatch) string { if path == nil || path.Value == nil || *path.Value == "" { return "/" diff --git a/internal/state/configuration_test.go b/internal/state/configuration_test.go index dde277d90f..cd412ac472 100644 --- a/internal/state/configuration_test.go +++ b/internal/state/configuration_test.go @@ -123,7 +123,17 @@ func TestBuildConfiguration(t *testing.T) { httpsRouteHR4 := &route{ Source: httpsHR4, ValidSectionNameRefs: map[string]struct{}{ - "listener-80-1": {}, + "listener-443-1": {}, + }, + InvalidSectionNameRefs: map[string]struct{}{}, + } + + httpsHR5 := createRoute("https-hr-5", "example.com", "listener-443-with-hostname", "/") + + httpsRouteHR5 := &route{ + Source: httpsHR5, + ValidSectionNameRefs: map[string]struct{}{ + "listener-443-with-hostname": {}, }, InvalidSectionNameRefs: map[string]struct{}{}, } @@ -151,6 +161,24 @@ func TestBuildConfiguration(t *testing.T) { }, }, } + hostname := v1alpha2.Hostname("example.com") + + listener443WithHostname := v1alpha2.Listener{ + Name: "listener-443-with-hostname", + Hostname: &hostname, + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{ + { + Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer("test")), + }, + }, + }, + } invalidListener := v1alpha2.Listener{ Name: "invalid-listener", @@ -201,8 +229,34 @@ func TestBuildConfiguration(t *testing.T) { Routes: map[types.NamespacedName]*route{}, AcceptedHostnames: map[string]struct{}{}, }, + }, + }, + Routes: map[types.NamespacedName]*route{}, + }, + expected: Configuration{ + HTTPServers: []VirtualServer{}, + SSLServers: []VirtualServer{}, + }, + msg: "http listener with no routes", + }, + { + graph: &graph{ + GatewayClass: &gatewayClass{ + Source: &v1alpha2.GatewayClass{}, + Valid: true, + }, + Gateway: &gateway{ + Source: &v1alpha2.Gateway{}, + Listeners: map[string]*listener{ "listener-443-1": { - Source: listener443, + Source: listener443, // nil hostname + Valid: true, + Routes: map[types.NamespacedName]*route{}, + AcceptedHostnames: map[string]struct{}{}, + SecretPath: secretPath, + }, + "listener-443-with-hostname": { + Source: listener443WithHostname, // non-nil hostname Valid: true, Routes: map[types.NamespacedName]*route{}, AcceptedHostnames: map[string]struct{}{}, @@ -214,9 +268,18 @@ func TestBuildConfiguration(t *testing.T) { }, expected: Configuration{ HTTPServers: []VirtualServer{}, - SSLServers: []VirtualServer{}, + SSLServers: []VirtualServer{ + { + Hostname: string(hostname), + SSL: &SSL{CertificatePath: secretPath}, + }, + { + Hostname: wildcardHostname, + SSL: &SSL{CertificatePath: secretPath}, + }, + }, }, - msg: "http and https listeners with no routes", + msg: "https listeners with no routes", }, { graph: &graph{ @@ -274,26 +337,11 @@ func TestBuildConfiguration(t *testing.T) { "bar.example.com": {}, }, }, - "listener-443-1": { - Source: listener443, - Valid: true, - SecretPath: secretPath, - Routes: map[types.NamespacedName]*route{ - {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, - {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, - }, - AcceptedHostnames: map[string]struct{}{ - "foo.example.com": {}, - "bar.example.com": {}, - }, - }, }, }, Routes: map[types.NamespacedName]*route{ - {Namespace: "test", Name: "hr-1"}: routeHR1, - {Namespace: "test", Name: "hr-2"}: routeHR2, - {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, - {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, + {Namespace: "test", Name: "hr-1"}: routeHR1, + {Namespace: "test", Name: "hr-2"}: routeHR2, }, }, expected: Configuration{ @@ -329,6 +377,53 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, + SSLServers: []VirtualServer{}, + }, + msg: "one http listener with two routes for different hostnames", + }, + { + graph: &graph{ + GatewayClass: &gatewayClass{ + Source: &v1alpha2.GatewayClass{}, + Valid: true, + }, + Gateway: &gateway{ + Source: &v1alpha2.Gateway{}, + Listeners: map[string]*listener{ + "listener-443-1": { + Source: listener443, + Valid: true, + SecretPath: secretPath, + Routes: map[types.NamespacedName]*route{ + {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, + {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, + }, + AcceptedHostnames: map[string]struct{}{ + "foo.example.com": {}, + "bar.example.com": {}, + }, + }, + "listener-443-with-hostname": { + Source: listener443WithHostname, + Valid: true, + SecretPath: secretPath, + Routes: map[types.NamespacedName]*route{ + {Namespace: "test", Name: "https-hr-5"}: httpsRouteHR5, + }, + AcceptedHostnames: map[string]struct{}{ + "example.com": {}, + }, + }, + }, + }, + Routes: map[types.NamespacedName]*route{ + {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, + {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, + {Namespace: "test", Name: "https-hr-5"}: httpsRouteHR5, + }, + }, + expected: Configuration{ + HTTPServers: []VirtualServer{}, SSLServers: []VirtualServer{ { Hostname: "bar.example.com", @@ -348,6 +443,24 @@ func TestBuildConfiguration(t *testing.T) { CertificatePath: secretPath, }, }, + { + Hostname: "example.com", + PathRules: []PathRule{ + { + Path: "/", + MatchRules: []MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: httpsHR5, + }, + }, + }, + }, + SSL: &SSL{ + CertificatePath: secretPath, + }, + }, { Hostname: "foo.example.com", PathRules: []PathRule{ @@ -366,9 +479,13 @@ func TestBuildConfiguration(t *testing.T) { CertificatePath: secretPath, }, }, + { + Hostname: wildcardHostname, + SSL: &SSL{CertificatePath: secretPath}, + }, }, }, - msg: "one http and one https listener each with two routes for different hostnames", + msg: "two https listeners each with routes for different hostnames", }, { graph: &graph{ @@ -498,6 +615,10 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, + { + Hostname: wildcardHostname, + SSL: &SSL{CertificatePath: secretPath}, + }, }, }, msg: "one http and one https listener with two routes with the same hostname with and without collisions", @@ -679,3 +800,37 @@ func TestMatchRuleGetMatch(t *testing.T) { } } } + +func TestGetListenerHostname(t *testing.T) { + var emptyHostname v1alpha2.Hostname + var hostname v1alpha2.Hostname = "example.com" + + tests := []struct { + hostname *v1alpha2.Hostname + expected string + msg string + }{ + { + hostname: nil, + expected: wildcardHostname, + msg: "nil hostname", + }, + { + hostname: &emptyHostname, + expected: wildcardHostname, + msg: "empty hostname", + }, + { + hostname: &hostname, + expected: string(hostname), + msg: "normal hostname", + }, + } + + for _, test := range tests { + result := getListenerHostname(test.hostname) + if result != test.expected { + t.Errorf("getListenerHostname() returned %q but expected %q for the case of %q", result, test.expected, test.msg) + } + } +} diff --git a/internal/state/graph_test.go b/internal/state/graph_test.go index fe4922ea45..5790bc6cbf 100644 --- a/internal/state/graph_test.go +++ b/internal/state/graph_test.go @@ -411,6 +411,17 @@ func TestBuildListeners(t *testing.T) { }, }, } + + tlsConfigInvalidSecret := &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{ + { + Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("Secret")), + Name: "does-not-exist", + Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer("test")), + }, + }, + } // https listeners listener4431 := v1alpha2.Listener{ Name: "listener-443-1", @@ -440,6 +451,13 @@ func TestBuildListeners(t *testing.T) { TLS: nil, // invalid https listener; missing tls config Protocol: v1alpha2.HTTPSProtocolType, } + listener4435 := v1alpha2.Listener{ + Name: "listener-443-5", + Hostname: (*v1alpha2.Hostname)(helpers.GetStringPointer("foo.example.com")), + Port: 443, + TLS: tlsConfigInvalidSecret, // invalid https listener; secret does not exist + Protocol: v1alpha2.HTTPSProtocolType, + } tests := []struct { gateway *v1alpha2.Gateway expected map[string]*listener @@ -535,6 +553,28 @@ func TestBuildListeners(t *testing.T) { }, msg: "invalid https listener (tls config missing)", }, + { + gateway: &v1alpha2.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1alpha2.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1alpha2.Listener{ + listener4435, + }, + }, + }, + expected: map[string]*listener{ + "listener-443-5": { + Source: listener4435, + Valid: false, + Routes: map[types.NamespacedName]*route{}, + AcceptedHostnames: map[string]struct{}{}, + }, + }, + msg: "invalid https listener (secret does not exist)", + }, { gateway: &v1alpha2.Gateway{ ObjectMeta: metav1.ObjectMeta{