From 68d1f6eb80d23c8650c11629459dd6a06c986ca1 Mon Sep 17 00:00:00 2001 From: Michael Pleshakov Date: Wed, 22 Feb 2023 10:55:07 -0800 Subject: [PATCH] Refactor graph and configuration into separate packages - Move graph-related types and functions to state/graph package. Those are further split into Gateway resource-related file like httproute.go - Move configuration-related types and functions to state/dataplane package - Add package docs for both packages. --- internal/events/handler.go | 8 +- internal/events/handler_test.go | 16 +- internal/manager/manager.go | 5 +- .../config/configfakes/fake_generator.go | 14 +- internal/nginx/config/generator.go | 8 +- internal/nginx/config/generator_test.go | 19 +- internal/nginx/config/servers.go | 12 +- internal/nginx/config/servers_test.go | 87 +- internal/nginx/config/split_clients.go | 15 +- internal/nginx/config/split_clients_test.go | 109 +- internal/nginx/config/upstreams.go | 8 +- internal/nginx/config/upstreams_test.go | 16 +- internal/sort/doc.go | 2 + internal/sort/sort.go | 16 + internal/sort/sort_test.go | 91 ++ internal/state/change_processor.go | 28 +- internal/state/change_processor_test.go | 178 +-- .../state/{ => dataplane}/configuration.go | 57 +- .../{ => dataplane}/configuration_test.go | 255 ++-- internal/state/dataplane/doc.go | 10 + internal/state/{ => dataplane}/sort.go | 17 +- internal/state/{ => dataplane}/sort_test.go | 120 +- internal/state/{ => dataplane}/warnings.go | 2 +- .../state/{ => dataplane}/warnings_test.go | 2 +- internal/state/{ => graph}/backend_group.go | 2 +- .../state/{ => graph}/backend_group_test.go | 6 +- internal/state/{ => graph}/backend_refs.go | 4 +- .../state/{ => graph}/backend_refs_test.go | 6 +- internal/state/graph/doc.go | 14 + .../state/{listener.go => graph/gateway.go} | 117 +- internal/state/graph/gateway_test.go | 782 ++++++++++ internal/state/graph/gatewayclass.go | 44 + internal/state/graph/gatewayclass_test.go | 78 + internal/state/graph/graph.go | 71 + internal/state/graph/graph_test.go | 297 ++++ .../state/{graph.go => graph/httproute.go} | 166 +-- internal/state/graph/httproute_test.go | 397 +++++ internal/state/graph_test.go | 1293 ----------------- internal/state/listener_test.go | 236 --- internal/state/{ => secrets}/file_manager.go | 2 +- internal/state/{ => secrets}/secrets.go | 2 +- internal/state/{ => secrets}/secrets_test.go | 42 +- .../secretsfakes}/fake_dir_entry.go | 2 +- .../secretsfakes}/fake_file_manager.go | 6 +- .../fake_secret_disk_memory_manager.go | 6 +- .../secretsfakes}/fake_secret_store.go | 26 +- .../state/statefakes/fake_change_processor.go | 21 +- internal/state/statuses.go | 5 +- internal/state/statuses_test.go | 31 +- 49 files changed, 2419 insertions(+), 2332 deletions(-) create mode 100644 internal/sort/doc.go create mode 100644 internal/sort/sort.go create mode 100644 internal/sort/sort_test.go rename internal/state/{ => dataplane}/configuration.go (87%) rename internal/state/{ => dataplane}/configuration_test.go (84%) create mode 100644 internal/state/dataplane/doc.go rename internal/state/{ => dataplane}/sort.go (80%) rename internal/state/{ => dataplane}/sort_test.go (59%) rename internal/state/{ => dataplane}/warnings.go (97%) rename internal/state/{ => dataplane}/warnings_test.go (99%) rename internal/state/{ => graph}/backend_group.go (98%) rename internal/state/{ => graph}/backend_group_test.go (75%) rename internal/state/{ => graph}/backend_refs.go (98%) rename internal/state/{ => graph}/backend_refs_test.go (98%) create mode 100644 internal/state/graph/doc.go rename internal/state/{listener.go => graph/gateway.go} (71%) create mode 100644 internal/state/graph/gateway_test.go create mode 100644 internal/state/graph/gatewayclass.go create mode 100644 internal/state/graph/gatewayclass_test.go create mode 100644 internal/state/graph/graph.go create mode 100644 internal/state/graph/graph_test.go rename internal/state/{graph.go => graph/httproute.go} (53%) create mode 100644 internal/state/graph/httproute_test.go delete mode 100644 internal/state/graph_test.go delete mode 100644 internal/state/listener_test.go rename internal/state/{ => secrets}/file_manager.go (97%) rename internal/state/{ => secrets}/secrets.go (99%) rename internal/state/{ => secrets}/secrets_test.go (90%) rename internal/state/{statefakes => secrets/secretsfakes}/fake_dir_entry.go (99%) rename internal/state/{statefakes => secrets/secretsfakes}/fake_file_manager.go (98%) rename internal/state/{statefakes => secrets/secretsfakes}/fake_secret_disk_memory_manager.go (97%) rename internal/state/{statefakes => secrets/secretsfakes}/fake_secret_store.go (88%) diff --git a/internal/events/handler.go b/internal/events/handler.go index d4ab3e415..ee7ab749a 100644 --- a/internal/events/handler.go +++ b/internal/events/handler.go @@ -13,6 +13,8 @@ import ( "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/file" "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/runtime" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets" "github.com/nginxinc/nginx-kubernetes-gateway/internal/status" ) @@ -30,9 +32,9 @@ type EventHandlerConfig struct { // Processor is the state ChangeProcessor. Processor state.ChangeProcessor // SecretStore is the state SecretStore. - SecretStore state.SecretStore + SecretStore secrets.SecretStore // SecretMemoryManager is the state SecretMemoryManager. - SecretMemoryManager state.SecretDiskMemoryManager + SecretMemoryManager secrets.SecretDiskMemoryManager // Generator is the nginx config Generator. Generator config.Generator // NginxFileMgr is the file Manager for nginx. @@ -88,7 +90,7 @@ func (h *EventHandlerImpl) HandleEventBatch(ctx context.Context, batch EventBatc h.cfg.StatusUpdater.Update(ctx, statuses) } -func (h *EventHandlerImpl) updateNginx(ctx context.Context, conf state.Configuration) error { +func (h *EventHandlerImpl) updateNginx(ctx context.Context, conf dataplane.Configuration) error { // Write all secrets (nuke and pave). // This will remove all secrets in the secrets directory before writing the requested secrets. // FIXME(kate-osborn): We may want to rethink this approach in the future and write and remove secrets individually. diff --git a/internal/events/handler_test.go b/internal/events/handler_test.go index f849982c6..2f7d49620 100644 --- a/internal/events/handler_test.go +++ b/internal/events/handler_test.go @@ -20,6 +20,8 @@ import ( "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/file/filefakes" "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/runtime/runtimefakes" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets/secretsfakes" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/statefakes" "github.com/nginxinc/nginx-kubernetes-gateway/internal/status/statusfakes" ) @@ -40,15 +42,15 @@ var _ = Describe("EventHandler", func() { var ( handler *events.EventHandlerImpl fakeProcessor *statefakes.FakeChangeProcessor - fakeSecretStore *statefakes.FakeSecretStore - fakeSecretMemoryManager *statefakes.FakeSecretDiskMemoryManager + fakeSecretStore *secretsfakes.FakeSecretStore + fakeSecretMemoryManager *secretsfakes.FakeSecretDiskMemoryManager fakeGenerator *configfakes.FakeGenerator fakeNginxFileMgr *filefakes.FakeManager fakeNginxRuntimeMgr *runtimefakes.FakeManager fakeStatusUpdater *statusfakes.FakeUpdater ) - expectReconfig := func(expectedConf state.Configuration, expectedCfg []byte, expectedStatuses state.Statuses) { + expectReconfig := func(expectedConf dataplane.Configuration, expectedCfg []byte, expectedStatuses state.Statuses) { Expect(fakeProcessor.ProcessCallCount()).Should(Equal(1)) Expect(fakeGenerator.GenerateCallCount()).Should(Equal(1)) @@ -68,8 +70,8 @@ var _ = Describe("EventHandler", func() { BeforeEach(func() { fakeProcessor = &statefakes.FakeChangeProcessor{} - fakeSecretMemoryManager = &statefakes.FakeSecretDiskMemoryManager{} - fakeSecretStore = &statefakes.FakeSecretStore{} + fakeSecretMemoryManager = &secretsfakes.FakeSecretDiskMemoryManager{} + fakeSecretStore = &secretsfakes.FakeSecretStore{} fakeGenerator = &configfakes.FakeGenerator{} fakeNginxFileMgr = &filefakes.FakeManager{} fakeNginxRuntimeMgr = &runtimefakes.FakeManager{} @@ -91,7 +93,7 @@ var _ = Describe("EventHandler", func() { DescribeTable( "A batch with one event", func(e interface{}) { - fakeConf := state.Configuration{} + fakeConf := dataplane.Configuration{} fakeStatuses := state.Statuses{} changed := true fakeProcessor.ProcessReturns(changed, fakeConf, fakeStatuses) @@ -256,7 +258,7 @@ var _ = Describe("EventHandler", func() { batch = append(batch, upserts...) batch = append(batch, deletes...) - fakeConf := state.Configuration{} + fakeConf := dataplane.Configuration{} changed := true fakeStatuses := state.Statuses{} fakeProcessor.ProcessReturns(changed, fakeConf, fakeStatuses) diff --git a/internal/manager/manager.go b/internal/manager/manager.go index b7f196380..ec7e79d04 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -27,6 +27,7 @@ import ( "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/relationship" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/resolver" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets" "github.com/nginxinc/nginx-kubernetes-gateway/internal/status" ) @@ -118,8 +119,8 @@ func Start(cfg config.Config) error { } } - secretStore := state.NewSecretStore() - secretMemoryMgr := state.NewSecretDiskMemoryManager(secretsFolder, secretStore) + secretStore := secrets.NewSecretStore() + secretMemoryMgr := secrets.NewSecretDiskMemoryManager(secretsFolder, secretStore) processor := state.NewChangeProcessorImpl(state.ChangeProcessorConfig{ GatewayCtlrName: cfg.GatewayCtlrName, diff --git a/internal/nginx/config/configfakes/fake_generator.go b/internal/nginx/config/configfakes/fake_generator.go index 09d8f5702..73247d738 100644 --- a/internal/nginx/config/configfakes/fake_generator.go +++ b/internal/nginx/config/configfakes/fake_generator.go @@ -5,14 +5,14 @@ import ( "sync" "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" ) type FakeGenerator struct { - GenerateStub func(state.Configuration) []byte + GenerateStub func(dataplane.Configuration) []byte generateMutex sync.RWMutex generateArgsForCall []struct { - arg1 state.Configuration + arg1 dataplane.Configuration } generateReturns struct { result1 []byte @@ -24,11 +24,11 @@ type FakeGenerator struct { invocationsMutex sync.RWMutex } -func (fake *FakeGenerator) Generate(arg1 state.Configuration) []byte { +func (fake *FakeGenerator) Generate(arg1 dataplane.Configuration) []byte { fake.generateMutex.Lock() ret, specificReturn := fake.generateReturnsOnCall[len(fake.generateArgsForCall)] fake.generateArgsForCall = append(fake.generateArgsForCall, struct { - arg1 state.Configuration + arg1 dataplane.Configuration }{arg1}) stub := fake.GenerateStub fakeReturns := fake.generateReturns @@ -49,13 +49,13 @@ func (fake *FakeGenerator) GenerateCallCount() int { return len(fake.generateArgsForCall) } -func (fake *FakeGenerator) GenerateCalls(stub func(state.Configuration) []byte) { +func (fake *FakeGenerator) GenerateCalls(stub func(dataplane.Configuration) []byte) { fake.generateMutex.Lock() defer fake.generateMutex.Unlock() fake.GenerateStub = stub } -func (fake *FakeGenerator) GenerateArgsForCall(i int) state.Configuration { +func (fake *FakeGenerator) GenerateArgsForCall(i int) dataplane.Configuration { fake.generateMutex.RLock() defer fake.generateMutex.RUnlock() argsForCall := fake.generateArgsForCall[i] diff --git a/internal/nginx/config/generator.go b/internal/nginx/config/generator.go index 8b9de2670..59403d24f 100644 --- a/internal/nginx/config/generator.go +++ b/internal/nginx/config/generator.go @@ -1,7 +1,7 @@ package config import ( - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" ) //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . Generator @@ -10,7 +10,7 @@ import ( // This interface is used for testing purposes only. type Generator interface { // Generate generates NGINX configuration from internal representation. - Generate(configuration state.Configuration) []byte + Generate(configuration dataplane.Configuration) []byte } // GeneratorImpl is an implementation of Generator. @@ -22,9 +22,9 @@ func NewGeneratorImpl() GeneratorImpl { } // executeFunc is a function that generates NGINX configuration from internal representation. -type executeFunc func(configuration state.Configuration) []byte +type executeFunc func(configuration dataplane.Configuration) []byte -func (g GeneratorImpl) Generate(conf state.Configuration) []byte { +func (g GeneratorImpl) Generate(conf dataplane.Configuration) []byte { var generated []byte for _, execute := range getExecuteFuncs() { generated = append(generated, execute(conf)...) diff --git a/internal/nginx/config/generator_test.go b/internal/nginx/config/generator_test.go index cfdc38045..6f39b3366 100644 --- a/internal/nginx/config/generator_test.go +++ b/internal/nginx/config/generator_test.go @@ -7,23 +7,24 @@ import ( "k8s.io/apimachinery/pkg/types" "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/graph" ) // Note: this test only verifies that Generate() returns a byte array with upstream, server, and split_client blocks. // It does not test the correctness of those blocks. That functionality is covered by other tests in this package. func TestGenerate(t *testing.T) { - bg := state.BackendGroup{ + bg := graph.BackendGroup{ Source: types.NamespacedName{Namespace: "test", Name: "hr"}, RuleIdx: 0, - Backends: []state.BackendRef{ + Backends: []graph.BackendRef{ {Name: "test", Valid: true, Weight: 1}, {Name: "test2", Valid: true, Weight: 1}, }, } - conf := state.Configuration{ - HTTPServers: []state.VirtualServer{ + conf := dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ { IsDefault: true, }, @@ -31,24 +32,24 @@ func TestGenerate(t *testing.T) { Hostname: "example.com", }, }, - SSLServers: []state.VirtualServer{ + SSLServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "example.com", - SSL: &state.SSL{ + SSL: &dataplane.SSL{ CertificatePath: "/etc/nginx/secrets/default", }, }, }, - Upstreams: []state.Upstream{ + Upstreams: []dataplane.Upstream{ { Name: "up", Endpoints: nil, }, }, - BackendGroups: []state.BackendGroup{bg}, + BackendGroups: []graph.BackendGroup{bg}, } generator := config.NewGeneratorImpl() cfg := string(generator.Generate(conf)) diff --git a/internal/nginx/config/servers.go b/internal/nginx/config/servers.go index 332407b5e..1771a7d0c 100644 --- a/internal/nginx/config/servers.go +++ b/internal/nginx/config/servers.go @@ -9,20 +9,20 @@ import ( "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config/http" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" ) var serversTemplate = gotemplate.Must(gotemplate.New("servers").Parse(serversTemplateText)) const rootPath = "/" -func executeServers(conf state.Configuration) []byte { +func executeServers(conf dataplane.Configuration) []byte { servers := createServers(conf.HTTPServers, conf.SSLServers) return execute(serversTemplate, servers) } -func createServers(httpServers, sslServers []state.VirtualServer) []http.Server { +func createServers(httpServers, sslServers []dataplane.VirtualServer) []http.Server { servers := make([]http.Server, 0, len(httpServers)+len(sslServers)) for _, s := range httpServers { @@ -36,7 +36,7 @@ func createServers(httpServers, sslServers []state.VirtualServer) []http.Server return servers } -func createSSLServer(virtualServer state.VirtualServer) http.Server { +func createSSLServer(virtualServer dataplane.VirtualServer) http.Server { if virtualServer.IsDefault { return createDefaultSSLServer() } @@ -51,7 +51,7 @@ func createSSLServer(virtualServer state.VirtualServer) http.Server { } } -func createServer(virtualServer state.VirtualServer) http.Server { +func createServer(virtualServer dataplane.VirtualServer) http.Server { if virtualServer.IsDefault { return createDefaultHTTPServer() } @@ -62,7 +62,7 @@ func createServer(virtualServer state.VirtualServer) http.Server { } } -func createLocations(pathRules []state.PathRule, listenerPort int) []http.Location { +func createLocations(pathRules []dataplane.PathRule, listenerPort int) []http.Location { lenPathRules := len(pathRules) if lenPathRules == 0 { diff --git a/internal/nginx/config/servers_test.go b/internal/nginx/config/servers_test.go index 621d4d2d0..793bb556a 100644 --- a/internal/nginx/config/servers_test.go +++ b/internal/nginx/config/servers_test.go @@ -14,12 +14,13 @@ import ( "github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers" "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config/http" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/graph" ) func TestExecuteServers(t *testing.T) { - conf := state.Configuration{ - HTTPServers: []state.VirtualServer{ + conf := dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ { IsDefault: true, }, @@ -30,19 +31,19 @@ func TestExecuteServers(t *testing.T) { Hostname: "cafe.example.com", }, }, - SSLServers: []state.VirtualServer{ + SSLServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "example.com", - SSL: &state.SSL{ + SSL: &dataplane.SSL{ CertificatePath: "cert-path", }, }, { Hostname: "cafe.example.com", - SSL: &state.SSL{ + SSL: &dataplane.SSL{ CertificatePath: "cert-path", }, }, @@ -75,19 +76,19 @@ func TestExecuteServers(t *testing.T) { func TestExecuteForDefaultServers(t *testing.T) { testcases := []struct { msg string - conf state.Configuration + conf dataplane.Configuration httpDefault bool sslDefault bool }{ { - conf: state.Configuration{}, + conf: dataplane.Configuration{}, httpDefault: false, sslDefault: false, msg: "no default servers", }, { - conf: state.Configuration{ - HTTPServers: []state.VirtualServer{ + conf: dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ { IsDefault: true, }, @@ -98,8 +99,8 @@ func TestExecuteForDefaultServers(t *testing.T) { msg: "only HTTP default server", }, { - conf: state.Configuration{ - SSLServers: []state.VirtualServer{ + conf: dataplane.Configuration{ + SSLServers: []dataplane.VirtualServer{ { IsDefault: true, }, @@ -110,13 +111,13 @@ func TestExecuteForDefaultServers(t *testing.T) { msg: "only HTTPS default server", }, { - conf: state.Configuration{ - HTTPServers: []state.VirtualServer{ + conf: dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ { IsDefault: true, }, }, - SSLServers: []state.VirtualServer{ + SSLServers: []dataplane.VirtualServer{ { IsDefault: true, }, @@ -276,10 +277,10 @@ func TestCreateServers(t *testing.T) { hrNsName := types.NamespacedName{Namespace: hr.Namespace, Name: hr.Name} - fooGroup := state.BackendGroup{ + fooGroup := graph.BackendGroup{ Source: hrNsName, RuleIdx: 0, - Backends: []state.BackendRef{ + Backends: []graph.BackendRef{ { Name: "test_foo_80", Valid: true, @@ -289,10 +290,10 @@ func TestCreateServers(t *testing.T) { } // barGroup has two backends, which should generate a proxy pass with a variable. - barGroup := state.BackendGroup{ + barGroup := graph.BackendGroup{ Source: hrNsName, RuleIdx: 1, - Backends: []state.BackendRef{ + Backends: []graph.BackendRef{ { Name: "test_bar_80", Valid: true, @@ -307,10 +308,10 @@ func TestCreateServers(t *testing.T) { } // baz group has an invalid backend, which should generate a proxy pass to the invalid ref backend. - bazGroup := state.BackendGroup{ + bazGroup := graph.BackendGroup{ Source: hrNsName, RuleIdx: 2, - Backends: []state.BackendRef{ + Backends: []graph.BackendRef{ { Name: "test_baz_80", Valid: false, @@ -319,14 +320,14 @@ func TestCreateServers(t *testing.T) { }, } - filterGroup1 := state.BackendGroup{Source: hrNsName, RuleIdx: 3} + filterGroup1 := graph.BackendGroup{Source: hrNsName, RuleIdx: 3} - filterGroup2 := state.BackendGroup{Source: hrNsName, RuleIdx: 4} + filterGroup2 := graph.BackendGroup{Source: hrNsName, RuleIdx: 4} - cafePathRules := []state.PathRule{ + cafePathRules := []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -349,7 +350,7 @@ func TestCreateServers(t *testing.T) { }, { Path: "/test", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 1, @@ -360,7 +361,7 @@ func TestCreateServers(t *testing.T) { }, { Path: "/path-only", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 2, @@ -371,12 +372,12 @@ func TestCreateServers(t *testing.T) { }, { Path: "/redirect-implicit-port", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 3, Source: hr, - Filters: state.Filters{ + Filters: dataplane.Filters{ RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("foo.example.com")), }, @@ -387,12 +388,12 @@ func TestCreateServers(t *testing.T) { }, { Path: "/redirect-explicit-port", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 4, Source: hr, - Filters: state.Filters{ + Filters: dataplane.Filters{ RequestRedirect: &v1beta1.HTTPRequestRedirectFilter{ Hostname: (*v1beta1.PreciseHostname)(helpers.GetStringPointer("bar.example.com")), Port: (*v1beta1.PortNumber)(helpers.GetInt32Pointer(8080)), @@ -404,7 +405,7 @@ func TestCreateServers(t *testing.T) { }, } - httpServers := []state.VirtualServer{ + httpServers := []dataplane.VirtualServer{ { IsDefault: true, }, @@ -414,13 +415,13 @@ func TestCreateServers(t *testing.T) { }, } - sslServers := []state.VirtualServer{ + sslServers := []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "cafe.example.com", - SSL: &state.SSL{CertificatePath: certPath}, + SSL: &dataplane.SSL{CertificatePath: certPath}, PathRules: cafePathRules, }, } @@ -577,10 +578,10 @@ func TestCreateLocationsRootPath(t *testing.T) { hrNsName := types.NamespacedName{Namespace: "test", Name: "route1"} - fooGroup := state.BackendGroup{ + fooGroup := graph.BackendGroup{ Source: hrNsName, RuleIdx: 0, - Backends: []state.BackendRef{ + Backends: []graph.BackendRef{ { Name: "test_foo_80", Valid: true, @@ -589,11 +590,11 @@ func TestCreateLocationsRootPath(t *testing.T) { }, } - getPathRules := func(source *v1beta1.HTTPRoute, rootPath bool) []state.PathRule { - rules := []state.PathRule{ + getPathRules := func(source *v1beta1.HTTPRoute, rootPath bool) []dataplane.PathRule { + rules := []dataplane.PathRule{ { Path: "/path-1", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { Source: source, BackendGroup: fooGroup, @@ -604,7 +605,7 @@ func TestCreateLocationsRootPath(t *testing.T) { }, { Path: "/path-2", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { Source: source, BackendGroup: fooGroup, @@ -616,9 +617,9 @@ func TestCreateLocationsRootPath(t *testing.T) { } if rootPath { - rules = append(rules, state.PathRule{ + rules = append(rules, dataplane.PathRule{ Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { Source: source, BackendGroup: fooGroup, @@ -634,7 +635,7 @@ func TestCreateLocationsRootPath(t *testing.T) { tests := []struct { name string - pathRules []state.PathRule + pathRules []dataplane.PathRule expLocations []http.Location }{ { diff --git a/internal/nginx/config/split_clients.go b/internal/nginx/config/split_clients.go index 93a826cd2..28d39030d 100644 --- a/internal/nginx/config/split_clients.go +++ b/internal/nginx/config/split_clients.go @@ -6,18 +6,19 @@ import ( gotemplate "text/template" "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config/http" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/graph" ) var splitClientsTemplate = gotemplate.Must(gotemplate.New("split_clients").Parse(splitClientsTemplateText)) -func executeSplitClients(conf state.Configuration) []byte { +func executeSplitClients(conf dataplane.Configuration) []byte { splitClients := createSplitClients(conf.BackendGroups) return execute(splitClientsTemplate, splitClients) } -func createSplitClients(backendGroups []state.BackendGroup) []http.SplitClient { +func createSplitClients(backendGroups []graph.BackendGroup) []http.SplitClient { numSplits := 0 for _, group := range backendGroups { if backendGroupNeedsSplit(group) { @@ -48,7 +49,7 @@ func createSplitClients(backendGroups []state.BackendGroup) []http.SplitClient { return splitClients } -func createSplitClientDistributions(group state.BackendGroup) []http.SplitClientDistribution { +func createSplitClientDistributions(group graph.BackendGroup) []http.SplitClientDistribution { if !backendGroupNeedsSplit(group) { return nil } @@ -100,7 +101,7 @@ func createSplitClientDistributions(group state.BackendGroup) []http.SplitClient return distributions } -func getSplitClientValue(b state.BackendRef) string { +func getSplitClientValue(b graph.BackendRef) string { if b.Valid { return b.Name } @@ -117,7 +118,7 @@ func percentOf(weight, totalWeight int32) float64 { return math.Floor(p*100) / 100 } -func backendGroupNeedsSplit(group state.BackendGroup) bool { +func backendGroupNeedsSplit(group graph.BackendGroup) bool { return len(group.Backends) > 1 } @@ -125,7 +126,7 @@ func backendGroupNeedsSplit(group state.BackendGroup) bool { // If the group needs to be split, the name returned is the group name. // If the group doesn't need to be split, the name returned is the name of the backend if it is valid. // If the name cannot be determined, it returns the name of the invalid backend upstream. -func backendGroupName(group state.BackendGroup) string { +func backendGroupName(group graph.BackendGroup) string { switch len(group.Backends) { case 0: return invalidBackendRef diff --git a/internal/nginx/config/split_clients_test.go b/internal/nginx/config/split_clients_test.go index 749d078c5..91b82f807 100644 --- a/internal/nginx/config/split_clients_test.go +++ b/internal/nginx/config/split_clients_test.go @@ -8,31 +8,32 @@ import ( "k8s.io/apimachinery/pkg/types" "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config/http" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/graph" ) func TestExecuteSplitClients(t *testing.T) { - bg1 := state.BackendGroup{ + bg1 := graph.BackendGroup{ Source: types.NamespacedName{Namespace: "test", Name: "hr"}, RuleIdx: 0, - Backends: []state.BackendRef{ + Backends: []graph.BackendRef{ {Name: "test1", Valid: true, Weight: 1}, {Name: "test2", Valid: true, Weight: 1}, }, } - bg2 := state.BackendGroup{ + bg2 := graph.BackendGroup{ Source: types.NamespacedName{Namespace: "test", Name: "no-split"}, RuleIdx: 1, - Backends: []state.BackendRef{ + Backends: []graph.BackendRef{ {Name: "no-split", Valid: true, Weight: 1}, }, } - bg3 := state.BackendGroup{ + bg3 := graph.BackendGroup{ Source: types.NamespacedName{Namespace: "test", Name: "hr"}, RuleIdx: 1, - Backends: []state.BackendRef{ + Backends: []graph.BackendRef{ {Name: "test3", Valid: true, Weight: 1}, {Name: "test4", Valid: true, Weight: 1}, }, @@ -40,13 +41,13 @@ func TestExecuteSplitClients(t *testing.T) { tests := []struct { msg string - backendGroups []state.BackendGroup + backendGroups []graph.BackendGroup expStrings []string notExpStrings []string }{ { msg: "non-zero weights", - backendGroups: []state.BackendGroup{ + backendGroups: []graph.BackendGroup{ bg1, bg2, bg3, @@ -63,11 +64,11 @@ func TestExecuteSplitClients(t *testing.T) { }, { msg: "zero weight", - backendGroups: []state.BackendGroup{ + backendGroups: []graph.BackendGroup{ { Source: types.NamespacedName{Namespace: "test", Name: "zero-percent"}, RuleIdx: 0, - Backends: []state.BackendRef{ + Backends: []graph.BackendRef{ {Name: "non-zero", Valid: true, Weight: 1}, {Name: "zero", Valid: true, Weight: 0}, }, @@ -82,11 +83,11 @@ func TestExecuteSplitClients(t *testing.T) { }, { msg: "no split clients", - backendGroups: []state.BackendGroup{ + backendGroups: []graph.BackendGroup{ { Source: types.NamespacedName{Namespace: "test", Name: "single-backend-route"}, RuleIdx: 0, - Backends: []state.BackendRef{ + Backends: []graph.BackendRef{ {Name: "single-backend", Valid: true, Weight: 1}, }, }, @@ -97,7 +98,7 @@ func TestExecuteSplitClients(t *testing.T) { } for _, test := range tests { - sc := string(executeSplitClients(state.Configuration{BackendGroups: test.backendGroups})) + sc := string(executeSplitClients(dataplane.Configuration{BackendGroups: test.backendGroups})) for _, expSubString := range test.expStrings { if !strings.Contains(sc, expSubString) { @@ -131,9 +132,9 @@ func TestCreateSplitClients(t *testing.T) { createBackendGroup := func( sourceNsName types.NamespacedName, ruleIdx int, - backends ...state.BackendRef, - ) state.BackendGroup { - return state.BackendGroup{ + backends ...graph.BackendRef, + ) graph.BackendGroup { + return graph.BackendGroup{ Source: sourceNsName, RuleIdx: ruleIdx, Backends: backends, @@ -145,46 +146,46 @@ func TestCreateSplitClients(t *testing.T) { oneBackend := createBackendGroup( hrNoSplit, 0, - state.BackendRef{Name: "one-backend", Valid: true, Weight: 1}, + graph.BackendRef{Name: "one-backend", Valid: true, Weight: 1}, ) invalidBackend := createBackendGroup( hrNoSplit, 0, - state.BackendRef{Name: "invalid-backend", Valid: false, Weight: 1}, + graph.BackendRef{Name: "invalid-backend", Valid: false, Weight: 1}, ) // the following backends need splits oneSplit := createBackendGroup( hrOneSplit, 0, - state.BackendRef{Name: "one-split-1", Valid: true, Weight: 50}, - state.BackendRef{Name: "one-split-2", Valid: true, Weight: 50}, + graph.BackendRef{Name: "one-split-1", Valid: true, Weight: 50}, + graph.BackendRef{Name: "one-split-2", Valid: true, Weight: 50}, ) twoSplitGroup0 := createBackendGroup( hrTwoSplits, 0, - state.BackendRef{Name: "two-split-1", Valid: true, Weight: 50}, - state.BackendRef{Name: "two-split-2", Valid: true, Weight: 50}, + graph.BackendRef{Name: "two-split-1", Valid: true, Weight: 50}, + graph.BackendRef{Name: "two-split-2", Valid: true, Weight: 50}, ) twoSplitGroup1 := createBackendGroup( hrTwoSplits, 1, - state.BackendRef{Name: "two-split-3", Valid: true, Weight: 50}, - state.BackendRef{Name: "two-split-4", Valid: true, Weight: 50}, - state.BackendRef{Name: "two-split-5", Valid: true, Weight: 50}, + graph.BackendRef{Name: "two-split-3", Valid: true, Weight: 50}, + graph.BackendRef{Name: "two-split-4", Valid: true, Weight: 50}, + graph.BackendRef{Name: "two-split-5", Valid: true, Weight: 50}, ) tests := []struct { msg string - backendGroups []state.BackendGroup + backendGroups []graph.BackendGroup expSplitClients []http.SplitClient }{ { msg: "normal case", - backendGroups: []state.BackendGroup{ + backendGroups: []graph.BackendGroup{ noBackends, oneBackend, invalidBackend, @@ -240,7 +241,7 @@ func TestCreateSplitClients(t *testing.T) { }, { msg: "no split clients are needed", - backendGroups: []state.BackendGroup{ + backendGroups: []graph.BackendGroup{ noBackends, oneBackend, }, @@ -259,7 +260,7 @@ func TestCreateSplitClients(t *testing.T) { func TestCreateSplitClientDistributions(t *testing.T) { tests := []struct { msg string - backends []state.BackendRef + backends []graph.BackendRef expDistributions []http.SplitClientDistribution }{ { @@ -269,7 +270,7 @@ func TestCreateSplitClientDistributions(t *testing.T) { }, { msg: "one backend", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "one", Valid: true, @@ -280,7 +281,7 @@ func TestCreateSplitClientDistributions(t *testing.T) { }, { msg: "total weight 0", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "one", Valid: true, @@ -301,7 +302,7 @@ func TestCreateSplitClientDistributions(t *testing.T) { }, { msg: "two backends; equal weights that sum to 100", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "one", Valid: true, @@ -326,7 +327,7 @@ func TestCreateSplitClientDistributions(t *testing.T) { }, { msg: "three backends; whole percentages that sum to 100", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "one", Valid: true, @@ -360,7 +361,7 @@ func TestCreateSplitClientDistributions(t *testing.T) { }, { msg: "three backends; whole percentages that sum to less than 100", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "one", Valid: true, @@ -395,7 +396,7 @@ func TestCreateSplitClientDistributions(t *testing.T) { } for _, test := range tests { - result := createSplitClientDistributions(state.BackendGroup{Backends: test.backends}) + result := createSplitClientDistributions(graph.BackendGroup{Backends: test.backends}) if diff := cmp.Diff(test.expDistributions, result); diff != "" { t.Errorf("createSplitClientDistributions() mismatch for %q (-want +got):\n%s", test.msg, diff) } @@ -406,11 +407,11 @@ func TestGetSplitClientValue(t *testing.T) { tests := []struct { msg string expValue string - backend state.BackendRef + backend graph.BackendRef }{ { msg: "valid backend", - backend: state.BackendRef{ + backend: graph.BackendRef{ Name: "valid", Valid: true, }, @@ -418,7 +419,7 @@ func TestGetSplitClientValue(t *testing.T) { }, { msg: "invalid backend", - backend: state.BackendRef{ + backend: graph.BackendRef{ Name: "invalid", Valid: false, }, @@ -514,12 +515,12 @@ func TestPercentOf(t *testing.T) { func TestBackendGroupNeedsSplit(t *testing.T) { tests := []struct { msg string - backends []state.BackendRef + backends []graph.BackendRef expSplit bool }{ { msg: "empty backends", - backends: []state.BackendRef{}, + backends: []graph.BackendRef{}, expSplit: false, }, { @@ -529,7 +530,7 @@ func TestBackendGroupNeedsSplit(t *testing.T) { }, { msg: "one valid backend", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "backend1", Valid: true, @@ -540,7 +541,7 @@ func TestBackendGroupNeedsSplit(t *testing.T) { }, { msg: "one invalid backend", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "backend1", Valid: false, @@ -551,7 +552,7 @@ func TestBackendGroupNeedsSplit(t *testing.T) { }, { msg: "multiple valid backends", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "backend1", Valid: true, @@ -567,7 +568,7 @@ func TestBackendGroupNeedsSplit(t *testing.T) { }, { msg: "multiple backends - one invalid", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "backend1", Valid: true, @@ -584,7 +585,7 @@ func TestBackendGroupNeedsSplit(t *testing.T) { } for _, test := range tests { - bg := state.BackendGroup{ + bg := graph.BackendGroup{ Source: types.NamespacedName{Namespace: "test", Name: "hr"}, Backends: test.backends, } @@ -599,11 +600,11 @@ func TestBackendGroupName(t *testing.T) { tests := []struct { msg string expName string - backends []state.BackendRef + backends []graph.BackendRef }{ { msg: "empty backends", - backends: []state.BackendRef{}, + backends: []graph.BackendRef{}, expName: invalidBackendRef, }, { @@ -613,7 +614,7 @@ func TestBackendGroupName(t *testing.T) { }, { msg: "one valid backend with non-zero weight", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "backend1", Valid: true, @@ -624,7 +625,7 @@ func TestBackendGroupName(t *testing.T) { }, { msg: "one valid backend with zero weight", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "backend1", Valid: true, @@ -635,7 +636,7 @@ func TestBackendGroupName(t *testing.T) { }, { msg: "one invalid backend", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "backend1", Valid: false, @@ -646,7 +647,7 @@ func TestBackendGroupName(t *testing.T) { }, { msg: "multiple valid backends", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "backend1", Valid: true, @@ -662,7 +663,7 @@ func TestBackendGroupName(t *testing.T) { }, { msg: "multiple invalid backends", - backends: []state.BackendRef{ + backends: []graph.BackendRef{ { Name: "backend1", Valid: false, @@ -679,7 +680,7 @@ func TestBackendGroupName(t *testing.T) { } for _, test := range tests { - bg := state.BackendGroup{ + bg := graph.BackendGroup{ Source: types.NamespacedName{Namespace: "test", Name: "hr"}, RuleIdx: 0, Backends: test.backends, diff --git a/internal/nginx/config/upstreams.go b/internal/nginx/config/upstreams.go index c7fdbbf84..927745ed5 100644 --- a/internal/nginx/config/upstreams.go +++ b/internal/nginx/config/upstreams.go @@ -5,7 +5,7 @@ import ( gotemplate "text/template" "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config/http" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" ) var upstreamsTemplate = gotemplate.Must(gotemplate.New("upstreams").Parse(upstreamsTemplateText)) @@ -19,13 +19,13 @@ const ( invalidBackendRef = "invalid-backend-ref" ) -func executeUpstreams(conf state.Configuration) []byte { +func executeUpstreams(conf dataplane.Configuration) []byte { upstreams := createUpstreams(conf.Upstreams) return execute(upstreamsTemplate, upstreams) } -func createUpstreams(upstreams []state.Upstream) []http.Upstream { +func createUpstreams(upstreams []dataplane.Upstream) []http.Upstream { // capacity is the number of upstreams + 1 for the invalid backend ref upstream ups := make([]http.Upstream, 0, len(upstreams)+1) @@ -38,7 +38,7 @@ func createUpstreams(upstreams []state.Upstream) []http.Upstream { return ups } -func createUpstream(up state.Upstream) http.Upstream { +func createUpstream(up dataplane.Upstream) http.Upstream { if len(up.Endpoints) == 0 { return http.Upstream{ Name: up.Name, diff --git a/internal/nginx/config/upstreams_test.go b/internal/nginx/config/upstreams_test.go index 3713d16a9..95cb9bfe6 100644 --- a/internal/nginx/config/upstreams_test.go +++ b/internal/nginx/config/upstreams_test.go @@ -7,12 +7,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config/http" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/resolver" ) func TestExecuteUpstreams(t *testing.T) { - stateUpstreams := []state.Upstream{ + stateUpstreams := []dataplane.Upstream{ { Name: "up1", Endpoints: []resolver.Endpoint{ @@ -47,7 +47,7 @@ func TestExecuteUpstreams(t *testing.T) { "server unix:/var/lib/nginx/nginx-502-server.sock;", } - upstreams := string(executeUpstreams(state.Configuration{Upstreams: stateUpstreams})) + upstreams := string(executeUpstreams(dataplane.Configuration{Upstreams: stateUpstreams})) for _, expSubString := range expectedSubStrings { if !strings.Contains(upstreams, expSubString) { t.Errorf( @@ -60,7 +60,7 @@ func TestExecuteUpstreams(t *testing.T) { } func TestCreateUpstreams(t *testing.T) { - stateUpstreams := []state.Upstream{ + stateUpstreams := []dataplane.Upstream{ { Name: "up1", Endpoints: []resolver.Endpoint{ @@ -143,11 +143,11 @@ func TestCreateUpstreams(t *testing.T) { func TestCreateUpstream(t *testing.T) { tests := []struct { msg string - stateUpstream state.Upstream + stateUpstream dataplane.Upstream expectedUpstream http.Upstream }{ { - stateUpstream: state.Upstream{ + stateUpstream: dataplane.Upstream{ Name: "nil-endpoints", Endpoints: nil, }, @@ -162,7 +162,7 @@ func TestCreateUpstream(t *testing.T) { msg: "nil endpoints", }, { - stateUpstream: state.Upstream{ + stateUpstream: dataplane.Upstream{ Name: "no-endpoints", Endpoints: []resolver.Endpoint{}, }, @@ -177,7 +177,7 @@ func TestCreateUpstream(t *testing.T) { msg: "no endpoints", }, { - stateUpstream: state.Upstream{ + stateUpstream: dataplane.Upstream{ Name: "multiple-endpoints", Endpoints: []resolver.Endpoint{ { diff --git a/internal/sort/doc.go b/internal/sort/doc.go new file mode 100644 index 000000000..aa748e1bd --- /dev/null +++ b/internal/sort/doc.go @@ -0,0 +1,2 @@ +// Package sort provides utilities for sorting Kubernetes resources. +package sort diff --git a/internal/sort/sort.go b/internal/sort/sort.go new file mode 100644 index 000000000..ee9db1b24 --- /dev/null +++ b/internal/sort/sort.go @@ -0,0 +1,16 @@ +package sort + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// LessObjectMeta compares two ObjectMetas according to the Gateway API conflict resolution guidelines. +// See https://gateway-api.sigs.k8s.io/concepts/guidelines/?h=conflict#conflicts +func LessObjectMeta(meta1 *metav1.ObjectMeta, meta2 *metav1.ObjectMeta) bool { + if meta1.CreationTimestamp.Equal(&meta2.CreationTimestamp) { + if meta1.Namespace == meta2.Namespace { + return meta1.Name < meta2.Name + } + return meta1.Namespace < meta2.Namespace + } + + return meta1.CreationTimestamp.Before(&meta2.CreationTimestamp) +} diff --git a/internal/sort/sort_test.go b/internal/sort/sort_test.go new file mode 100644 index 000000000..c5fbb5975 --- /dev/null +++ b/internal/sort/sort_test.go @@ -0,0 +1,91 @@ +package sort + +import ( + "testing" + "time" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestLessObjectMeta(t *testing.T) { + before := metav1.Now() + later := metav1.NewTime(before.Add(1 * time.Second)) + + tests := []struct { + meta1 *metav1.ObjectMeta + meta2 *metav1.ObjectMeta + name string + expected bool + }{ + { + meta1: &metav1.ObjectMeta{ + Namespace: "ns1", + Name: "meta1", + CreationTimestamp: before, + }, + meta2: &metav1.ObjectMeta{ + Namespace: "ns1", + Name: "meta2", + UID: "b", + CreationTimestamp: later, + }, + name: "first is less by timestamp", + expected: true, + }, + { + meta1: &metav1.ObjectMeta{ + Namespace: "ns1", + Name: "meta1", + CreationTimestamp: before, + }, + meta2: &metav1.ObjectMeta{ + Namespace: "ns2", + Name: "meta2", + CreationTimestamp: before, + }, + name: "first is less by namespace", + expected: true, + }, + { + meta1: &metav1.ObjectMeta{ + Namespace: "ns1", + Name: "meta1", + CreationTimestamp: before, + }, + meta2: &metav1.ObjectMeta{ + Namespace: "ns1", + Name: "meta2", + CreationTimestamp: before, + }, + name: "first is less by name", + expected: true, + }, + { + meta1: &metav1.ObjectMeta{ + Namespace: "ns1", + Name: "meta1", + CreationTimestamp: before, + }, + meta2: &metav1.ObjectMeta{ + Namespace: "ns1", + Name: "meta1", + CreationTimestamp: before, + }, + name: "equal", + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + result := LessObjectMeta(test.meta1, test.meta2) + invertedResult := LessObjectMeta(test.meta2, test.meta1) + + g.Expect(result).To(Equal(test.expected)) + g.Expect(invertedResult).To(Equal(false)) + }) + } +} diff --git a/internal/state/change_processor.go b/internal/state/change_processor.go index 4f21af566..eb87a0a4d 100644 --- a/internal/state/change_processor.go +++ b/internal/state/change_processor.go @@ -12,8 +12,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/gateway-api/apis/v1beta1" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/graph" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/relationship" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/resolver" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets" ) //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . ChangeProcessor @@ -33,7 +36,7 @@ type ChangeProcessor interface { // the status information about the processed resources. // If no changes were captured, the changed return argument will be false and both the configuration and statuses // will be empty. - Process(ctx context.Context) (changed bool, conf Configuration, statuses Statuses) + Process(ctx context.Context) (changed bool, conf dataplane.Configuration, statuses Statuses) } // ChangeProcessorConfig holds configuration parameters for ChangeProcessorImpl. @@ -43,7 +46,7 @@ type ChangeProcessorConfig struct { // GatewayClassName is the name of the GatewayClass resource. GatewayClassName string // SecretMemoryManager is the secret memory manager. - SecretMemoryManager SecretDiskMemoryManager + SecretMemoryManager secrets.SecretDiskMemoryManager // ServiceResolver resolves Services to Endpoints. ServiceResolver resolver.ServiceResolver // RelationshipCapturer captures relationships between Kubernetes API resources and Gateway API resources. @@ -136,7 +139,9 @@ func (c *ChangeProcessorImpl) CaptureDeleteChange(resourceType client.Object, ns c.cfg.RelationshipCapturer.Remove(resourceType, nsname) } -func (c *ChangeProcessorImpl) Process(ctx context.Context) (changed bool, conf Configuration, statuses Statuses) { +func (c *ChangeProcessorImpl) Process( + ctx context.Context, +) (changed bool, conf dataplane.Configuration, statuses Statuses) { c.lock.Lock() defer c.lock.Unlock() @@ -147,20 +152,25 @@ func (c *ChangeProcessorImpl) Process(ctx context.Context) (changed bool, conf C c.store.changed = false c.changed = false - graph := buildGraph( - c.store, + g := graph.BuildGraph( + graph.ClusterStore{ + GatewayClass: c.store.gc, + Gateways: c.store.gateways, + HTTPRoutes: c.store.httpRoutes, + Services: c.store.services, + }, c.cfg.GatewayCtlrName, c.cfg.GatewayClassName, c.cfg.SecretMemoryManager, ) - var warnings Warnings - conf, warnings = buildConfiguration(ctx, graph, c.cfg.ServiceResolver) + var warnings dataplane.Warnings + conf, warnings = dataplane.BuildConfiguration(ctx, g, c.cfg.ServiceResolver) for obj, objWarnings := range warnings { for _, w := range objWarnings { // FIXME(pleshakov): report warnings via Object status - c.cfg.Logger.Info("Got warning while building graph", + c.cfg.Logger.Info("Got warning while building Graph", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "namespace", obj.GetNamespace(), "name", obj.GetName(), @@ -168,7 +178,7 @@ func (c *ChangeProcessorImpl) Process(ctx context.Context) (changed bool, conf C } } - statuses = buildStatuses(graph) + statuses = buildStatuses(g) return true, conf, statuses } diff --git a/internal/state/change_processor_test.go b/internal/state/change_processor_test.go index 6dff58a11..db0da9332 100644 --- a/internal/state/change_processor_test.go +++ b/internal/state/change_processor_test.go @@ -19,9 +19,11 @@ import ( "github.com/nginxinc/nginx-kubernetes-gateway/internal/manager/index" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/conditions" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/graph" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/relationship" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/relationship/relationshipfakes" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/statefakes" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets/secretsfakes" ) const ( @@ -178,11 +180,11 @@ var _ = Describe("ChangeProcessor", func() { }, } processor state.ChangeProcessor - fakeSecretMemoryMgr *statefakes.FakeSecretDiskMemoryManager + fakeSecretMemoryMgr *secretsfakes.FakeSecretDiskMemoryManager ) BeforeEach(OncePerOrdered, func() { - fakeSecretMemoryMgr = &statefakes.FakeSecretDiskMemoryManager{} + fakeSecretMemoryMgr = &secretsfakes.FakeSecretDiskMemoryManager{} processor = state.NewChangeProcessorImpl(state.ChangeProcessorConfig{ GatewayCtlrName: controllerName, @@ -199,7 +201,7 @@ var _ = Describe("ChangeProcessor", func() { var ( gcUpdated *v1beta1.GatewayClass hr1, hr1Updated, hr2 *v1beta1.HTTPRoute - hr1Group, hr2Group state.BackendGroup + hr1Group, hr2Group graph.BackendGroup gw1, gw1Updated, gw2 *v1beta1.Gateway ) BeforeAll(func() { @@ -208,7 +210,7 @@ var _ = Describe("ChangeProcessor", func() { hr1 = createRoute("hr-1", "gateway-1", "foo.example.com") - hr1Group = state.BackendGroup{ + hr1Group = graph.BackendGroup{ Source: types.NamespacedName{Namespace: hr1.Namespace, Name: hr1.Name}, RuleIdx: 0, } @@ -218,7 +220,7 @@ var _ = Describe("ChangeProcessor", func() { hr2 = createRoute("hr-2", "gateway-2", "bar.example.com") - hr2Group = state.BackendGroup{ + hr2Group = graph.BackendGroup{ Source: types.NamespacedName{Namespace: hr2.Namespace, Name: hr2.Name}, RuleIdx: 0, } @@ -262,7 +264,7 @@ var _ = Describe("ChangeProcessor", func() { It("returns empty configuration and statuses", func() { processor.CaptureUpsertChange(hr1) - expectedConf := state.Configuration{} + expectedConf := dataplane.Configuration{} expectedStatuses := state.Statuses{ IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{}, HTTPRouteStatuses: map[types.NamespacedName]state.HTTPRouteStatus{}, @@ -279,7 +281,7 @@ var _ = Describe("ChangeProcessor", func() { It("returns empty configuration and updated statuses", func() { processor.CaptureUpsertChange(gw1) - expectedConf := state.Configuration{} + expectedConf := dataplane.Configuration{} expectedStatuses := state.Statuses{ GatewayStatus: &state.GatewayStatus{ NsName: types.NamespacedName{Namespace: "test", Name: "gateway-1"}, @@ -334,17 +336,17 @@ var _ = Describe("ChangeProcessor", func() { It("returns updated configuration and statuses", func() { processor.CaptureUpsertChange(gc) - expectedConf := state.Configuration{ - HTTPServers: []state.VirtualServer{ + expectedConf := dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "foo.example.com", - PathRules: []state.PathRule{ + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -356,17 +358,17 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - SSLServers: []state.VirtualServer{ + SSLServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "foo.example.com", - SSL: &state.SSL{CertificatePath: certificatePath}, - PathRules: []state.PathRule{ + SSL: &dataplane.SSL{CertificatePath: certificatePath}, + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -379,10 +381,10 @@ var _ = Describe("ChangeProcessor", func() { }, { Hostname: "~^", - SSL: &state.SSL{CertificatePath: certificatePath}, + SSL: &dataplane.SSL{CertificatePath: certificatePath}, }, }, - BackendGroups: []state.BackendGroup{ + BackendGroups: []graph.BackendGroup{ hr1Group, }, } @@ -444,17 +446,17 @@ var _ = Describe("ChangeProcessor", func() { It("returns updated configuration and statuses", func() { processor.CaptureUpsertChange(hr1Updated) - expectedConf := state.Configuration{ - HTTPServers: []state.VirtualServer{ + expectedConf := dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "foo.example.com", - PathRules: []state.PathRule{ + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -466,17 +468,17 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - SSLServers: []state.VirtualServer{ + SSLServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "foo.example.com", - SSL: &state.SSL{CertificatePath: certificatePath}, - PathRules: []state.PathRule{ + SSL: &dataplane.SSL{CertificatePath: certificatePath}, + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -489,10 +491,10 @@ var _ = Describe("ChangeProcessor", func() { }, { Hostname: "~^", - SSL: &state.SSL{CertificatePath: certificatePath}, + SSL: &dataplane.SSL{CertificatePath: certificatePath}, }, }, - BackendGroups: []state.BackendGroup{ + BackendGroups: []graph.BackendGroup{ hr1Group, }, } @@ -554,17 +556,17 @@ var _ = Describe("ChangeProcessor", func() { It("returns updated configuration and statuses", func() { processor.CaptureUpsertChange(gw1Updated) - expectedConf := state.Configuration{ - HTTPServers: []state.VirtualServer{ + expectedConf := dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "foo.example.com", - PathRules: []state.PathRule{ + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -576,17 +578,17 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - SSLServers: []state.VirtualServer{ + SSLServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "foo.example.com", - SSL: &state.SSL{CertificatePath: certificatePath}, - PathRules: []state.PathRule{ + SSL: &dataplane.SSL{CertificatePath: certificatePath}, + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -599,10 +601,10 @@ var _ = Describe("ChangeProcessor", func() { }, { Hostname: "~^", - SSL: &state.SSL{CertificatePath: certificatePath}, + SSL: &dataplane.SSL{CertificatePath: certificatePath}, }, }, - BackendGroups: []state.BackendGroup{ + BackendGroups: []graph.BackendGroup{ hr1Group, }, } @@ -663,17 +665,17 @@ var _ = Describe("ChangeProcessor", func() { It("returns updated configuration and statuses", func() { processor.CaptureUpsertChange(gcUpdated) - expectedConf := state.Configuration{ - HTTPServers: []state.VirtualServer{ + expectedConf := dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "foo.example.com", - PathRules: []state.PathRule{ + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -685,17 +687,17 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - SSLServers: []state.VirtualServer{ + SSLServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "foo.example.com", - SSL: &state.SSL{CertificatePath: certificatePath}, - PathRules: []state.PathRule{ + SSL: &dataplane.SSL{CertificatePath: certificatePath}, + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -708,10 +710,10 @@ var _ = Describe("ChangeProcessor", func() { }, { Hostname: "~^", - SSL: &state.SSL{CertificatePath: certificatePath}, + SSL: &dataplane.SSL{CertificatePath: certificatePath}, }, }, - BackendGroups: []state.BackendGroup{ + BackendGroups: []graph.BackendGroup{ hr1Group, }, } @@ -769,17 +771,17 @@ var _ = Describe("ChangeProcessor", func() { It("returns updated configuration and statuses", func() { processor.CaptureUpsertChange(gw2) - expectedConf := state.Configuration{ - HTTPServers: []state.VirtualServer{ + expectedConf := dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "foo.example.com", - PathRules: []state.PathRule{ + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -791,16 +793,16 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - SSLServers: []state.VirtualServer{ + SSLServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "foo.example.com", - PathRules: []state.PathRule{ + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -810,16 +812,16 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - SSL: &state.SSL{ + SSL: &dataplane.SSL{ CertificatePath: certificatePath, }, }, { Hostname: "~^", - SSL: &state.SSL{CertificatePath: certificatePath}, + SSL: &dataplane.SSL{CertificatePath: certificatePath}, }, }, - BackendGroups: []state.BackendGroup{ + BackendGroups: []graph.BackendGroup{ hr1Group, }, } @@ -872,17 +874,17 @@ var _ = Describe("ChangeProcessor", func() { It("returns same configuration and updated statuses", func() { processor.CaptureUpsertChange(hr2) - expectedConf := state.Configuration{ - HTTPServers: []state.VirtualServer{ + expectedConf := dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "foo.example.com", - PathRules: []state.PathRule{ + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -894,17 +896,17 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - SSLServers: []state.VirtualServer{ + SSLServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "foo.example.com", - SSL: &state.SSL{CertificatePath: certificatePath}, - PathRules: []state.PathRule{ + SSL: &dataplane.SSL{CertificatePath: certificatePath}, + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -917,10 +919,10 @@ var _ = Describe("ChangeProcessor", func() { }, { Hostname: "~^", - SSL: &state.SSL{CertificatePath: certificatePath}, + SSL: &dataplane.SSL{CertificatePath: certificatePath}, }, }, - BackendGroups: []state.BackendGroup{ + BackendGroups: []graph.BackendGroup{ hr1Group, }, } @@ -993,17 +995,17 @@ var _ = Describe("ChangeProcessor", func() { types.NamespacedName{Namespace: "test", Name: "gateway-1"}, ) - expectedConf := state.Configuration{ - HTTPServers: []state.VirtualServer{ + expectedConf := dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "bar.example.com", - PathRules: []state.PathRule{ + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -1015,17 +1017,17 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - SSLServers: []state.VirtualServer{ + SSLServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "bar.example.com", - SSL: &state.SSL{CertificatePath: certificatePath}, - PathRules: []state.PathRule{ + SSL: &dataplane.SSL{CertificatePath: certificatePath}, + PathRules: []dataplane.PathRule{ { Path: "/", - MatchRules: []state.MatchRule{ + MatchRules: []dataplane.MatchRule{ { MatchIdx: 0, RuleIdx: 0, @@ -1038,10 +1040,10 @@ var _ = Describe("ChangeProcessor", func() { }, { Hostname: "~^", - SSL: &state.SSL{CertificatePath: certificatePath}, + SSL: &dataplane.SSL{CertificatePath: certificatePath}, }, }, - BackendGroups: []state.BackendGroup{ + BackendGroups: []graph.BackendGroup{ hr2Group, }, } @@ -1093,19 +1095,19 @@ var _ = Describe("ChangeProcessor", func() { types.NamespacedName{Namespace: "test", Name: "hr-2"}, ) - expectedConf := state.Configuration{ - HTTPServers: []state.VirtualServer{ + expectedConf := dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ { IsDefault: true, }, }, - SSLServers: []state.VirtualServer{ + SSLServers: []dataplane.VirtualServer{ { IsDefault: true, }, { Hostname: "~^", - SSL: &state.SSL{CertificatePath: certificatePath}, + SSL: &dataplane.SSL{CertificatePath: certificatePath}, }, }, } @@ -1145,7 +1147,7 @@ var _ = Describe("ChangeProcessor", func() { types.NamespacedName{Name: gcName}, ) - expectedConf := state.Configuration{} + expectedConf := dataplane.Configuration{} expectedStatuses := state.Statuses{ GatewayStatus: &state.GatewayStatus{ NsName: types.NamespacedName{Namespace: "test", Name: "gateway-2"}, @@ -1184,7 +1186,7 @@ var _ = Describe("ChangeProcessor", func() { types.NamespacedName{Namespace: "test", Name: "gateway-2"}, ) - expectedConf := state.Configuration{} + expectedConf := dataplane.Configuration{} expectedStatuses := state.Statuses{ IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{}, HTTPRouteStatuses: map[types.NamespacedName]state.HTTPRouteStatus{}, @@ -1203,7 +1205,7 @@ var _ = Describe("ChangeProcessor", func() { types.NamespacedName{Namespace: "test", Name: "hr-1"}, ) - expectedConf := state.Configuration{} + expectedConf := dataplane.Configuration{} expectedStatuses := state.Statuses{ IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{}, HTTPRouteStatuses: map[types.NamespacedName]state.HTTPRouteStatus{}, @@ -1567,7 +1569,7 @@ var _ = Describe("ChangeProcessor", func() { ) BeforeEach(OncePerOrdered, func() { - fakeSecretMemoryMgr := &statefakes.FakeSecretDiskMemoryManager{} + fakeSecretMemoryMgr := &secretsfakes.FakeSecretDiskMemoryManager{} fakeRelationshipCapturer = &relationshipfakes.FakeCapturer{} processor = state.NewChangeProcessorImpl(state.ChangeProcessorConfig{ @@ -1864,12 +1866,12 @@ var _ = Describe("ChangeProcessor", func() { Describe("Edge cases with panic", func() { var ( processor state.ChangeProcessor - fakeSecretMemoryMgr *statefakes.FakeSecretDiskMemoryManager + fakeSecretMemoryMgr *secretsfakes.FakeSecretDiskMemoryManager fakeRelationshipCapturer *relationshipfakes.FakeCapturer ) BeforeEach(func() { - fakeSecretMemoryMgr = &statefakes.FakeSecretDiskMemoryManager{} + fakeSecretMemoryMgr = &secretsfakes.FakeSecretDiskMemoryManager{} fakeRelationshipCapturer = &relationshipfakes.FakeCapturer{} processor = state.NewChangeProcessorImpl(state.ChangeProcessorConfig{ diff --git a/internal/state/configuration.go b/internal/state/dataplane/configuration.go similarity index 87% rename from internal/state/configuration.go rename to internal/state/dataplane/configuration.go index 41728cb30..7251d824d 100644 --- a/internal/state/configuration.go +++ b/internal/state/dataplane/configuration.go @@ -1,4 +1,4 @@ -package state +package dataplane import ( "context" @@ -7,14 +7,13 @@ import ( "sigs.k8s.io/gateway-api/apis/v1beta1" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/graph" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/resolver" ) 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. +// Configuration is an intermediate representation of dataplane configuration. type Configuration struct { // HTTPServers holds all HTTPServers. // FIXME(pleshakov) We assume that all servers are HTTP and listen on port 80. @@ -25,7 +24,8 @@ type Configuration struct { // Upstreams holds all unique Upstreams. Upstreams []Upstream // BackendGroups holds all unique BackendGroups. - BackendGroups []BackendGroup + // FIXME(pleshakov): Ensure Configuration doesn't include types from the graph package. + BackendGroups []graph.BackendGroup } // VirtualServer is a virtual server. @@ -79,7 +79,7 @@ type MatchRule struct { // the entire resource. Source *v1beta1.HTTPRoute // BackendGroup is the group of Backends that the rule routes to. - BackendGroup BackendGroup + BackendGroup graph.BackendGroup // MatchIdx is the index of the rule in the Rule.Matches. MatchIdx int // RuleIdx is the index of the corresponding rule in the HTTPRoute. @@ -91,26 +91,26 @@ func (r *MatchRule) GetMatch() v1beta1.HTTPRouteMatch { return r.Source.Spec.Rules[r.RuleIdx].Matches[r.MatchIdx] } -// buildConfiguration builds the Configuration from the graph. +// BuildConfiguration builds the Configuration from the Graph. // FIXME(pleshakov) For now we only handle paths with prefix matches. Handle exact and regex matches -func buildConfiguration( +func BuildConfiguration( ctx context.Context, - graph *graph, + g *graph.Graph, resolver resolver.ServiceResolver, ) (Configuration, Warnings) { - if graph.GatewayClass == nil || !graph.GatewayClass.Valid { + if g.GatewayClass == nil || !g.GatewayClass.Valid { return Configuration{}, nil } - if graph.Gateway == nil { + if g.Gateway == nil { return Configuration{}, nil } - upstreamsMap := buildUpstreamsMap(ctx, graph.Gateway.Listeners, resolver) - httpServers, sslServers := buildServers(graph.Gateway.Listeners) - backendGroups := buildBackendGroups(graph.Gateway.Listeners) + upstreamsMap := buildUpstreamsMap(ctx, g.Gateway.Listeners, resolver) + httpServers, sslServers := buildServers(g.Gateway.Listeners) + backendGroups := buildBackendGroups(g.Gateway.Listeners) - warnings := buildWarnings(graph, upstreamsMap) + warnings := buildWarnings(g, upstreamsMap) config := Configuration{ HTTPServers: httpServers, @@ -135,7 +135,7 @@ func upstreamsMapToSlice(upstreamsMap map[string]Upstream) []Upstream { return upstreams } -func buildWarnings(graph *graph, upstreams map[string]Upstream) Warnings { +func buildWarnings(graph *graph.Graph, upstreams map[string]Upstream) Warnings { warnings := newWarnings() for _, l := range graph.Gateway.Listeners { @@ -189,10 +189,10 @@ func buildWarnings(graph *graph, upstreams map[string]Upstream) Warnings { return warnings } -func buildBackendGroups(listeners map[string]*listener) []BackendGroup { +func buildBackendGroups(listeners map[string]*graph.Listener) []graph.BackendGroup { // There can be duplicate backend groups if a route is attached to multiple listeners. // We use a map to deduplicate them. - uniqueGroups := make(map[string]BackendGroup) + uniqueGroups := make(map[string]graph.BackendGroup) for _, l := range listeners { @@ -215,7 +215,7 @@ func buildBackendGroups(listeners map[string]*listener) []BackendGroup { return nil } - groups := make([]BackendGroup, 0, numGroups) + groups := make([]graph.BackendGroup, 0, numGroups) for _, group := range uniqueGroups { groups = append(groups, group) } @@ -223,7 +223,7 @@ func buildBackendGroups(listeners map[string]*listener) []BackendGroup { return groups } -func buildServers(listeners map[string]*listener) (http, ssl []VirtualServer) { +func buildServers(listeners map[string]*graph.Listener) (http, ssl []VirtualServer) { rulesForProtocol := map[v1beta1.ProtocolType]*hostPathRules{ v1beta1.HTTPProtocolType: newHostPathRules(), v1beta1.HTTPSProtocolType: newHostPathRules(), @@ -244,20 +244,20 @@ func buildServers(listeners map[string]*listener) (http, ssl []VirtualServer) { type hostPathRules struct { rulesPerHost map[string]map[string]PathRule - listenersForHost map[string]*listener - httpsListeners []*listener + listenersForHost map[string]*graph.Listener + httpsListeners []*graph.Listener listenersExist bool } func newHostPathRules() *hostPathRules { return &hostPathRules{ rulesPerHost: make(map[string]map[string]PathRule), - listenersForHost: make(map[string]*listener), - httpsListeners: make([]*listener, 0), + listenersForHost: make(map[string]*graph.Listener), + httpsListeners: make([]*graph.Listener, 0), } } -func (hpr *hostPathRules) upsertListener(l *listener) { +func (hpr *hostPathRules) upsertListener(l *graph.Listener) { hpr.listenersExist = true if l.Source.Protocol == v1beta1.HTTPSProtocolType { @@ -373,7 +373,7 @@ func (hpr *hostPathRules) buildServers() []VirtualServer { func buildUpstreamsMap( ctx context.Context, - listeners map[string]*listener, + listeners map[string]*graph.Listener, resolver resolver.ServiceResolver, ) map[string]Upstream { // There can be duplicate upstreams if multiple routes reference the same upstream. @@ -418,12 +418,11 @@ func buildUpstreamsMap( } func getListenerHostname(h *v1beta1.Hostname) string { - name := getHostname(h) - if name == "" { + if h == nil || *h == "" { return wildcardHostname } - return name + return string(*h) } func getPath(path *v1beta1.HTTPPathMatch) string { diff --git a/internal/state/configuration_test.go b/internal/state/dataplane/configuration_test.go similarity index 84% rename from internal/state/configuration_test.go rename to internal/state/dataplane/configuration_test.go index b5ad79637..6f024b068 100644 --- a/internal/state/configuration_test.go +++ b/internal/state/dataplane/configuration_test.go @@ -1,4 +1,4 @@ -package state +package dataplane import ( "context" @@ -15,6 +15,7 @@ import ( "github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/conditions" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/graph" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/resolver" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/resolver/resolverfakes" ) @@ -82,11 +83,11 @@ func TestBuildConfiguration(t *testing.T) { fakeResolver := &resolverfakes.FakeServiceResolver{} fakeResolver.ResolveReturns(fooEndpoints, nil) - createBackendGroup := func(nsname types.NamespacedName, idx int) BackendGroup { - return BackendGroup{ + createBackendGroup := func(nsname types.NamespacedName, idx int) graph.BackendGroup { + return graph.BackendGroup{ Source: nsname, RuleIdx: idx, - Backends: []BackendRef{ + Backends: []graph.BackendRef{ { Name: fooUpstreamName, Svc: fooSvc, @@ -98,8 +99,12 @@ func TestBuildConfiguration(t *testing.T) { } } - createInternalRoute := func(source *v1beta1.HTTPRoute, validSectionName string, groups ...BackendGroup) *route { - r := &route{ + createInternalRoute := func( + source *v1beta1.HTTPRoute, + validSectionName string, + groups ...graph.BackendGroup, + ) *graph.Route { + r := &graph.Route{ Source: source, InvalidSectionNameRefs: make(map[string]conditions.Condition), ValidSectionNameRefs: map[string]struct{}{validSectionName: {}}, @@ -109,10 +114,10 @@ func TestBuildConfiguration(t *testing.T) { } createTestResources := func(name, hostname, listenerName string, paths ...string) ( - *v1beta1.HTTPRoute, []BackendGroup, *route, + *v1beta1.HTTPRoute, []graph.BackendGroup, *graph.Route, ) { hr := createRoute(name, hostname, listenerName, paths...) - groups := make([]BackendGroup, 0, len(paths)) + groups := make([]graph.BackendGroup, 0, len(paths)) for idx := range paths { groups = append(groups, createBackendGroup(types.NamespacedName{Namespace: "test", Name: name}, idx)) } @@ -173,16 +178,16 @@ func TestBuildConfiguration(t *testing.T) { []v1beta1.HTTPRouteFilter{redirect}, ) - hr5BackendGroup := BackendGroup{ + hr5BackendGroup := graph.BackendGroup{ Source: types.NamespacedName{Namespace: hr5.Namespace, Name: hr5.Name}, RuleIdx: 0, } - routeHR5 := &route{ + routeHR5 := &graph.Route{ Source: hr5, InvalidSectionNameRefs: make(map[string]conditions.Condition), ValidSectionNameRefs: map[string]struct{}{"listener-80-1": {}}, - BackendGroups: []BackendGroup{hr5BackendGroup}, + BackendGroups: []graph.BackendGroup{hr5BackendGroup}, } listener80 := v1beta1.Listener{ @@ -239,22 +244,22 @@ func TestBuildConfiguration(t *testing.T) { secretPath := "/etc/nginx/secrets/secret" tests := []struct { - graph *graph + graph *graph.Graph expWarns Warnings msg string expConf Configuration }{ { - graph: &graph{ - GatewayClass: &gatewayClass{ + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ Source: &v1beta1.GatewayClass{}, Valid: true, }, - Gateway: &gateway{ + Gateway: &graph.Gateway{ Source: &v1beta1.Gateway{}, - Listeners: map[string]*listener{}, + Listeners: map[string]*graph.Listener{}, }, - Routes: map[types.NamespacedName]*route{}, + Routes: map[types.NamespacedName]*graph.Route{}, }, expConf: Configuration{ HTTPServers: []VirtualServer{}, @@ -263,23 +268,23 @@ func TestBuildConfiguration(t *testing.T) { msg: "no listeners and routes", }, { - graph: &graph{ - GatewayClass: &gatewayClass{ + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ Source: &v1beta1.GatewayClass{}, Valid: true, }, - Gateway: &gateway{ + Gateway: &graph.Gateway{ Source: &v1beta1.Gateway{}, - Listeners: map[string]*listener{ + Listeners: map[string]*graph.Listener{ "listener-80-1": { Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*route{}, + Routes: map[types.NamespacedName]*graph.Route{}, AcceptedHostnames: map[string]struct{}{}, }, }, }, - Routes: map[types.NamespacedName]*route{}, + Routes: map[types.NamespacedName]*graph.Route{}, }, expConf: Configuration{ HTTPServers: []VirtualServer{ @@ -292,31 +297,31 @@ func TestBuildConfiguration(t *testing.T) { msg: "http listener with no routes", }, { - graph: &graph{ - GatewayClass: &gatewayClass{ + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ Source: &v1beta1.GatewayClass{}, Valid: true, }, - Gateway: &gateway{ + Gateway: &graph.Gateway{ Source: &v1beta1.Gateway{}, - Listeners: map[string]*listener{ + Listeners: map[string]*graph.Listener{ "listener-443-1": { Source: listener443, // nil hostname Valid: true, - Routes: map[types.NamespacedName]*route{}, + Routes: map[types.NamespacedName]*graph.Route{}, AcceptedHostnames: map[string]struct{}{}, SecretPath: secretPath, }, "listener-443-with-hostname": { Source: listener443WithHostname, // non-nil hostname Valid: true, - Routes: map[types.NamespacedName]*route{}, + Routes: map[types.NamespacedName]*graph.Route{}, AcceptedHostnames: map[string]struct{}{}, SecretPath: secretPath, }, }, }, - Routes: map[types.NamespacedName]*route{}, + Routes: map[types.NamespacedName]*graph.Route{}, }, expConf: Configuration{ HTTPServers: []VirtualServer{}, @@ -337,18 +342,18 @@ func TestBuildConfiguration(t *testing.T) { msg: "https listeners with no routes", }, { - graph: &graph{ - GatewayClass: &gatewayClass{ + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ Source: &v1beta1.GatewayClass{}, Valid: true, }, - Gateway: &gateway{ + Gateway: &graph.Gateway{ Source: &v1beta1.Gateway{}, - Listeners: map[string]*listener{ + Listeners: map[string]*graph.Listener{ "invalid-listener": { Source: invalidListener, Valid: false, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, }, @@ -360,7 +365,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, }, @@ -376,18 +381,18 @@ func TestBuildConfiguration(t *testing.T) { msg: "invalid listener", }, { - graph: &graph{ - GatewayClass: &gatewayClass{ + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ Source: &v1beta1.GatewayClass{}, Valid: true, }, - Gateway: &gateway{ + Gateway: &graph.Gateway{ Source: &v1beta1.Gateway{}, - Listeners: map[string]*listener{ + Listeners: map[string]*graph.Listener{ "listener-80-1": { Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "hr-1"}: routeHR1, {Namespace: "test", Name: "hr-2"}: routeHR2, }, @@ -398,7 +403,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "hr-1"}: routeHR1, {Namespace: "test", Name: "hr-2"}: routeHR2, }, @@ -443,24 +448,24 @@ func TestBuildConfiguration(t *testing.T) { }, SSLServers: []VirtualServer{}, Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{hr1Groups[0], hr2Groups[0]}, + BackendGroups: []graph.BackendGroup{hr1Groups[0], hr2Groups[0]}, }, msg: "one http listener with two routes for different hostnames", }, { - graph: &graph{ - GatewayClass: &gatewayClass{ + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ Source: &v1beta1.GatewayClass{}, Valid: true, }, - Gateway: &gateway{ + Gateway: &graph.Gateway{ Source: &v1beta1.Gateway{}, - Listeners: map[string]*listener{ + Listeners: map[string]*graph.Listener{ "listener-443-1": { Source: listener443, Valid: true, SecretPath: secretPath, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, }, @@ -473,7 +478,7 @@ func TestBuildConfiguration(t *testing.T) { Source: listener443WithHostname, Valid: true, SecretPath: secretPath, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "https-hr-5"}: httpsRouteHR5, }, AcceptedHostnames: map[string]struct{}{ @@ -482,7 +487,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, {Namespace: "test", Name: "https-hr-5"}: httpsRouteHR5, @@ -557,23 +562,23 @@ func TestBuildConfiguration(t *testing.T) { }, }, Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{httpsHR1Groups[0], httpsHR2Groups[0], httpsHR5Groups[0]}, + BackendGroups: []graph.BackendGroup{httpsHR1Groups[0], httpsHR2Groups[0], httpsHR5Groups[0]}, }, msg: "two https listeners each with routes for different hostnames", }, { - graph: &graph{ - GatewayClass: &gatewayClass{ + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ Source: &v1beta1.GatewayClass{}, Valid: true, }, - Gateway: &gateway{ + Gateway: &graph.Gateway{ Source: &v1beta1.Gateway{}, - Listeners: map[string]*listener{ + Listeners: map[string]*graph.Listener{ "listener-80-1": { Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "hr-3"}: routeHR3, {Namespace: "test", Name: "hr-4"}: routeHR4, }, @@ -585,7 +590,7 @@ func TestBuildConfiguration(t *testing.T) { Source: listener443, Valid: true, SecretPath: secretPath, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "https-hr-3"}: httpsRouteHR3, {Namespace: "test", Name: "https-hr-4"}: httpsRouteHR4, }, @@ -595,7 +600,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "hr-3"}: routeHR3, {Namespace: "test", Name: "hr-4"}: routeHR4, {Namespace: "test", Name: "https-hr-3"}: httpsRouteHR3, @@ -709,7 +714,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, Upstreams: []Upstream{fooUpstream}, - BackendGroups: []BackendGroup{ + BackendGroups: []graph.BackendGroup{ hr3Groups[0], hr3Groups[1], hr4Groups[0], @@ -723,19 +728,19 @@ func TestBuildConfiguration(t *testing.T) { msg: "one http and one https listener with two routes with the same hostname with and without collisions", }, { - graph: &graph{ - GatewayClass: &gatewayClass{ + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ Source: &v1beta1.GatewayClass{}, Valid: false, ErrorMsg: "error", }, - Gateway: &gateway{ + Gateway: &graph.Gateway{ Source: &v1beta1.Gateway{}, - Listeners: map[string]*listener{ + Listeners: map[string]*graph.Listener{ "listener-80-1": { Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "hr-1"}: routeHR1, }, AcceptedHostnames: map[string]struct{}{ @@ -744,7 +749,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "hr-1"}: routeHR1, }, }, @@ -752,15 +757,15 @@ func TestBuildConfiguration(t *testing.T) { msg: "invalid gatewayclass", }, { - graph: &graph{ + graph: &graph.Graph{ GatewayClass: nil, - Gateway: &gateway{ + Gateway: &graph.Gateway{ Source: &v1beta1.Gateway{}, - Listeners: map[string]*listener{ + Listeners: map[string]*graph.Listener{ "listener-80-1": { Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "hr-1"}: routeHR1, }, AcceptedHostnames: map[string]struct{}{ @@ -769,7 +774,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "hr-1"}: routeHR1, }, }, @@ -777,30 +782,30 @@ func TestBuildConfiguration(t *testing.T) { msg: "missing gatewayclass", }, { - graph: &graph{ - GatewayClass: &gatewayClass{ + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ Source: &v1beta1.GatewayClass{}, Valid: true, }, Gateway: nil, - Routes: map[types.NamespacedName]*route{}, + Routes: map[types.NamespacedName]*graph.Route{}, }, expConf: Configuration{}, msg: "missing gateway", }, { - graph: &graph{ - GatewayClass: &gatewayClass{ + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ Source: &v1beta1.GatewayClass{}, Valid: true, }, - Gateway: &gateway{ + Gateway: &graph.Gateway{ Source: &v1beta1.Gateway{}, - Listeners: map[string]*listener{ + Listeners: map[string]*graph.Listener{ "listener-80-1": { Source: listener80, Valid: true, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "hr-5"}: routeHR5, }, AcceptedHostnames: map[string]struct{}{ @@ -809,7 +814,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "hr-5"}: routeHR5, }, }, @@ -839,14 +844,14 @@ func TestBuildConfiguration(t *testing.T) { }, }, SSLServers: []VirtualServer{}, - BackendGroups: []BackendGroup{hr5BackendGroup}, + BackendGroups: []graph.BackendGroup{hr5BackendGroup}, }, msg: "one http listener with one route with filters", }, } for _, test := range tests { - result, warns := buildConfiguration(context.TODO(), test.graph, fakeResolver) + result, warns := BuildConfiguration(context.TODO(), test.graph, fakeResolver) sort.Slice(result.BackendGroups, func(i, j int) bool { return result.BackendGroups[i].GroupName() < result.BackendGroups[j].GroupName() @@ -857,11 +862,11 @@ func TestBuildConfiguration(t *testing.T) { }) if diff := cmp.Diff(test.expConf, result); diff != "" { - t.Errorf("buildConfiguration() %q mismatch for configuration (-want +got):\n%s", test.msg, diff) + t.Errorf("BuildConfiguration() %q mismatch for configuration (-want +got):\n%s", test.msg, diff) } if diff := cmp.Diff(test.expWarns, warns); diff != "" { - t.Errorf("buildConfiguration() %q mismatch for warnings (-want +got):\n%s", test.msg, diff) + t.Errorf("BuildConfiguration() %q mismatch for warnings (-want +got):\n%s", test.msg, diff) } } } @@ -1114,15 +1119,15 @@ func TestBuildUpstreams(t *testing.T) { }, } - createBackendGroup := func(serviceNames ...string) BackendGroup { - var backends []BackendRef + createBackendGroup := func(serviceNames ...string) graph.BackendGroup { + var backends []graph.BackendRef for _, name := range serviceNames { - backends = append(backends, BackendRef{ + backends = append(backends, graph.BackendRef{ Name: name, Svc: &v1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: name}}, }) } - return BackendGroup{ + return graph.BackendGroup{ Backends: backends, } } @@ -1143,31 +1148,31 @@ func TestBuildUpstreams(t *testing.T) { invalidGroup := createBackendGroup("invalid") - routes := map[types.NamespacedName]*route{ + routes := map[types.NamespacedName]*graph.Route{ {Name: "hr1", Namespace: "test"}: { - BackendGroups: []BackendGroup{hr1Group0, hr1Group1}, + BackendGroups: []graph.BackendGroup{hr1Group0, hr1Group1}, }, {Name: "hr2", Namespace: "test"}: { - BackendGroups: []BackendGroup{hr2Group0, hr2Group1}, + BackendGroups: []graph.BackendGroup{hr2Group0, hr2Group1}, }, {Name: "hr3", Namespace: "test"}: { - BackendGroups: []BackendGroup{hr3Group0}, + BackendGroups: []graph.BackendGroup{hr3Group0}, }, } - routes2 := map[types.NamespacedName]*route{ + routes2 := map[types.NamespacedName]*graph.Route{ {Name: "hr4", Namespace: "test"}: { - BackendGroups: []BackendGroup{hr4Group0, hr4Group1}, + BackendGroups: []graph.BackendGroup{hr4Group0, hr4Group1}, }, } - invalidRoutes := map[types.NamespacedName]*route{ + invalidRoutes := map[types.NamespacedName]*graph.Route{ {Name: "invalid", Namespace: "test"}: { - BackendGroups: []BackendGroup{invalidGroup}, + BackendGroups: []graph.BackendGroup{invalidGroup}, }, } - listeners := map[string]*listener{ + listeners := map[string]*graph.Listener{ "invalid-listener": { Valid: false, Routes: invalidRoutes, // shouldn't be included since listener is invalid @@ -1241,13 +1246,13 @@ func TestBuildUpstreams(t *testing.T) { } func TestBuildBackendGroups(t *testing.T) { - createBackendGroup := func(name string, ruleIdx int, backendNames ...string) BackendGroup { - backends := make([]BackendRef, len(backendNames)) + createBackendGroup := func(name string, ruleIdx int, backendNames ...string) graph.BackendGroup { + backends := make([]graph.BackendRef, len(backendNames)) for i, name := range backendNames { - backends[i] = BackendRef{Name: name} + backends[i] = graph.BackendRef{Name: name} } - return BackendGroup{ + return graph.BackendGroup{ Source: types.NamespacedName{Namespace: "test", Name: name}, RuleIdx: ruleIdx, Backends: backends, @@ -1268,32 +1273,32 @@ func TestBuildBackendGroups(t *testing.T) { hrInvalid := createBackendGroup("hr-invalid", 0, "invalid") - invalidRoutes := map[types.NamespacedName]*route{ + invalidRoutes := map[types.NamespacedName]*graph.Route{ {Name: "invalid", Namespace: "test"}: { - BackendGroups: []BackendGroup{hrInvalid}, + BackendGroups: []graph.BackendGroup{hrInvalid}, }, } - routes := map[types.NamespacedName]*route{ + routes := map[types.NamespacedName]*graph.Route{ {Name: "hr1", Namespace: "test"}: { - BackendGroups: []BackendGroup{hr1Rule0, hr1Rule1}, + BackendGroups: []graph.BackendGroup{hr1Rule0, hr1Rule1}, }, {Name: "hr2", Namespace: "test"}: { - BackendGroups: []BackendGroup{hr2Rule0, hr2Rule1}, + BackendGroups: []graph.BackendGroup{hr2Rule0, hr2Rule1}, }, } - routes2 := map[types.NamespacedName]*route{ + routes2 := map[types.NamespacedName]*graph.Route{ // this backend group is a dupe and should be ignored. {Name: "hr1", Namespace: "test"}: { - BackendGroups: []BackendGroup{hr1Rule0, hr1Rule1}, + BackendGroups: []graph.BackendGroup{hr1Rule0, hr1Rule1}, }, {Name: "hr3", Namespace: "test"}: { - BackendGroups: []BackendGroup{hr3Rule0, hr3Rule1}, + BackendGroups: []graph.BackendGroup{hr3Rule0, hr3Rule1}, }, } - listeners := map[string]*listener{ + listeners := map[string]*graph.Listener{ "invalid-listener": { Valid: false, Routes: invalidRoutes, // routes on invalid listener should be ignored. @@ -1308,7 +1313,7 @@ func TestBuildBackendGroups(t *testing.T) { }, } - expGroups := []BackendGroup{ + expGroups := []graph.BackendGroup{ hr1Rule0, hr1Rule1, hr2Rule0, @@ -1329,17 +1334,17 @@ func TestBuildBackendGroups(t *testing.T) { } func TestBuildWarnings(t *testing.T) { - createBackendRefs := func(names ...string) []BackendRef { - backends := make([]BackendRef, len(names)) + createBackendRefs := func(names ...string) []graph.BackendRef { + backends := make([]graph.BackendRef, len(names)) for idx, name := range names { - backends[idx] = BackendRef{Name: name} + backends[idx] = graph.BackendRef{Name: name} } return backends } - createBackendGroup := func(sourceName string, backends []BackendRef, errMsgs ...string) BackendGroup { - return BackendGroup{ + createBackendGroup := func(sourceName string, backends []graph.BackendRef, errMsgs ...string) graph.BackendGroup { + return graph.BackendGroup{ Source: types.NamespacedName{Namespace: "test", Name: sourceName}, Backends: backends, Errors: errMsgs, @@ -1390,28 +1395,28 @@ func TestBuildWarnings(t *testing.T) { hr3 := &v1beta1.HTTPRoute{ObjectMeta: metav1.ObjectMeta{Name: "hr3", Namespace: "test"}} hrInvalid := &v1beta1.HTTPRoute{ObjectMeta: metav1.ObjectMeta{Name: "hr-invalid", Namespace: "test"}} - invalidRoutes := map[types.NamespacedName]*route{ + invalidRoutes := map[types.NamespacedName]*graph.Route{ {Name: "invalid", Namespace: "test"}: { Source: hrInvalid, - BackendGroups: []BackendGroup{hrInvalidGroup}, + BackendGroups: []graph.BackendGroup{hrInvalidGroup}, }, } - routes := map[types.NamespacedName]*route{ + routes := map[types.NamespacedName]*graph.Route{ {Name: "hr1", Namespace: "test"}: { Source: hr1, - BackendGroups: []BackendGroup{hr1BackendGroup0, hr1BackendGroup1}, + BackendGroups: []graph.BackendGroup{hr1BackendGroup0, hr1BackendGroup1}, }, {Name: "hr2", Namespace: "test"}: { Source: hr2, - BackendGroups: []BackendGroup{hr2BackendGroup0, hr2BackendGroup1}, + BackendGroups: []graph.BackendGroup{hr2BackendGroup0, hr2BackendGroup1}, }, } - routes2 := map[types.NamespacedName]*route{ + routes2 := map[types.NamespacedName]*graph.Route{ {Name: "hr3", Namespace: "test"}: { Source: hr3, - BackendGroups: []BackendGroup{hr3BackendGroup0, hr3BackendGroup1}, + BackendGroups: []graph.BackendGroup{hr3BackendGroup0, hr3BackendGroup1}, }, } @@ -1421,9 +1426,9 @@ func TestBuildWarnings(t *testing.T) { "resolve-error": {ErrorMsg: "resolve error"}, } - graph := &graph{ - Gateway: &gateway{ - Listeners: map[string]*listener{ + graph := &graph.Graph{ + Gateway: &graph.Gateway{ + Listeners: map[string]*graph.Listener{ "invalid-listener": { Source: v1beta1.Listener{ Name: "invalid", diff --git a/internal/state/dataplane/doc.go b/internal/state/dataplane/doc.go new file mode 100644 index 000000000..46bfc669c --- /dev/null +++ b/internal/state/dataplane/doc.go @@ -0,0 +1,10 @@ +/* +Package dataplane translates Graph representation of the cluster state into an intermediate representation of +data plane configuration. We can think of it as an intermediate state between the cluster resources +and NGINX configuration files. + +The package includes: +- The types to hold the intermediate representation. +- The functions to translate the Graph into the representation. +*/ +package dataplane diff --git a/internal/state/sort.go b/internal/state/dataplane/sort.go similarity index 80% rename from internal/state/sort.go rename to internal/state/dataplane/sort.go index e2ad4ba5b..5064bb391 100644 --- a/internal/state/sort.go +++ b/internal/state/dataplane/sort.go @@ -1,9 +1,9 @@ -package state +package dataplane import ( "sort" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + nkgsort "github.com/nginxinc/nginx-kubernetes-gateway/internal/sort" ) func sortMatchRules(matchRules []MatchRule) { @@ -61,16 +61,5 @@ func higherPriority(rule1, rule2 MatchRule) bool { } // If still tied, compare the object meta of the two routes. - return lessObjectMeta(&rule1.Source.ObjectMeta, &rule2.Source.ObjectMeta) -} - -func lessObjectMeta(meta1 *metav1.ObjectMeta, meta2 *metav1.ObjectMeta) bool { - if meta1.CreationTimestamp.Equal(&meta2.CreationTimestamp) { - if meta1.Namespace == meta2.Namespace { - return meta1.Name < meta2.Name - } - return meta1.Namespace < meta2.Namespace - } - - return meta1.CreationTimestamp.Before(&meta2.CreationTimestamp) + return nkgsort.LessObjectMeta(&rule1.Source.ObjectMeta, &rule2.Source.ObjectMeta) } diff --git a/internal/state/sort_test.go b/internal/state/dataplane/sort_test.go similarity index 59% rename from internal/state/sort_test.go rename to internal/state/dataplane/sort_test.go index dc991f07c..9e0643473 100644 --- a/internal/state/sort_test.go +++ b/internal/state/dataplane/sort_test.go @@ -1,4 +1,4 @@ -package state +package dataplane import ( "testing" @@ -204,121 +204,3 @@ func TestSort(t *testing.T) { t.Errorf("sortMatchRules() mismatch (-want +got):\n%s", diff) } } - -func TestLessObjectMeta(t *testing.T) { - sooner := metav1.Now() - later := metav1.NewTime(sooner.Add(10 * time.Millisecond)) - - tests := []struct { - meta1 *metav1.ObjectMeta - meta2 *metav1.ObjectMeta - msg string - expected bool - }{ - { - meta1: &metav1.ObjectMeta{ - CreationTimestamp: sooner, - Namespace: "test", - Name: "myname", - }, - meta2: &metav1.ObjectMeta{ - CreationTimestamp: sooner, - Namespace: "test", - Name: "myname", - }, - expected: false, - msg: "equal", - }, - { - meta1: &metav1.ObjectMeta{ - CreationTimestamp: sooner, - Namespace: "test", - Name: "myname", - }, - meta2: &metav1.ObjectMeta{ - CreationTimestamp: later, - Namespace: "test", - Name: "myname", - }, - expected: true, - msg: "less by timestamp", - }, - { - meta1: &metav1.ObjectMeta{ - CreationTimestamp: later, - Namespace: "test", - Name: "myname", - }, - meta2: &metav1.ObjectMeta{ - CreationTimestamp: sooner, - Namespace: "test", - Name: "myname", - }, - expected: false, - msg: "greater by timestamp", - }, - { - meta1: &metav1.ObjectMeta{ - CreationTimestamp: sooner, - Namespace: "atest", - Name: "myname", - }, - meta2: &metav1.ObjectMeta{ - CreationTimestamp: sooner, - Namespace: "test", - Name: "myname", - }, - expected: true, - msg: "less by namespace", - }, - { - meta1: &metav1.ObjectMeta{ - CreationTimestamp: sooner, - Namespace: "test", - Name: "myname", - }, - meta2: &metav1.ObjectMeta{ - CreationTimestamp: sooner, - Namespace: "atest", - Name: "myname", - }, - expected: false, - msg: "greater by namespace", - }, - { - meta1: &metav1.ObjectMeta{ - CreationTimestamp: sooner, - Namespace: "test", - Name: "amyname", - }, - meta2: &metav1.ObjectMeta{ - CreationTimestamp: sooner, - Namespace: "test", - Name: "myname", - }, - expected: true, - msg: "less by name", - }, - { - meta1: &metav1.ObjectMeta{ - CreationTimestamp: sooner, - Namespace: "test", - Name: "myname", - }, - meta2: &metav1.ObjectMeta{ - CreationTimestamp: sooner, - Namespace: "test", - Name: "amyname", - }, - expected: false, - msg: "greater by name", - }, - } - - for _, test := range tests { - result := lessObjectMeta(test.meta1, test.meta2) - if result != test.expected { - t.Errorf("lessObjectMeta() returned %v but expected %v for the case of %q", result, test.expected, test.msg) - } - } -} diff --git a/internal/state/warnings.go b/internal/state/dataplane/warnings.go similarity index 97% rename from internal/state/warnings.go rename to internal/state/dataplane/warnings.go index 425373aac..2bdad975c 100644 --- a/internal/state/warnings.go +++ b/internal/state/dataplane/warnings.go @@ -1,4 +1,4 @@ -package state +package dataplane import ( "fmt" diff --git a/internal/state/warnings_test.go b/internal/state/dataplane/warnings_test.go similarity index 99% rename from internal/state/warnings_test.go rename to internal/state/dataplane/warnings_test.go index 34040d86e..08f0969db 100644 --- a/internal/state/warnings_test.go +++ b/internal/state/dataplane/warnings_test.go @@ -1,4 +1,4 @@ -package state +package dataplane import ( "testing" diff --git a/internal/state/backend_group.go b/internal/state/graph/backend_group.go similarity index 98% rename from internal/state/backend_group.go rename to internal/state/graph/backend_group.go index 215932aa5..d21fd1771 100644 --- a/internal/state/backend_group.go +++ b/internal/state/graph/backend_group.go @@ -1,4 +1,4 @@ -package state +package graph import ( "fmt" diff --git a/internal/state/backend_group_test.go b/internal/state/graph/backend_group_test.go similarity index 75% rename from internal/state/backend_group_test.go rename to internal/state/graph/backend_group_test.go index 0b8e216ca..4d29f1f85 100644 --- a/internal/state/backend_group_test.go +++ b/internal/state/graph/backend_group_test.go @@ -1,15 +1,15 @@ -package state_test +package graph_test import ( "testing" "k8s.io/apimachinery/pkg/types" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/graph" ) func TestBackendGroup_GroupName(t *testing.T) { - bg := state.BackendGroup{ + bg := graph.BackendGroup{ Source: types.NamespacedName{Namespace: "test", Name: "hr"}, RuleIdx: 20, } diff --git a/internal/state/backend_refs.go b/internal/state/graph/backend_refs.go similarity index 98% rename from internal/state/backend_refs.go rename to internal/state/graph/backend_refs.go index 2c2df5d95..be2139e01 100644 --- a/internal/state/backend_refs.go +++ b/internal/state/graph/backend_refs.go @@ -1,4 +1,4 @@ -package state +package graph import ( "errors" @@ -18,7 +18,7 @@ import ( // - the Namespace is not the same as the HTTPRoute namespace // - the Port is nil func addBackendGroupsToRoutes( - routes map[types.NamespacedName]*route, + routes map[types.NamespacedName]*Route, services map[types.NamespacedName]*v1.Service, ) { for _, r := range routes { diff --git a/internal/state/backend_refs_test.go b/internal/state/graph/backend_refs_test.go similarity index 98% rename from internal/state/backend_refs_test.go rename to internal/state/graph/backend_refs_test.go index 3b4c10d6d..5d4876a54 100644 --- a/internal/state/backend_refs_test.go +++ b/internal/state/graph/backend_refs_test.go @@ -1,4 +1,4 @@ -package state +package graph import ( "testing" @@ -214,7 +214,7 @@ func TestResolveBackendRefs(t *testing.T) { hr3 := createRoute("hr3", "NotService", "not-svc") hr4 := removeRefs(createRoute("hr4", "Service", "no-backend-refs")) - routes := map[types.NamespacedName]*route{ + routes := map[types.NamespacedName]*Route{ {Namespace: "test", Name: "hr1"}: { Source: hr1, }, @@ -241,7 +241,7 @@ func TestResolveBackendRefs(t *testing.T) { {Namespace: "test", Name: "svc4"}: svc4, } - expRoutes := map[types.NamespacedName]*route{ + expRoutes := map[types.NamespacedName]*Route{ {Namespace: "test", Name: "hr1"}: { Source: hr1, BackendGroups: []BackendGroup{ diff --git a/internal/state/graph/doc.go b/internal/state/graph/doc.go new file mode 100644 index 000000000..1b4e99a7b --- /dev/null +++ b/internal/state/graph/doc.go @@ -0,0 +1,14 @@ +/* +Package graph translates the cluster state (Gateway API and Kubernetes resources) into a graph-like representation, +for which: +- Resources are validated. For example, if a Gateway listener is invalid, the graph representation will reflect that. +- Connections between resources are found. For example, if an HTTPRoute attaches to a Gateway, the graph representation +reflects that. +- Any validation or connections-related errors are captured. + +Those three points make it easier to generate intermediate data plane configuration and statuses for resources. + +The package includes the types to represent the graph and the functions to convert resources into their graph +representation. +*/ +package graph diff --git a/internal/state/listener.go b/internal/state/graph/gateway.go similarity index 71% rename from internal/state/listener.go rename to internal/state/graph/gateway.go index d38d0f485..d97425db0 100644 --- a/internal/state/listener.go +++ b/internal/state/graph/gateway.go @@ -1,38 +1,105 @@ -package state +package graph import ( "errors" "fmt" + "sort" "strings" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/gateway-api/apis/v1beta1" + nkgsort "github.com/nginxinc/nginx-kubernetes-gateway/internal/sort" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/conditions" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets" ) -// listener represents a listener of the Gateway resource. +// Gateway represents the winning Gateway resource. +type Gateway struct { + // Source is the corresponding Gateway resource. + Source *v1beta1.Gateway + // Listeners include the listeners of the Gateway. + Listeners map[string]*Listener +} + +// Listener represents a Listener of the Gateway resource. // FIXME(pleshakov) For now, we only support HTTP and HTTPS listeners. -type listener struct { - // Source holds the source of the listener from the Gateway resource. +type Listener struct { + // Source holds the source of the Listener from the Gateway resource. Source v1beta1.Listener - // Routes holds the routes attached to the listener. - Routes map[types.NamespacedName]*route - // AcceptedHostnames is an intersection between the hostnames supported by the listener and the hostnames + // Routes holds the routes attached to the Listener. + Routes map[types.NamespacedName]*Route + // AcceptedHostnames is an intersection between the hostnames supported by the Listener and the hostnames // from the attached routes. AcceptedHostnames map[string]struct{} // SecretPath is the path to the secret on disk. SecretPath string - // Conditions holds the conditions of the listener. + // Conditions holds the conditions of the Listener. Conditions []conditions.Condition - // Valid shows whether the listener is valid. - // A listener is considered valid if NKG can generate valid NGINX configuration for it. + // Valid shows whether the Listener is valid. + // A Listener is considered valid if NKG can generate valid NGINX configuration for it. Valid bool } +// processGateways determines which Gateway resource the NGINX Gateway will use (the winner) and which Gateway(s) will +// be ignored. Note that the function will not take into the account any unrelated Gateway resources - the ones with the +// different GatewayClassName field. +func processGateways( + gws map[types.NamespacedName]*v1beta1.Gateway, + gcName string, +) (winner *v1beta1.Gateway, ignoredGateways map[types.NamespacedName]*v1beta1.Gateway) { + referencedGws := make([]*v1beta1.Gateway, 0, len(gws)) + + for _, gw := range gws { + if string(gw.Spec.GatewayClassName) != gcName { + continue + } + + referencedGws = append(referencedGws, gw) + } + + if len(referencedGws) == 0 { + return nil, nil + } + + sort.Slice(referencedGws, func(i, j int) bool { + return nkgsort.LessObjectMeta(&referencedGws[i].ObjectMeta, &referencedGws[j].ObjectMeta) + }) + + ignoredGws := make(map[types.NamespacedName]*v1beta1.Gateway) + + for _, gw := range referencedGws[1:] { + ignoredGws[client.ObjectKeyFromObject(gw)] = gw + } + + return referencedGws[0], ignoredGws +} + +func buildListeners( + gw *v1beta1.Gateway, + gcName string, + secretMemoryMgr secrets.SecretDiskMemoryManager, +) map[string]*Listener { + listeners := make(map[string]*Listener) + + if gw == nil || string(gw.Spec.GatewayClassName) != gcName { + return listeners + } + + listenerFactory := newListenerConfiguratorFactory(gw, secretMemoryMgr) + + for _, gl := range gw.Spec.Listeners { + configurator := listenerFactory.getConfiguratorForListener(gl) + listeners[string(gl.Name)] = configurator.configure(gl) + } + + return listeners +} + type listenerConfigurator interface { - configure(listener v1beta1.Listener) *listener + configure(listener v1beta1.Listener) *Listener } type listenerConfiguratorFactory struct { @@ -53,7 +120,7 @@ func (f *listenerConfiguratorFactory) getConfiguratorForListener(l v1beta1.Liste func newListenerConfiguratorFactory( gw *v1beta1.Gateway, - secretMemoryMgr SecretDiskMemoryManager, + secretMemoryMgr secrets.SecretDiskMemoryManager, ) *listenerConfiguratorFactory { return &listenerConfiguratorFactory{ https: newHTTPSListenerConfigurator(gw, secretMemoryMgr), @@ -63,14 +130,14 @@ func newListenerConfiguratorFactory( type httpListenerConfigurator struct { gateway *v1beta1.Gateway - secretMemoryMgr SecretDiskMemoryManager - usedHostnames map[string]*listener + secretMemoryMgr secrets.SecretDiskMemoryManager + usedHostnames map[string]*Listener validate func(gl v1beta1.Listener) []conditions.Condition } func newHTTPListenerConfigurator(gw *v1beta1.Gateway) *httpListenerConfigurator { return &httpListenerConfigurator{ - usedHostnames: make(map[string]*listener), + usedHostnames: make(map[string]*Listener), gateway: gw, validate: validateHTTPListener, } @@ -78,12 +145,12 @@ func newHTTPListenerConfigurator(gw *v1beta1.Gateway) *httpListenerConfigurator func newHTTPSListenerConfigurator( gateway *v1beta1.Gateway, - secretMemoryMgr SecretDiskMemoryManager, + secretMemoryMgr secrets.SecretDiskMemoryManager, ) *httpListenerConfigurator { return &httpListenerConfigurator{ gateway: gateway, secretMemoryMgr: secretMemoryMgr, - usedHostnames: make(map[string]*listener), + usedHostnames: make(map[string]*Listener), validate: func(gl v1beta1.Listener) []conditions.Condition { return validateHTTPSListener(gl, gateway.Namespace) }, @@ -110,7 +177,7 @@ func validateListener( return conds, validHostnameErr == nil } -func (c *httpListenerConfigurator) ensureUniqueHostnamesAmongListeners(l *listener) { +func (c *httpListenerConfigurator) ensureUniqueHostnamesAmongListeners(l *Listener) { h := getHostname(l.Source.Hostname) if holder, exist := c.usedHostnames[h]; exist { @@ -132,7 +199,7 @@ func (c *httpListenerConfigurator) ensureUniqueHostnamesAmongListeners(l *listen c.usedHostnames[h] = l } -func (c *httpListenerConfigurator) loadSecretIntoListener(l *listener) { +func (c *httpListenerConfigurator) loadSecretIntoListener(l *Listener) { if !l.Valid { return } @@ -152,13 +219,13 @@ func (c *httpListenerConfigurator) loadSecretIntoListener(l *listener) { } } -func (c *httpListenerConfigurator) configure(gl v1beta1.Listener) *listener { +func (c *httpListenerConfigurator) configure(gl v1beta1.Listener) *Listener { conds, validHostname := validateListener(gl, c.gateway, c.validate) - l := &listener{ + l := &Listener{ Source: gl, Valid: len(conds) == 0, - Routes: make(map[types.NamespacedName]*route), + Routes: make(map[types.NamespacedName]*Route), AcceptedHostnames: make(map[string]struct{}), Conditions: conds, } @@ -180,14 +247,14 @@ func newInvalidProtocolListenerConfigurator() *invalidProtocolListenerConfigurat return &invalidProtocolListenerConfigurator{} } -func (c *invalidProtocolListenerConfigurator) configure(gl v1beta1.Listener) *listener { +func (c *invalidProtocolListenerConfigurator) configure(gl v1beta1.Listener) *Listener { msg := fmt.Sprintf("Protocol %q is not supported, use %q or %q", gl.Protocol, v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType) - return &listener{ + return &Listener{ Source: gl, Valid: false, - Routes: make(map[types.NamespacedName]*route), + Routes: make(map[types.NamespacedName]*Route), AcceptedHostnames: make(map[string]struct{}), Conditions: []conditions.Condition{ conditions.NewListenerUnsupportedProtocol(msg), diff --git a/internal/state/graph/gateway_test.go b/internal/state/graph/gateway_test.go new file mode 100644 index 000000000..c9d70fb62 --- /dev/null +++ b/internal/state/graph/gateway_test.go @@ -0,0 +1,782 @@ +package graph + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/conditions" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets" +) + +func TestProcessGateways(t *testing.T) { + const gcName = "test-gc" + + winner := &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gateway-1", + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: gcName, + }, + } + loser := &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gateway-2", + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: gcName, + }, + } + + tests := []struct { + gws map[types.NamespacedName]*v1beta1.Gateway + expectedWinner *v1beta1.Gateway + expectedIgnoredGws map[types.NamespacedName]*v1beta1.Gateway + msg string + }{ + { + gws: nil, + expectedWinner: nil, + expectedIgnoredGws: nil, + msg: "no gateways", + }, + { + gws: map[types.NamespacedName]*v1beta1.Gateway{ + {Namespace: "test", Name: "some-gateway"}: { + Spec: v1beta1.GatewaySpec{GatewayClassName: "some-class"}, + }, + }, + expectedWinner: nil, + expectedIgnoredGws: nil, + msg: "unrelated gateway", + }, + { + gws: map[types.NamespacedName]*v1beta1.Gateway{ + {Namespace: "test", Name: "gateway"}: winner, + }, + expectedWinner: winner, + expectedIgnoredGws: map[types.NamespacedName]*v1beta1.Gateway{}, + msg: "one gateway", + }, + { + gws: map[types.NamespacedName]*v1beta1.Gateway{ + {Namespace: "test", Name: "gateway-1"}: winner, + {Namespace: "test", Name: "gateway-2"}: loser, + }, + expectedWinner: winner, + expectedIgnoredGws: map[types.NamespacedName]*v1beta1.Gateway{ + {Namespace: "test", Name: "gateway-2"}: loser, + }, + msg: "multiple gateways", + }, + } + + for _, test := range tests { + winner, ignoredGws := processGateways(test.gws, gcName) + + if diff := cmp.Diff(winner, test.expectedWinner); diff != "" { + t.Errorf("processGateways() '%s' mismatch for winner (-want +got):\n%s", test.msg, diff) + } + if diff := cmp.Diff(ignoredGws, test.expectedIgnoredGws); diff != "" { + t.Errorf("processGateways() '%s' mismatch for ignored gateways (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestBuildListeners(t *testing.T) { + const gcName = "my-gateway-class" + + listener801 := v1beta1.Listener{ + Name: "listener-80-1", + Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), + Port: 80, + Protocol: v1beta1.HTTPProtocolType, + } + listener802 := v1beta1.Listener{ + Name: "listener-80-2", + Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("bar.example.com")), + Port: 80, + Protocol: v1beta1.TCPProtocolType, // invalid protocol + } + listener803 := v1beta1.Listener{ + Name: "listener-80-3", + Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("bar.example.com")), + Port: 80, + Protocol: v1beta1.HTTPProtocolType, + } + listener804 := v1beta1.Listener{ + Name: "listener-80-4", + Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), + Port: 80, + Protocol: v1beta1.HTTPProtocolType, + } + listener805 := v1beta1.Listener{ + Name: "listener-80-5", + Port: 81, // invalid port + Protocol: v1beta1.HTTPProtocolType, + } + listener806 := v1beta1.Listener{ + Name: "listener-80-6", + Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("$example.com")), // invalid hostname + Port: 80, + Protocol: v1beta1.HTTPProtocolType, + } + + gatewayTLSConfig := &v1beta1.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), + CertificateRefs: []v1beta1.SecretObjectReference{ + { + Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), + }, + }, + } + + tlsConfigInvalidSecret := &v1beta1.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), + CertificateRefs: []v1beta1.SecretObjectReference{ + { + Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Secret")), + Name: "does-not-exist", + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), + }, + }, + } + // https listeners + listener4431 := v1beta1.Listener{ + Name: "listener-443-1", + Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), + Port: 443, + TLS: gatewayTLSConfig, + Protocol: v1beta1.HTTPSProtocolType, + } + listener4432 := v1beta1.Listener{ + Name: "listener-443-2", + Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("bar.example.com")), + Port: 443, + TLS: gatewayTLSConfig, + Protocol: v1beta1.HTTPSProtocolType, + } + listener4433 := v1beta1.Listener{ + Name: "listener-443-3", + Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), + Port: 443, + TLS: gatewayTLSConfig, + Protocol: v1beta1.HTTPSProtocolType, + } + listener4434 := v1beta1.Listener{ + Name: "listener-443-4", + Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("$example.com")), // invalid hostname + Port: 443, + TLS: gatewayTLSConfig, + Protocol: v1beta1.HTTPSProtocolType, + } + listener4435 := v1beta1.Listener{ + Name: "listener-443-5", + Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), + Port: 443, + TLS: tlsConfigInvalidSecret, // invalid https listener; secret does not exist + Protocol: v1beta1.HTTPSProtocolType, + } + listener4436 := v1beta1.Listener{ + Name: "listener-443-6", + Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), + Port: 444, // invalid port + TLS: gatewayTLSConfig, + Protocol: v1beta1.HTTPSProtocolType, + } + + const ( + invalidHostnameMsg = "Invalid hostname: a lowercase RFC 1123 subdomain " + + "must consist of lower case alphanumeric characters, '-' or '.', and must start and end " + + "with an alphanumeric character (e.g. 'example.com', regex used for validation is " + + `'[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')` + + conflictedHostnamesMsg = `Multiple listeners for the same port use the same hostname "foo.example.com"; ` + + "ensure only one listener uses that hostname" + ) + + tests := []struct { + gateway *v1beta1.Gateway + expected map[string]*Listener + name string + }{ + { + gateway: &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1beta1.Listener{ + listener801, + }, + }, + Status: v1beta1.GatewayStatus{}, + }, + expected: map[string]*Listener{ + "listener-80-1": { + Source: listener801, + Valid: true, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + }, + }, + name: "valid http listener", + }, + { + gateway: &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1beta1.Listener{ + listener4431, + }, + }, + }, + expected: map[string]*Listener{ + "listener-443-1": { + Source: listener4431, + Valid: true, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + SecretPath: secretPath, + }, + }, + name: "valid https listener", + }, + { + gateway: &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1beta1.Listener{ + listener802, + }, + }, + }, + expected: map[string]*Listener{ + "listener-80-2": { + Source: listener802, + Valid: false, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + Conditions: []conditions.Condition{ + conditions.NewListenerUnsupportedProtocol(`Protocol "TCP" is not supported, use "HTTP" ` + + `or "HTTPS"`), + }, + }, + }, + name: "invalid listener protocol", + }, + { + gateway: &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1beta1.Listener{ + listener805, + }, + }, + }, + expected: map[string]*Listener{ + "listener-80-5": { + Source: listener805, + Valid: false, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + Conditions: []conditions.Condition{ + conditions.NewListenerPortUnavailable("Port 81 is not supported for HTTP, use 80"), + }, + }, + }, + name: "invalid http listener", + }, + { + gateway: &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1beta1.Listener{ + listener4436, + }, + }, + }, + expected: map[string]*Listener{ + "listener-443-6": { + Source: listener4436, + Valid: false, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + Conditions: []conditions.Condition{ + conditions.NewListenerPortUnavailable("Port 444 is not supported for HTTPS, use 443"), + }, + }, + }, + name: "invalid https listener", + }, + { + gateway: &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1beta1.Listener{ + listener806, + listener4434, + }, + }, + }, + expected: map[string]*Listener{ + "listener-80-6": { + Source: listener806, + Valid: false, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + Conditions: []conditions.Condition{ + conditions.NewListenerUnsupportedValue(invalidHostnameMsg), + }, + }, + "listener-443-4": { + Source: listener4434, + Valid: false, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + Conditions: []conditions.Condition{ + conditions.NewListenerUnsupportedValue(invalidHostnameMsg), + }, + }, + }, + name: "invalid hostnames", + }, + { + gateway: &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1beta1.Listener{ + listener4435, + }, + }, + }, + expected: map[string]*Listener{ + "listener-443-5": { + Source: listener4435, + Valid: false, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + Conditions: conditions.NewListenerInvalidCertificateRef("Failed to get the certificate " + + "test/does-not-exist: secret test/does-not-exist does not exist"), + }, + }, + name: "invalid https listener (secret does not exist)", + }, + { + gateway: &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1beta1.Listener{ + listener801, listener803, + listener4431, listener4432, + }, + }, + }, + expected: map[string]*Listener{ + "listener-80-1": { + Source: listener801, + Valid: true, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + }, + "listener-80-3": { + Source: listener803, + Valid: true, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + }, + "listener-443-1": { + Source: listener4431, + Valid: true, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + SecretPath: secretPath, + }, + "listener-443-2": { + Source: listener4432, + Valid: true, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + SecretPath: secretPath, + }, + }, + name: "multiple valid http/https listeners", + }, + { + gateway: &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1beta1.Listener{ + listener801, listener804, + listener4431, listener4433, + }, + }, + }, + expected: map[string]*Listener{ + "listener-80-1": { + Source: listener801, + Valid: false, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + Conditions: conditions.NewListenerConflictedHostname(conflictedHostnamesMsg), + }, + "listener-80-4": { + Source: listener804, + Valid: false, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + Conditions: conditions.NewListenerConflictedHostname(conflictedHostnamesMsg), + }, + "listener-443-1": { + Source: listener4431, + Valid: false, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + Conditions: conditions.NewListenerConflictedHostname(conflictedHostnamesMsg), + }, + "listener-443-3": { + Source: listener4433, + Valid: false, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + Conditions: conditions.NewListenerConflictedHostname(conflictedHostnamesMsg), + }, + }, + name: "collisions", + }, + { + gateway: &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1beta1.Listener{ + listener801, + listener4431, + }, + Addresses: []v1beta1.GatewayAddress{ + {}, + }, + }, + }, + expected: map[string]*Listener{ + "listener-80-1": { + Source: listener801, + Valid: false, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + Conditions: []conditions.Condition{ + conditions.NewListenerUnsupportedAddress("Specifying Gateway addresses is not supported"), + }, + }, + "listener-443-1": { + Source: listener4431, + Valid: false, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + SecretPath: "", + Conditions: []conditions.Condition{ + conditions.NewListenerUnsupportedAddress("Specifying Gateway addresses is not supported"), + }, + }, + }, + name: "gateway addresses are not supported", + }, + { + gateway: nil, + expected: map[string]*Listener{}, + name: "no gateway", + }, + { + gateway: &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: "wrong-class", + Listeners: []v1beta1.Listener{ + listener801, listener804, + }, + }, + }, + expected: map[string]*Listener{}, + name: "wrong gatewayclass", + }, + } + + // add secret to store + secretStore := secrets.NewSecretStore() + secretStore.Upsert(testSecret) + + secretMemoryMgr := secrets.NewSecretDiskMemoryManager(secretsDirectory, secretStore) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + result := buildListeners(test.gateway, gcName, secretMemoryMgr) + g.Expect(helpers.Diff(test.expected, result)).To(BeEmpty()) + }) + } +} + +func TestValidateHTTPListener(t *testing.T) { + tests := []struct { + l v1beta1.Listener + name string + expected []conditions.Condition + }{ + { + l: v1beta1.Listener{ + Port: 80, + }, + expected: nil, + name: "valid", + }, + { + l: v1beta1.Listener{ + Port: 81, + }, + expected: []conditions.Condition{ + conditions.NewListenerPortUnavailable("Port 81 is not supported for HTTP, use 80"), + }, + name: "invalid port", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + result := validateHTTPListener(test.l) + g.Expect(result).To(Equal(test.expected)) + }) + } +} + +func TestValidateHTTPSListener(t *testing.T) { + gwNs := "gateway-ns" + + validSecretRef := v1beta1.SecretObjectReference{ + Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer(gwNs)), + } + + invalidSecretRefGroup := v1beta1.SecretObjectReference{ + Group: (*v1beta1.Group)(helpers.GetStringPointer("some-group")), + Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer(gwNs)), + } + + invalidSecretRefKind := v1beta1.SecretObjectReference{ + Kind: (*v1beta1.Kind)(helpers.GetStringPointer("ConfigMap")), + Name: "secret", + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer(gwNs)), + } + + invalidSecretRefTNamespace := v1beta1.SecretObjectReference{ + Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("diff-ns")), + } + + tests := []struct { + l v1beta1.Listener + name string + expected []conditions.Condition + }{ + { + l: v1beta1.Listener{ + Port: 443, + TLS: &v1beta1.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), + CertificateRefs: []v1beta1.SecretObjectReference{validSecretRef}, + }, + }, + expected: nil, + name: "valid", + }, + { + l: v1beta1.Listener{ + Port: 80, + TLS: &v1beta1.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), + CertificateRefs: []v1beta1.SecretObjectReference{validSecretRef}, + }, + }, + expected: []conditions.Condition{ + conditions.NewListenerPortUnavailable("Port 80 is not supported for HTTPS, use 443"), + }, + name: "invalid port", + }, + { + l: v1beta1.Listener{ + Port: 443, + TLS: &v1beta1.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), + CertificateRefs: []v1beta1.SecretObjectReference{validSecretRef}, + Options: map[v1beta1.AnnotationKey]v1beta1.AnnotationValue{"key": "val"}, + }, + }, + expected: []conditions.Condition{ + conditions.NewListenerUnsupportedValue("tls.options are not supported"), + }, + name: "invalid options", + }, + { + l: v1beta1.Listener{ + Port: 443, + TLS: &v1beta1.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1beta1.TLSModePassthrough), + CertificateRefs: []v1beta1.SecretObjectReference{validSecretRef}, + }, + }, + expected: []conditions.Condition{ + conditions.NewListenerUnsupportedValue(`tls.mode "Passthrough" is not supported, use "Terminate"`), + }, + name: "invalid tls mode", + }, + { + l: v1beta1.Listener{ + Port: 443, + TLS: &v1beta1.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), + CertificateRefs: []v1beta1.SecretObjectReference{invalidSecretRefGroup}, + }, + }, + expected: conditions.NewListenerInvalidCertificateRef(`Group must be empty, got "some-group"`), + name: "invalid cert ref group", + }, + { + l: v1beta1.Listener{ + Port: 443, + TLS: &v1beta1.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), + CertificateRefs: []v1beta1.SecretObjectReference{invalidSecretRefKind}, + }, + }, + expected: conditions.NewListenerInvalidCertificateRef(`Kind must be Secret, got "ConfigMap"`), + name: "invalid cert ref kind", + }, + { + l: v1beta1.Listener{ + Port: 443, + TLS: &v1beta1.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), + CertificateRefs: []v1beta1.SecretObjectReference{invalidSecretRefTNamespace}, + }, + }, + expected: conditions.NewListenerInvalidCertificateRef("Referenced Secret must belong to the same " + + "namespace as the Gateway"), + name: "invalid cert ref namespace", + }, + { + l: v1beta1.Listener{ + Port: 443, + TLS: &v1beta1.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), + CertificateRefs: []v1beta1.SecretObjectReference{validSecretRef, validSecretRef}, + }, + }, + expected: []conditions.Condition{ + conditions.NewListenerUnsupportedValue("Only 1 certificateRef is supported, got 2"), + }, + name: "too many cert refs", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + result := validateHTTPSListener(test.l, gwNs) + g.Expect(result).To(Equal(test.expected)) + }) + } +} + +func TestValidateListenerHostname(t *testing.T) { + tests := []struct { + hostname *v1beta1.Hostname + name string + expectErr bool + }{ + { + hostname: nil, + expectErr: false, + name: "nil hostname", + }, + { + hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("")), + expectErr: false, + name: "empty hostname", + }, + { + hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), + expectErr: false, + name: "valid hostname", + }, + { + hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("*.example.com")), + expectErr: true, + name: "wildcard hostname", + }, + { + hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("example$com")), + expectErr: true, + name: "invalid hostname", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + err := validateListenerHostname(test.hostname) + + if test.expectErr { + g.Expect(err).ToNot(BeNil()) + } else { + g.Expect(err).To(BeNil()) + } + }) + } +} diff --git a/internal/state/graph/gatewayclass.go b/internal/state/graph/gatewayclass.go new file mode 100644 index 000000000..d5a7841f5 --- /dev/null +++ b/internal/state/graph/gatewayclass.go @@ -0,0 +1,44 @@ +package graph + +import ( + "fmt" + + "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +// GatewayClass represents the GatewayClass resource. +type GatewayClass struct { + // Source is the source resource. + Source *v1beta1.GatewayClass + // ErrorMsg explains the error when the resource is invalid. + ErrorMsg string + // Valid shows whether the GatewayClass is valid. + Valid bool +} + +func buildGatewayClass(gc *v1beta1.GatewayClass, controllerName string) *GatewayClass { + if gc == nil { + return nil + } + + var errorMsg string + + err := validateGatewayClass(gc, controllerName) + if err != nil { + errorMsg = err.Error() + } + + return &GatewayClass{ + Source: gc, + Valid: err == nil, + ErrorMsg: errorMsg, + } +} + +func validateGatewayClass(gc *v1beta1.GatewayClass, controllerName string) error { + if string(gc.Spec.ControllerName) != controllerName { + return fmt.Errorf("Spec.ControllerName must be %s got %s", controllerName, gc.Spec.ControllerName) + } + + return nil +} diff --git a/internal/state/graph/gatewayclass_test.go b/internal/state/graph/gatewayclass_test.go new file mode 100644 index 000000000..b60bf6640 --- /dev/null +++ b/internal/state/graph/gatewayclass_test.go @@ -0,0 +1,78 @@ +package graph + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +func TestBuildGatewayClass(t *testing.T) { + const controllerName = "my.controller" + + validGC := &v1beta1.GatewayClass{ + Spec: v1beta1.GatewayClassSpec{ + ControllerName: "my.controller", + }, + } + invalidGC := &v1beta1.GatewayClass{ + Spec: v1beta1.GatewayClassSpec{ + ControllerName: "wrong.controller", + }, + } + + tests := []struct { + gc *v1beta1.GatewayClass + expected *GatewayClass + msg string + }{ + { + gc: nil, + expected: nil, + msg: "no gatewayclass", + }, + { + gc: validGC, + expected: &GatewayClass{ + Source: validGC, + Valid: true, + ErrorMsg: "", + }, + msg: "valid gatewayclass", + }, + { + gc: invalidGC, + expected: &GatewayClass{ + Source: invalidGC, + Valid: false, + ErrorMsg: "Spec.ControllerName must be my.controller got wrong.controller", + }, + msg: "invalid gatewayclass", + }, + } + + for _, test := range tests { + result := buildGatewayClass(test.gc, controllerName) + if diff := cmp.Diff(test.expected, result); diff != "" { + t.Errorf("buildGatewayClass() '%s' mismatch (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestValidateGatewayClass(t *testing.T) { + gc := &v1beta1.GatewayClass{ + Spec: v1beta1.GatewayClassSpec{ + ControllerName: "test.controller", + }, + } + + err := validateGatewayClass(gc, "test.controller") + if err != nil { + t.Errorf("validateGatewayClass() returned unexpected error %v", err) + } + + err = validateGatewayClass(gc, "unmatched.controller") + if err == nil { + t.Errorf("validateGatewayClass() didn't return an error") + } +} diff --git a/internal/state/graph/graph.go b/internal/state/graph/graph.go new file mode 100644 index 000000000..32d221cd2 --- /dev/null +++ b/internal/state/graph/graph.go @@ -0,0 +1,71 @@ +package graph + +import ( + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets" +) + +// ClusterStore includes cluster resources necessary to build the Graph. +type ClusterStore struct { + GatewayClass *v1beta1.GatewayClass + Gateways map[types.NamespacedName]*v1beta1.Gateway + HTTPRoutes map[types.NamespacedName]*v1beta1.HTTPRoute + Services map[types.NamespacedName]*v1.Service +} + +// Graph is a Graph-like representation of Gateway API resources. +type Graph struct { + // GatewayClass holds the GatewayClass resource. + GatewayClass *GatewayClass + // Gateway holds the winning Gateway resource. + Gateway *Gateway + // IgnoredGateways holds the ignored Gateway resources, which belong to the NGINX Gateway (based on the + // GatewayClassName field of the resource) but ignored. It doesn't hold the Gateway resources that do not belong to + // the NGINX Gateway. + IgnoredGateways map[types.NamespacedName]*v1beta1.Gateway + // Routes holds Route resources. + Routes map[types.NamespacedName]*Route +} + +// BuildGraph builds a Graph from a store. +func BuildGraph( + store ClusterStore, + controllerName string, + gcName string, + secretMemoryMgr secrets.SecretDiskMemoryManager, +) *Graph { + gc := buildGatewayClass(store.GatewayClass, controllerName) + + gw, ignoredGws := processGateways(store.Gateways, gcName) + + listeners := buildListeners(gw, gcName, secretMemoryMgr) + + routes := make(map[types.NamespacedName]*Route) + for _, ghr := range store.HTTPRoutes { + ignored, r := bindHTTPRouteToListeners(ghr, gw, ignoredGws, listeners) + if !ignored { + routes[client.ObjectKeyFromObject(ghr)] = r + } + } + + addBackendGroupsToRoutes(routes, store.Services) + + g := &Graph{ + GatewayClass: gc, + Routes: routes, + IgnoredGateways: ignoredGws, + } + + if gw != nil { + g.Gateway = &Gateway{ + Source: gw, + Listeners: listeners, + } + } + + return g +} diff --git a/internal/state/graph/graph_test.go b/internal/state/graph/graph_test.go new file mode 100644 index 000000000..61be3767d --- /dev/null +++ b/internal/state/graph/graph_test.go @@ -0,0 +1,297 @@ +//nolint:gosec +package graph + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/conditions" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets" +) + +var testSecret = &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "test", + }, + Data: map[string][]byte{ + v1.TLSCertKey: []byte(`-----BEGIN CERTIFICATE----- +MIIDLjCCAhYCCQDAOF9tLsaXWjANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 +ZDEbMBkGA1UEAwwSY2FmZS5leGFtcGxlLmNvbSAgMB4XDTE4MDkxMjE2MTUzNVoX +DTIzMDkxMTE2MTUzNVowWDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMSEwHwYD +VQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxGTAXBgNVBAMMEGNhZmUuZXhh +bXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCp6Kn7sy81 +p0juJ/cyk+vCAmlsfjtFM2muZNK0KtecqG2fjWQb55xQ1YFA2XOSwHAYvSdwI2jZ +ruW8qXXCL2rb4CZCFxwpVECrcxdjm3teViRXVsYImmJHPPSyQgpiobs9x7DlLc6I +BA0ZjUOyl0PqG9SJexMV73WIIa5rDVSF2r4kSkbAj4Dcj7LXeFlVXH2I5XwXCptC +n67JCg42f+k8wgzcRVp8XZkZWZVjwq9RUKDXmFB2YyN1XEWdZ0ewRuKYUJlsm692 +skOrKQj0vkoPn41EE/+TaVEpqLTRoUY3rzg7DkdzfdBizFO2dsPNFx2CW0jXkNLv +Ko25CZrOhXAHAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAKHFCcyOjZvoHswUBMdL +RdHIb383pWFynZq/LuUovsVA58B0Cg7BEfy5vWVVrq5RIkv4lZ81N29x21d1JH6r +jSnQx+DXCO/TJEV5lSCUpIGzEUYaUPgRyjsM/NUdCJ8uHVhZJ+S6FA+CnOD9rn2i +ZBePCI5rHwEXwnnl8ywij3vvQ5zHIuyBglWr/Qyui9fjPpwWUvUm4nv5SMG9zCV7 +PpuwvuatqjO1208BjfE/cZHIg8Hw9mvW9x9C+IQMIMDE7b/g6OcK7LGTLwlFxvA8 +7WjEequnayIphMhKRXVf1N349eN98Ez38fOTHTPbdJjFA/PcC+Gyme+iGt5OQdFh +yRE= +-----END CERTIFICATE-----`), + v1.TLSPrivateKeyKey: []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAqeip+7MvNadI7if3MpPrwgJpbH47RTNprmTStCrXnKhtn41k +G+ecUNWBQNlzksBwGL0ncCNo2a7lvKl1wi9q2+AmQhccKVRAq3MXY5t7XlYkV1bG +CJpiRzz0skIKYqG7Pcew5S3OiAQNGY1DspdD6hvUiXsTFe91iCGuaw1Uhdq+JEpG +wI+A3I+y13hZVVx9iOV8FwqbQp+uyQoONn/pPMIM3EVafF2ZGVmVY8KvUVCg15hQ +dmMjdVxFnWdHsEbimFCZbJuvdrJDqykI9L5KD5+NRBP/k2lRKai00aFGN684Ow5H +c33QYsxTtnbDzRcdgltI15DS7yqNuQmazoVwBwIDAQABAoIBAQCPSdSYnQtSPyql +FfVFpTOsoOYRhf8sI+ibFxIOuRauWehhJxdm5RORpAzmCLyL5VhjtJme223gLrw2 +N99EjUKb/VOmZuDsBc6oCF6QNR58dz8cnORTewcotsJR1pn1hhlnR5HqJJBJask1 +ZEnUQfcXZrL94lo9JH3E+Uqjo1FFs8xxE8woPBqjZsV7pRUZgC3LhxnwLSExyFo4 +cxb9SOG5OmAJozStFoQ2GJOes8rJ5qfdvytgg9xbLaQL/x0kpQ62BoFMBDdqOePW +KfP5zZ6/07/vpj48yA1Q32PzobubsBLd3Kcn32jfm1E7prtWl+JeOFiOznBQFJbN +4qPVRz5hAoGBANtWyxhNCSLu4P+XgKyckljJ6F5668fNj5CzgFRqJ09zn0TlsNro +FTLZcxDqnR3HPYM42JERh2J/qDFZynRQo3cg3oeivUdBVGY8+FI1W0qdub/L9+yu +edOZTQ5XmGGp6r6jexymcJim/OsB3ZnYOpOrlD7SPmBvzNLk4MF6gxbXAoGBAMZO +0p6HbBmcP0tjFXfcKE77ImLm0sAG4uHoUx0ePj/2qrnTnOBBNE4MvgDuTJzy+caU +k8RqmdHCbHzTe6fzYq/9it8sZ77KVN1qkbIcuc+RTxA9nNh1TjsRne74Z0j1FCLk +hHcqH0ri7PYSKHTE8FvFCxZYdbuB84CmZihvxbpRAoGAIbjqaMYPTYuklCda5S79 +YSFJ1JzZe1Kja//tDw1zFcgVCKa31jAwciz0f/lSRq3HS1GGGmezhPVTiqLfeZqc +R0iKbhgbOcVVkJJ3K0yAyKwPTumxKHZ6zImZS0c0am+RY9YGq5T7YrzpzcfvpiOU +ffe3RyFT7cfCmfoOhDCtzukCgYB30oLC1RLFOrqn43vCS51zc5zoY44uBzspwwYN +TwvP/ExWMf3VJrDjBCH+T/6sysePbJEImlzM+IwytFpANfiIXEt/48Xf60Nx8gWM +uHyxZZx/NKtDw0V8vX1POnq2A5eiKa+8jRARYKJLYNdfDuwolxvG6bZhkPi/4EtT +3Y18sQKBgHtKbk+7lNJVeswXE5cUG6EDUsDe/2Ua7fXp7FcjqBEoap1LSw+6TXp0 +ZgrmKE8ARzM47+EJHUviiq/nupE15g0kJW3syhpU9zZLO7ltB0KIkO9ZRcmUjo8Q +cpLlHMAqbLJ8WYGJCkhiWxyal6hYTyWY4cVkC0xtTl/hUE9IeNKo +-----END RSA PRIVATE KEY-----`), + }, + Type: v1.SecretTypeTLS, +} + +var ( + secretPath = "/etc/nginx/secrets/test_secret" + secretsDirectory = "/etc/nginx/secrets" +) + +func TestBuildGraph(t *testing.T) { + const ( + gcName = "my-class" + controllerName = "my.controller" + ) + + createRoute := func(name string, gatewayName string, listenerName string) *v1beta1.HTTPRoute { + return &v1beta1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: name, + }, + Spec: v1beta1.HTTPRouteSpec{ + CommonRouteSpec: v1beta1.CommonRouteSpec{ + ParentRefs: []v1beta1.ParentReference{ + { + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), + Name: v1beta1.ObjectName(gatewayName), + SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer(listenerName)), + }, + }, + }, + Hostnames: []v1beta1.Hostname{ + "foo.example.com", + }, + Rules: []v1beta1.HTTPRouteRule{ + { + Matches: []v1beta1.HTTPRouteMatch{ + { + Path: &v1beta1.HTTPPathMatch{ + Value: helpers.GetStringPointer("/"), + }, + }, + }, + BackendRefs: []v1beta1.HTTPBackendRef{ + { + BackendRef: v1beta1.BackendRef{ + BackendObjectReference: v1beta1.BackendObjectReference{ + Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Service")), + Name: "foo", + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), + Port: (*v1beta1.PortNumber)(helpers.GetInt32Pointer(80)), + }, + }, + }, + }, + }, + }, + }, + } + } + + hr1 := createRoute("hr-1", "gateway-1", "listener-80-1") + hr2 := createRoute("hr-2", "wrong-gateway", "listener-80-1") + hr3 := createRoute("hr-3", "gateway-1", "listener-443-1") // https listener; should not conflict with hr1 + + fooSvc := &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test"}} + + hr1Group := BackendGroup{ + Errors: []string{}, + Source: types.NamespacedName{Namespace: hr1.Namespace, Name: hr1.Name}, + RuleIdx: 0, + Backends: []BackendRef{ + { + Name: "test_foo_80", + Svc: fooSvc, + Port: 80, + Valid: true, + Weight: 1, + }, + }, + } + + hr3Group := BackendGroup{ + Errors: []string{}, + Source: types.NamespacedName{Namespace: hr3.Namespace, Name: hr3.Name}, + RuleIdx: 0, + Backends: []BackendRef{ + { + Name: "test_foo_80", + Svc: fooSvc, + Port: 80, + Valid: true, + Weight: 1, + }, + }, + } + + createGateway := func(name string) *v1beta1.Gateway { + return &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: name, + }, + Spec: v1beta1.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1beta1.Listener{ + { + Name: "listener-80-1", + Hostname: nil, + Port: 80, + Protocol: v1beta1.HTTPProtocolType, + }, + + { + Name: "listener-443-1", + Hostname: nil, + Port: 443, + TLS: &v1beta1.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), + CertificateRefs: []v1beta1.SecretObjectReference{ + { + Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), + }, + }, + }, + Protocol: v1beta1.HTTPSProtocolType, + }, + }, + }, + } + } + + gw1 := createGateway("gateway-1") + gw2 := createGateway("gateway-2") + + svc := &v1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "foo"}} + + store := ClusterStore{ + GatewayClass: &v1beta1.GatewayClass{ + Spec: v1beta1.GatewayClassSpec{ + ControllerName: controllerName, + }, + }, + Gateways: map[types.NamespacedName]*v1beta1.Gateway{ + {Namespace: "test", Name: "gateway-1"}: gw1, + {Namespace: "test", Name: "gateway-2"}: gw2, + }, + HTTPRoutes: map[types.NamespacedName]*v1beta1.HTTPRoute{ + {Namespace: "test", Name: "hr-1"}: hr1, + {Namespace: "test", Name: "hr-2"}: hr2, + {Namespace: "test", Name: "hr-3"}: hr3, + }, + Services: map[types.NamespacedName]*v1.Service{ + {Namespace: "test", Name: "foo"}: svc, + }, + } + + routeHR1 := &Route{ + Source: hr1, + ValidSectionNameRefs: map[string]struct{}{ + "listener-80-1": {}, + }, + InvalidSectionNameRefs: map[string]conditions.Condition{}, + BackendGroups: []BackendGroup{hr1Group}, + } + + routeHR3 := &Route{ + Source: hr3, + ValidSectionNameRefs: map[string]struct{}{ + "listener-443-1": {}, + }, + InvalidSectionNameRefs: map[string]conditions.Condition{}, + BackendGroups: []BackendGroup{hr3Group}, + } + + // add test secret to store + secretStore := secrets.NewSecretStore() + secretStore.Upsert(testSecret) + secretMemoryMgr := secrets.NewSecretDiskMemoryManager(secretsDirectory, secretStore) + + expected := &Graph{ + GatewayClass: &GatewayClass{ + Source: store.GatewayClass, + Valid: true, + }, + Gateway: &Gateway{ + Source: gw1, + Listeners: map[string]*Listener{ + "listener-80-1": { + Source: gw1.Spec.Listeners[0], + Valid: true, + Routes: map[types.NamespacedName]*Route{ + {Namespace: "test", Name: "hr-1"}: routeHR1, + }, + AcceptedHostnames: map[string]struct{}{ + "foo.example.com": {}, + }, + }, + "listener-443-1": { + Source: gw1.Spec.Listeners[1], + Valid: true, + Routes: map[types.NamespacedName]*Route{ + {Namespace: "test", Name: "hr-3"}: routeHR3, + }, + AcceptedHostnames: map[string]struct{}{ + "foo.example.com": {}, + }, + SecretPath: secretPath, + }, + }, + }, + IgnoredGateways: map[types.NamespacedName]*v1beta1.Gateway{ + {Namespace: "test", Name: "gateway-2"}: gw2, + }, + Routes: map[types.NamespacedName]*Route{ + {Namespace: "test", Name: "hr-1"}: routeHR1, + {Namespace: "test", Name: "hr-3"}: routeHR3, + }, + } + + result := BuildGraph(store, controllerName, gcName, secretMemoryMgr) + if diff := cmp.Diff(expected, result); diff != "" { + t.Errorf("BuildGraph() mismatch (-want +got):\n%s", diff) + } +} diff --git a/internal/state/graph.go b/internal/state/graph/httproute.go similarity index 53% rename from internal/state/graph.go rename to internal/state/graph/httproute.go index f29cfbd3e..1c5e02803 100644 --- a/internal/state/graph.go +++ b/internal/state/graph/httproute.go @@ -1,9 +1,6 @@ -package state +package graph import ( - "fmt" - "sort" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/gateway-api/apis/v1beta1" @@ -11,17 +8,9 @@ import ( "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/conditions" ) -// gateway represents the winning Gateway resource. -type gateway struct { - // Source is the corresponding Gateway resource. - Source *v1beta1.Gateway - // Listeners include the listeners of the Gateway. - Listeners map[string]*listener -} - -// route represents an HTTPRoute. -type route struct { - // Source is the source resource of the route. +// Route represents an HTTPRoute. +type Route struct { + // Source is the source resource of the Route. // FIXME(pleshakov) // For now, we assume that the source is only HTTPRoute. // Later we can support more types - TLSRoute, TCPRoute and UDPRoute. @@ -40,139 +29,6 @@ type route struct { BackendGroups []BackendGroup } -// gatewayClass represents the GatewayClass resource. -type gatewayClass struct { - // Source is the source resource. - Source *v1beta1.GatewayClass - // ErrorMsg explains the error when the resource is invalid. - ErrorMsg string - // Valid shows whether the GatewayClass is valid. - Valid bool -} - -// graph is a graph-like representation of Gateway API resources. -type graph struct { - // GatewayClass holds the GatewayClass resource. - GatewayClass *gatewayClass - // Gateway holds the winning Gateway resource. - Gateway *gateway - // IgnoredGateways holds the ignored Gateway resources, which belong to the NGINX Gateway (based on the - // GatewayClassName field of the resource) but ignored. It doesn't hold the Gateway resources that do not belong to - // the NGINX Gateway. - IgnoredGateways map[types.NamespacedName]*v1beta1.Gateway - // Routes holds route resources. - Routes map[types.NamespacedName]*route -} - -// buildGraph builds a graph from a store assuming that the Gateway resource has the gwNsName namespace and name. -func buildGraph( - store *store, - controllerName string, - gcName string, - secretMemoryMgr SecretDiskMemoryManager, -) *graph { - gc := buildGatewayClass(store.gc, controllerName) - - gw, ignoredGws := processGateways(store.gateways, gcName) - - listeners := buildListeners(gw, gcName, secretMemoryMgr) - - routes := make(map[types.NamespacedName]*route) - for _, ghr := range store.httpRoutes { - ignored, r := bindHTTPRouteToListeners(ghr, gw, ignoredGws, listeners) - if !ignored { - routes[client.ObjectKeyFromObject(ghr)] = r - } - } - - addBackendGroupsToRoutes(routes, store.services) - - g := &graph{ - GatewayClass: gc, - Routes: routes, - IgnoredGateways: ignoredGws, - } - - if gw != nil { - g.Gateway = &gateway{ - Source: gw, - Listeners: listeners, - } - } - - return g -} - -// processGateways determines which Gateway resource the NGINX Gateway will use (the winner) and which Gateway(s) will -// be ignored. Note that the function will not take into the account any unrelated Gateway resources - the ones with the -// different GatewayClassName field. -func processGateways( - gws map[types.NamespacedName]*v1beta1.Gateway, - gcName string, -) (winner *v1beta1.Gateway, ignoredGateways map[types.NamespacedName]*v1beta1.Gateway) { - referencedGws := make([]*v1beta1.Gateway, 0, len(gws)) - - for _, gw := range gws { - if string(gw.Spec.GatewayClassName) != gcName { - continue - } - - referencedGws = append(referencedGws, gw) - } - - if len(referencedGws) == 0 { - return nil, nil - } - - sort.Slice(referencedGws, func(i, j int) bool { - return lessObjectMeta(&referencedGws[i].ObjectMeta, &referencedGws[j].ObjectMeta) - }) - - ignoredGws := make(map[types.NamespacedName]*v1beta1.Gateway) - - for _, gw := range referencedGws[1:] { - ignoredGws[client.ObjectKeyFromObject(gw)] = gw - } - - return referencedGws[0], ignoredGws -} - -func buildGatewayClass(gc *v1beta1.GatewayClass, controllerName string) *gatewayClass { - if gc == nil { - return nil - } - - var errorMsg string - - err := validateGatewayClass(gc, controllerName) - if err != nil { - errorMsg = err.Error() - } - - return &gatewayClass{ - Source: gc, - Valid: err == nil, - ErrorMsg: errorMsg, - } -} - -func buildListeners(gw *v1beta1.Gateway, gcName string, secretMemoryMgr SecretDiskMemoryManager) map[string]*listener { - listeners := make(map[string]*listener) - - if gw == nil || string(gw.Spec.GatewayClassName) != gcName { - return listeners - } - - listenerFactory := newListenerConfiguratorFactory(gw, secretMemoryMgr) - - for _, gl := range gw.Spec.Listeners { - configurator := listenerFactory.getConfiguratorForListener(gl) - listeners[string(gl.Name)] = configurator.configure(gl) - } - - return listeners -} - // bindHTTPRouteToListeners tries to bind an HTTPRoute to listener. // There are three possibilities: // (1) HTTPRoute will be ignored. @@ -182,14 +38,14 @@ func bindHTTPRouteToListeners( ghr *v1beta1.HTTPRoute, gw *v1beta1.Gateway, ignoredGws map[types.NamespacedName]*v1beta1.Gateway, - listeners map[string]*listener, -) (ignored bool, r *route) { + listeners map[string]*Listener, +) (ignored bool, r *Route) { if len(ghr.Spec.ParentRefs) == 0 { // ignore HTTPRoute without refs return true, nil } - r = &route{ + r = &Route{ Source: ghr, ValidSectionNameRefs: make(map[string]struct{}), InvalidSectionNameRefs: make(map[string]conditions.Condition), @@ -314,11 +170,3 @@ func getHostname(h *v1beta1.Hostname) string { } return string(*h) } - -func validateGatewayClass(gc *v1beta1.GatewayClass, controllerName string) error { - if string(gc.Spec.ControllerName) != controllerName { - return fmt.Errorf("Spec.ControllerName must be %s got %s", controllerName, gc.Spec.ControllerName) - } - - return nil -} diff --git a/internal/state/graph/httproute_test.go b/internal/state/graph/httproute_test.go new file mode 100644 index 000000000..12bd8b760 --- /dev/null +++ b/internal/state/graph/httproute_test.go @@ -0,0 +1,397 @@ +package graph + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/conditions" +) + +func TestBindRouteToListeners(t *testing.T) { + createRoute := func(hostname string, parentRefs ...v1beta1.ParentReference) *v1beta1.HTTPRoute { + return &v1beta1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "hr-1", + }, + Spec: v1beta1.HTTPRouteSpec{ + CommonRouteSpec: v1beta1.CommonRouteSpec{ + ParentRefs: parentRefs, + }, + Hostnames: []v1beta1.Hostname{ + v1beta1.Hostname(hostname), + }, + }, + } + } + + hrNonExistingSectionName := createRoute("foo.example.com", v1beta1.ParentReference{ + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), + Name: "gateway", + SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer("listener-80-2")), + }) + + hrEmptySectionName := createRoute("foo.example.com", v1beta1.ParentReference{ + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), + Name: "gateway", + }) + + hrIgnoredGateway := createRoute("foo.example.com", v1beta1.ParentReference{ + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), + Name: "ignored-gateway", + SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer("listener-80-1")), + }) + + hrFoo := createRoute("foo.example.com", v1beta1.ParentReference{ + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), + Name: "gateway", + SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer("listener-80-1")), + }) + + hrFooImplicitNamespace := createRoute("foo.example.com", v1beta1.ParentReference{ + Name: "gateway", + SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer("listener-80-1")), + }) + + hrBar := createRoute("bar.example.com", v1beta1.ParentReference{ + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), + Name: "gateway", + SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer("listener-80-1")), + }) + + // we create a new listener each time because the function under test can modify it + createListener := func() *Listener { + return &Listener{ + Source: v1beta1.Listener{ + Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), + }, + Valid: true, + Routes: map[types.NamespacedName]*Route{}, + AcceptedHostnames: map[string]struct{}{}, + } + } + + createModifiedListener := func(m func(*Listener)) *Listener { + l := createListener() + m(l) + return l + } + + gw := &v1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "gateway", + }, + } + + tests := []struct { + httpRoute *v1beta1.HTTPRoute + gw *v1beta1.Gateway + ignoredGws map[types.NamespacedName]*v1beta1.Gateway + listeners map[string]*Listener + expectedRoute *Route + expectedListeners map[string]*Listener + msg string + expectedIgnored bool + }{ + { + httpRoute: createRoute("foo.example.com"), + gw: gw, + ignoredGws: nil, + listeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + expectedIgnored: true, + expectedRoute: nil, + expectedListeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + msg: "HTTPRoute without parent refs", + }, + { + httpRoute: createRoute("foo.example.com", v1beta1.ParentReference{ + Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), + Name: "some-gateway", // wrong gateway + SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer("listener-1")), + }), + gw: gw, + ignoredGws: nil, + listeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + expectedIgnored: true, + expectedRoute: nil, + expectedListeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + msg: "HTTPRoute without good parent refs", + }, + { + httpRoute: hrNonExistingSectionName, + gw: gw, + ignoredGws: nil, + listeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + expectedIgnored: false, + expectedRoute: &Route{ + Source: hrNonExistingSectionName, + ValidSectionNameRefs: map[string]struct{}{}, + InvalidSectionNameRefs: map[string]conditions.Condition{ + "listener-80-2": conditions.NewTODO("listener is not found"), + }, + }, + expectedListeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + msg: "HTTPRoute with non-existing section name", + }, + { + httpRoute: hrEmptySectionName, + gw: gw, + ignoredGws: nil, + listeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + expectedIgnored: true, + expectedRoute: nil, + expectedListeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + msg: "HTTPRoute with empty section name", + }, + { + httpRoute: hrFoo, + gw: gw, + ignoredGws: nil, + listeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + expectedIgnored: false, + expectedRoute: &Route{ + Source: hrFoo, + ValidSectionNameRefs: map[string]struct{}{ + "listener-80-1": {}, + }, + InvalidSectionNameRefs: map[string]conditions.Condition{}, + }, + expectedListeners: map[string]*Listener{ + "listener-80-1": createModifiedListener(func(l *Listener) { + l.Routes = map[types.NamespacedName]*Route{ + {Namespace: "test", Name: "hr-1"}: { + Source: hrFoo, + ValidSectionNameRefs: map[string]struct{}{ + "listener-80-1": {}, + }, + InvalidSectionNameRefs: map[string]conditions.Condition{}, + }, + } + l.AcceptedHostnames = map[string]struct{}{ + "foo.example.com": {}, + } + }), + }, + msg: "HTTPRoute with one accepted hostname", + }, + { + httpRoute: hrFooImplicitNamespace, + gw: gw, + ignoredGws: nil, + listeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + expectedIgnored: false, + expectedRoute: &Route{ + Source: hrFooImplicitNamespace, + ValidSectionNameRefs: map[string]struct{}{ + "listener-80-1": {}, + }, + InvalidSectionNameRefs: map[string]conditions.Condition{}, + }, + expectedListeners: map[string]*Listener{ + "listener-80-1": createModifiedListener(func(l *Listener) { + l.Routes = map[types.NamespacedName]*Route{ + {Namespace: "test", Name: "hr-1"}: { + Source: hrFooImplicitNamespace, + ValidSectionNameRefs: map[string]struct{}{ + "listener-80-1": {}, + }, + InvalidSectionNameRefs: map[string]conditions.Condition{}, + }, + } + l.AcceptedHostnames = map[string]struct{}{ + "foo.example.com": {}, + } + }), + }, + msg: "HTTPRoute with one accepted hostname with implicit namespace in parentRef", + }, + { + httpRoute: hrBar, + gw: gw, + ignoredGws: nil, + listeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + expectedIgnored: false, + expectedRoute: &Route{ + Source: hrBar, + ValidSectionNameRefs: map[string]struct{}{}, + InvalidSectionNameRefs: map[string]conditions.Condition{ + "listener-80-1": conditions.NewRouteNoMatchingListenerHostname(), + }, + }, + expectedListeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + msg: "HTTPRoute with zero accepted hostnames", + }, + { + httpRoute: hrIgnoredGateway, + gw: gw, + ignoredGws: map[types.NamespacedName]*v1beta1.Gateway{ + {Namespace: "test", Name: "ignored-gateway"}: {}, + }, + listeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + expectedIgnored: false, + expectedRoute: &Route{ + Source: hrIgnoredGateway, + ValidSectionNameRefs: map[string]struct{}{}, + InvalidSectionNameRefs: map[string]conditions.Condition{ + "listener-80-1": conditions.NewTODO("Gateway is ignored"), + }, + }, + expectedListeners: map[string]*Listener{ + "listener-80-1": createListener(), + }, + msg: "HTTPRoute with ignored gateway reference", + }, + { + httpRoute: hrFoo, + gw: nil, + ignoredGws: nil, + listeners: nil, + expectedIgnored: true, + expectedRoute: nil, + expectedListeners: nil, + msg: "HTTPRoute when no gateway exists", + }, + { + httpRoute: hrFoo, + gw: gw, + ignoredGws: nil, + listeners: map[string]*Listener{ + "listener-80-1": createModifiedListener(func(l *Listener) { + l.Valid = false + }), + }, + expectedIgnored: false, + expectedRoute: &Route{ + Source: hrFoo, + ValidSectionNameRefs: map[string]struct{}{}, + InvalidSectionNameRefs: map[string]conditions.Condition{ + "listener-80-1": conditions.NewRouteInvalidListener(), + }, + }, + expectedListeners: map[string]*Listener{ + "listener-80-1": createModifiedListener(func(l *Listener) { + l.Valid = false + }), + }, + msg: "HTTPRoute with invalid listener parentRef", + }, + } + + for _, test := range tests { + ignored, route := bindHTTPRouteToListeners(test.httpRoute, test.gw, test.ignoredGws, test.listeners) + if diff := cmp.Diff(test.expectedIgnored, ignored); diff != "" { + t.Errorf("bindHTTPRouteToListeners() %q mismatch on ignored (-want +got):\n%s", test.msg, diff) + } + if diff := cmp.Diff(test.expectedRoute, route); diff != "" { + t.Errorf("bindHTTPRouteToListeners() %q mismatch on route (-want +got):\n%s", test.msg, diff) + } + if diff := cmp.Diff(test.expectedListeners, test.listeners); diff != "" { + t.Errorf("bindHTTPRouteToListeners() %q mismatch on listeners (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestFindAcceptedHostnames(t *testing.T) { + var listenerHostnameFoo v1beta1.Hostname = "foo.example.com" + var listenerHostnameCafe v1beta1.Hostname = "cafe.example.com" + routeHostnames := []v1beta1.Hostname{"foo.example.com", "bar.example.com"} + + tests := []struct { + listenerHostname *v1beta1.Hostname + msg string + routeHostnames []v1beta1.Hostname + expected []string + }{ + { + listenerHostname: &listenerHostnameFoo, + routeHostnames: routeHostnames, + expected: []string{"foo.example.com"}, + msg: "one match", + }, + { + listenerHostname: &listenerHostnameCafe, + routeHostnames: routeHostnames, + expected: nil, + msg: "no match", + }, + { + listenerHostname: nil, + routeHostnames: routeHostnames, + expected: []string{"foo.example.com", "bar.example.com"}, + msg: "nil listener hostname", + }, + } + + for _, test := range tests { + result := findAcceptedHostnames(test.listenerHostname, test.routeHostnames) + if diff := cmp.Diff(test.expected, result); diff != "" { + t.Errorf("findAcceptedHostnames() %q mismatch (-want +got):\n%s", test.msg, diff) + } + } +} + +func TestGetHostname(t *testing.T) { + var emptyHostname v1beta1.Hostname + var hostname v1beta1.Hostname = "example.com" + + tests := []struct { + h *v1beta1.Hostname + expected string + msg string + }{ + { + h: nil, + expected: "", + msg: "nil hostname", + }, + { + h: &emptyHostname, + expected: "", + msg: "empty hostname", + }, + { + h: &hostname, + expected: string(hostname), + msg: "normal hostname", + }, + } + + for _, test := range tests { + result := getHostname(test.h) + if result != test.expected { + t.Errorf("getHostname() 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 deleted file mode 100644 index 8103cdfaf..000000000 --- a/internal/state/graph_test.go +++ /dev/null @@ -1,1293 +0,0 @@ -//nolint:gosec -package state - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - . "github.com/onsi/gomega" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/gateway-api/apis/v1beta1" - - "github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/conditions" -) - -var testSecret = &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "secret", - Namespace: "test", - }, - Data: map[string][]byte{ - v1.TLSCertKey: []byte(`-----BEGIN CERTIFICATE----- -MIIDLjCCAhYCCQDAOF9tLsaXWjANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJV -UzELMAkGA1UECAwCQ0ExITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 -ZDEbMBkGA1UEAwwSY2FmZS5leGFtcGxlLmNvbSAgMB4XDTE4MDkxMjE2MTUzNVoX -DTIzMDkxMTE2MTUzNVowWDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMSEwHwYD -VQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxGTAXBgNVBAMMEGNhZmUuZXhh -bXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCp6Kn7sy81 -p0juJ/cyk+vCAmlsfjtFM2muZNK0KtecqG2fjWQb55xQ1YFA2XOSwHAYvSdwI2jZ -ruW8qXXCL2rb4CZCFxwpVECrcxdjm3teViRXVsYImmJHPPSyQgpiobs9x7DlLc6I -BA0ZjUOyl0PqG9SJexMV73WIIa5rDVSF2r4kSkbAj4Dcj7LXeFlVXH2I5XwXCptC -n67JCg42f+k8wgzcRVp8XZkZWZVjwq9RUKDXmFB2YyN1XEWdZ0ewRuKYUJlsm692 -skOrKQj0vkoPn41EE/+TaVEpqLTRoUY3rzg7DkdzfdBizFO2dsPNFx2CW0jXkNLv -Ko25CZrOhXAHAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAKHFCcyOjZvoHswUBMdL -RdHIb383pWFynZq/LuUovsVA58B0Cg7BEfy5vWVVrq5RIkv4lZ81N29x21d1JH6r -jSnQx+DXCO/TJEV5lSCUpIGzEUYaUPgRyjsM/NUdCJ8uHVhZJ+S6FA+CnOD9rn2i -ZBePCI5rHwEXwnnl8ywij3vvQ5zHIuyBglWr/Qyui9fjPpwWUvUm4nv5SMG9zCV7 -PpuwvuatqjO1208BjfE/cZHIg8Hw9mvW9x9C+IQMIMDE7b/g6OcK7LGTLwlFxvA8 -7WjEequnayIphMhKRXVf1N349eN98Ez38fOTHTPbdJjFA/PcC+Gyme+iGt5OQdFh -yRE= ------END CERTIFICATE-----`), - v1.TLSPrivateKeyKey: []byte(`-----BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAqeip+7MvNadI7if3MpPrwgJpbH47RTNprmTStCrXnKhtn41k -G+ecUNWBQNlzksBwGL0ncCNo2a7lvKl1wi9q2+AmQhccKVRAq3MXY5t7XlYkV1bG -CJpiRzz0skIKYqG7Pcew5S3OiAQNGY1DspdD6hvUiXsTFe91iCGuaw1Uhdq+JEpG -wI+A3I+y13hZVVx9iOV8FwqbQp+uyQoONn/pPMIM3EVafF2ZGVmVY8KvUVCg15hQ -dmMjdVxFnWdHsEbimFCZbJuvdrJDqykI9L5KD5+NRBP/k2lRKai00aFGN684Ow5H -c33QYsxTtnbDzRcdgltI15DS7yqNuQmazoVwBwIDAQABAoIBAQCPSdSYnQtSPyql -FfVFpTOsoOYRhf8sI+ibFxIOuRauWehhJxdm5RORpAzmCLyL5VhjtJme223gLrw2 -N99EjUKb/VOmZuDsBc6oCF6QNR58dz8cnORTewcotsJR1pn1hhlnR5HqJJBJask1 -ZEnUQfcXZrL94lo9JH3E+Uqjo1FFs8xxE8woPBqjZsV7pRUZgC3LhxnwLSExyFo4 -cxb9SOG5OmAJozStFoQ2GJOes8rJ5qfdvytgg9xbLaQL/x0kpQ62BoFMBDdqOePW -KfP5zZ6/07/vpj48yA1Q32PzobubsBLd3Kcn32jfm1E7prtWl+JeOFiOznBQFJbN -4qPVRz5hAoGBANtWyxhNCSLu4P+XgKyckljJ6F5668fNj5CzgFRqJ09zn0TlsNro -FTLZcxDqnR3HPYM42JERh2J/qDFZynRQo3cg3oeivUdBVGY8+FI1W0qdub/L9+yu -edOZTQ5XmGGp6r6jexymcJim/OsB3ZnYOpOrlD7SPmBvzNLk4MF6gxbXAoGBAMZO -0p6HbBmcP0tjFXfcKE77ImLm0sAG4uHoUx0ePj/2qrnTnOBBNE4MvgDuTJzy+caU -k8RqmdHCbHzTe6fzYq/9it8sZ77KVN1qkbIcuc+RTxA9nNh1TjsRne74Z0j1FCLk -hHcqH0ri7PYSKHTE8FvFCxZYdbuB84CmZihvxbpRAoGAIbjqaMYPTYuklCda5S79 -YSFJ1JzZe1Kja//tDw1zFcgVCKa31jAwciz0f/lSRq3HS1GGGmezhPVTiqLfeZqc -R0iKbhgbOcVVkJJ3K0yAyKwPTumxKHZ6zImZS0c0am+RY9YGq5T7YrzpzcfvpiOU -ffe3RyFT7cfCmfoOhDCtzukCgYB30oLC1RLFOrqn43vCS51zc5zoY44uBzspwwYN -TwvP/ExWMf3VJrDjBCH+T/6sysePbJEImlzM+IwytFpANfiIXEt/48Xf60Nx8gWM -uHyxZZx/NKtDw0V8vX1POnq2A5eiKa+8jRARYKJLYNdfDuwolxvG6bZhkPi/4EtT -3Y18sQKBgHtKbk+7lNJVeswXE5cUG6EDUsDe/2Ua7fXp7FcjqBEoap1LSw+6TXp0 -ZgrmKE8ARzM47+EJHUviiq/nupE15g0kJW3syhpU9zZLO7ltB0KIkO9ZRcmUjo8Q -cpLlHMAqbLJ8WYGJCkhiWxyal6hYTyWY4cVkC0xtTl/hUE9IeNKo ------END RSA PRIVATE KEY-----`), - }, - Type: v1.SecretTypeTLS, -} - -var ( - secretPath = "/etc/nginx/secrets/test_secret" - secretsDirectory = "/etc/nginx/secrets" -) - -func TestBuildGraph(t *testing.T) { - const ( - gcName = "my-class" - controllerName = "my.controller" - ) - - createRoute := func(name string, gatewayName string, listenerName string) *v1beta1.HTTPRoute { - return &v1beta1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: name, - }, - Spec: v1beta1.HTTPRouteSpec{ - CommonRouteSpec: v1beta1.CommonRouteSpec{ - ParentRefs: []v1beta1.ParentReference{ - { - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), - Name: v1beta1.ObjectName(gatewayName), - SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer(listenerName)), - }, - }, - }, - Hostnames: []v1beta1.Hostname{ - "foo.example.com", - }, - Rules: []v1beta1.HTTPRouteRule{ - { - Matches: []v1beta1.HTTPRouteMatch{ - { - Path: &v1beta1.HTTPPathMatch{ - Value: helpers.GetStringPointer("/"), - }, - }, - }, - BackendRefs: []v1beta1.HTTPBackendRef{ - { - BackendRef: v1beta1.BackendRef{ - BackendObjectReference: v1beta1.BackendObjectReference{ - Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Service")), - Name: "foo", - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), - Port: (*v1beta1.PortNumber)(helpers.GetInt32Pointer(80)), - }, - }, - }, - }, - }, - }, - }, - } - } - - hr1 := createRoute("hr-1", "gateway-1", "listener-80-1") - hr2 := createRoute("hr-2", "wrong-gateway", "listener-80-1") - hr3 := createRoute("hr-3", "gateway-1", "listener-443-1") // https listener; should not conflict with hr1 - - fooSvc := &v1.Service{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "test"}} - - hr1Group := BackendGroup{ - Errors: []string{}, - Source: types.NamespacedName{Namespace: hr1.Namespace, Name: hr1.Name}, - RuleIdx: 0, - Backends: []BackendRef{ - { - Name: "test_foo_80", - Svc: fooSvc, - Port: 80, - Valid: true, - Weight: 1, - }, - }, - } - - hr3Group := BackendGroup{ - Errors: []string{}, - Source: types.NamespacedName{Namespace: hr3.Namespace, Name: hr3.Name}, - RuleIdx: 0, - Backends: []BackendRef{ - { - Name: "test_foo_80", - Svc: fooSvc, - Port: 80, - Valid: true, - Weight: 1, - }, - }, - } - - createGateway := func(name string) *v1beta1.Gateway { - return &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: name, - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: gcName, - Listeners: []v1beta1.Listener{ - { - Name: "listener-80-1", - Hostname: nil, - Port: 80, - Protocol: v1beta1.HTTPProtocolType, - }, - - { - Name: "listener-443-1", - Hostname: nil, - Port: 443, - TLS: &v1beta1.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), - CertificateRefs: []v1beta1.SecretObjectReference{ - { - Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Secret")), - Name: "secret", - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), - }, - }, - }, - Protocol: v1beta1.HTTPSProtocolType, - }, - }, - }, - } - } - - gw1 := createGateway("gateway-1") - gw2 := createGateway("gateway-2") - - svc := &v1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: "test", Name: "foo"}} - - store := &store{ - gc: &v1beta1.GatewayClass{ - Spec: v1beta1.GatewayClassSpec{ - ControllerName: controllerName, - }, - }, - gateways: map[types.NamespacedName]*v1beta1.Gateway{ - {Namespace: "test", Name: "gateway-1"}: gw1, - {Namespace: "test", Name: "gateway-2"}: gw2, - }, - httpRoutes: map[types.NamespacedName]*v1beta1.HTTPRoute{ - {Namespace: "test", Name: "hr-1"}: hr1, - {Namespace: "test", Name: "hr-2"}: hr2, - {Namespace: "test", Name: "hr-3"}: hr3, - }, - services: map[types.NamespacedName]*v1.Service{ - {Namespace: "test", Name: "foo"}: svc, - }, - } - - routeHR1 := &route{ - Source: hr1, - ValidSectionNameRefs: map[string]struct{}{ - "listener-80-1": {}, - }, - InvalidSectionNameRefs: map[string]conditions.Condition{}, - BackendGroups: []BackendGroup{hr1Group}, - } - - routeHR3 := &route{ - Source: hr3, - ValidSectionNameRefs: map[string]struct{}{ - "listener-443-1": {}, - }, - InvalidSectionNameRefs: map[string]conditions.Condition{}, - BackendGroups: []BackendGroup{hr3Group}, - } - - // add test secret to store - secretStore := NewSecretStore() - secretStore.Upsert(testSecret) - secretMemoryMgr := NewSecretDiskMemoryManager(secretsDirectory, secretStore) - - expected := &graph{ - GatewayClass: &gatewayClass{ - Source: store.gc, - Valid: true, - }, - Gateway: &gateway{ - Source: gw1, - Listeners: map[string]*listener{ - "listener-80-1": { - Source: gw1.Spec.Listeners[0], - Valid: true, - Routes: map[types.NamespacedName]*route{ - {Namespace: "test", Name: "hr-1"}: routeHR1, - }, - AcceptedHostnames: map[string]struct{}{ - "foo.example.com": {}, - }, - }, - "listener-443-1": { - Source: gw1.Spec.Listeners[1], - Valid: true, - Routes: map[types.NamespacedName]*route{ - {Namespace: "test", Name: "hr-3"}: routeHR3, - }, - AcceptedHostnames: map[string]struct{}{ - "foo.example.com": {}, - }, - SecretPath: secretPath, - }, - }, - }, - IgnoredGateways: map[types.NamespacedName]*v1beta1.Gateway{ - {Namespace: "test", Name: "gateway-2"}: gw2, - }, - Routes: map[types.NamespacedName]*route{ - {Namespace: "test", Name: "hr-1"}: routeHR1, - {Namespace: "test", Name: "hr-3"}: routeHR3, - }, - } - - result := buildGraph(store, controllerName, gcName, secretMemoryMgr) - if diff := cmp.Diff(expected, result); diff != "" { - t.Errorf("buildGraph() mismatch (-want +got):\n%s", diff) - } -} - -func TestProcessGateways(t *testing.T) { - const gcName = "test-gc" - - winner := &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: "gateway-1", - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: gcName, - }, - } - loser := &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: "gateway-2", - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: gcName, - }, - } - - tests := []struct { - gws map[types.NamespacedName]*v1beta1.Gateway - expectedWinner *v1beta1.Gateway - expectedIgnoredGws map[types.NamespacedName]*v1beta1.Gateway - msg string - }{ - { - gws: nil, - expectedWinner: nil, - expectedIgnoredGws: nil, - msg: "no gateways", - }, - { - gws: map[types.NamespacedName]*v1beta1.Gateway{ - {Namespace: "test", Name: "some-gateway"}: { - Spec: v1beta1.GatewaySpec{GatewayClassName: "some-class"}, - }, - }, - expectedWinner: nil, - expectedIgnoredGws: nil, - msg: "unrelated gateway", - }, - { - gws: map[types.NamespacedName]*v1beta1.Gateway{ - {Namespace: "test", Name: "gateway"}: winner, - }, - expectedWinner: winner, - expectedIgnoredGws: map[types.NamespacedName]*v1beta1.Gateway{}, - msg: "one gateway", - }, - { - gws: map[types.NamespacedName]*v1beta1.Gateway{ - {Namespace: "test", Name: "gateway-1"}: winner, - {Namespace: "test", Name: "gateway-2"}: loser, - }, - expectedWinner: winner, - expectedIgnoredGws: map[types.NamespacedName]*v1beta1.Gateway{ - {Namespace: "test", Name: "gateway-2"}: loser, - }, - msg: "multiple gateways", - }, - } - - for _, test := range tests { - winner, ignoredGws := processGateways(test.gws, gcName) - - if diff := cmp.Diff(winner, test.expectedWinner); diff != "" { - t.Errorf("processGateways() '%s' mismatch for winner (-want +got):\n%s", test.msg, diff) - } - if diff := cmp.Diff(ignoredGws, test.expectedIgnoredGws); diff != "" { - t.Errorf("processGateways() '%s' mismatch for ignored gateways (-want +got):\n%s", test.msg, diff) - } - } -} - -func TestBuildGatewayClass(t *testing.T) { - const controllerName = "my.controller" - - validGC := &v1beta1.GatewayClass{ - Spec: v1beta1.GatewayClassSpec{ - ControllerName: "my.controller", - }, - } - invalidGC := &v1beta1.GatewayClass{ - Spec: v1beta1.GatewayClassSpec{ - ControllerName: "wrong.controller", - }, - } - - tests := []struct { - gc *v1beta1.GatewayClass - expected *gatewayClass - msg string - }{ - { - gc: nil, - expected: nil, - msg: "no gatewayclass", - }, - { - gc: validGC, - expected: &gatewayClass{ - Source: validGC, - Valid: true, - ErrorMsg: "", - }, - msg: "valid gatewayclass", - }, - { - gc: invalidGC, - expected: &gatewayClass{ - Source: invalidGC, - Valid: false, - ErrorMsg: "Spec.ControllerName must be my.controller got wrong.controller", - }, - msg: "invalid gatewayclass", - }, - } - - for _, test := range tests { - result := buildGatewayClass(test.gc, controllerName) - if diff := cmp.Diff(test.expected, result); diff != "" { - t.Errorf("buildGatewayClass() '%s' mismatch (-want +got):\n%s", test.msg, diff) - } - } -} - -func TestBuildListeners(t *testing.T) { - const gcName = "my-gateway-class" - - listener801 := v1beta1.Listener{ - Name: "listener-80-1", - Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), - Port: 80, - Protocol: v1beta1.HTTPProtocolType, - } - listener802 := v1beta1.Listener{ - Name: "listener-80-2", - Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("bar.example.com")), - Port: 80, - Protocol: v1beta1.TCPProtocolType, // invalid protocol - } - listener803 := v1beta1.Listener{ - Name: "listener-80-3", - Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("bar.example.com")), - Port: 80, - Protocol: v1beta1.HTTPProtocolType, - } - listener804 := v1beta1.Listener{ - Name: "listener-80-4", - Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), - Port: 80, - Protocol: v1beta1.HTTPProtocolType, - } - listener805 := v1beta1.Listener{ - Name: "listener-80-5", - Port: 81, // invalid port - Protocol: v1beta1.HTTPProtocolType, - } - listener806 := v1beta1.Listener{ - Name: "listener-80-6", - Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("$example.com")), // invalid hostname - Port: 80, - Protocol: v1beta1.HTTPProtocolType, - } - - gatewayTLSConfig := &v1beta1.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), - CertificateRefs: []v1beta1.SecretObjectReference{ - { - Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Secret")), - Name: "secret", - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), - }, - }, - } - - tlsConfigInvalidSecret := &v1beta1.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), - CertificateRefs: []v1beta1.SecretObjectReference{ - { - Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Secret")), - Name: "does-not-exist", - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), - }, - }, - } - // https listeners - listener4431 := v1beta1.Listener{ - Name: "listener-443-1", - Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), - Port: 443, - TLS: gatewayTLSConfig, - Protocol: v1beta1.HTTPSProtocolType, - } - listener4432 := v1beta1.Listener{ - Name: "listener-443-2", - Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("bar.example.com")), - Port: 443, - TLS: gatewayTLSConfig, - Protocol: v1beta1.HTTPSProtocolType, - } - listener4433 := v1beta1.Listener{ - Name: "listener-443-3", - Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), - Port: 443, - TLS: gatewayTLSConfig, - Protocol: v1beta1.HTTPSProtocolType, - } - listener4434 := v1beta1.Listener{ - Name: "listener-443-4", - Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("$example.com")), // invalid hostname - Port: 443, - TLS: gatewayTLSConfig, - Protocol: v1beta1.HTTPSProtocolType, - } - listener4435 := v1beta1.Listener{ - Name: "listener-443-5", - Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), - Port: 443, - TLS: tlsConfigInvalidSecret, // invalid https listener; secret does not exist - Protocol: v1beta1.HTTPSProtocolType, - } - listener4436 := v1beta1.Listener{ - Name: "listener-443-6", - Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), - Port: 444, // invalid port - TLS: gatewayTLSConfig, - Protocol: v1beta1.HTTPSProtocolType, - } - - const ( - invalidHostnameMsg = "Invalid hostname: a lowercase RFC 1123 subdomain " + - "must consist of lower case alphanumeric characters, '-' or '.', and must start and end " + - "with an alphanumeric character (e.g. 'example.com', regex used for validation is " + - `'[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')` - - conflictedHostnamesMsg = `Multiple listeners for the same port use the same hostname "foo.example.com"; ` + - "ensure only one listener uses that hostname" - ) - - tests := []struct { - gateway *v1beta1.Gateway - expected map[string]*listener - name string - }{ - { - gateway: &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: gcName, - Listeners: []v1beta1.Listener{ - listener801, - }, - }, - Status: v1beta1.GatewayStatus{}, - }, - expected: map[string]*listener{ - "listener-80-1": { - Source: listener801, - Valid: true, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - }, - }, - name: "valid http listener", - }, - { - gateway: &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: gcName, - Listeners: []v1beta1.Listener{ - listener4431, - }, - }, - }, - expected: map[string]*listener{ - "listener-443-1": { - Source: listener4431, - Valid: true, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - SecretPath: secretPath, - }, - }, - name: "valid https listener", - }, - { - gateway: &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: gcName, - Listeners: []v1beta1.Listener{ - listener802, - }, - }, - }, - expected: map[string]*listener{ - "listener-80-2": { - Source: listener802, - Valid: false, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - Conditions: []conditions.Condition{ - conditions.NewListenerUnsupportedProtocol(`Protocol "TCP" is not supported, use "HTTP" ` + - `or "HTTPS"`), - }, - }, - }, - name: "invalid listener protocol", - }, - { - gateway: &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: gcName, - Listeners: []v1beta1.Listener{ - listener805, - }, - }, - }, - expected: map[string]*listener{ - "listener-80-5": { - Source: listener805, - Valid: false, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - Conditions: []conditions.Condition{ - conditions.NewListenerPortUnavailable("Port 81 is not supported for HTTP, use 80"), - }, - }, - }, - name: "invalid http listener", - }, - { - gateway: &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: gcName, - Listeners: []v1beta1.Listener{ - listener4436, - }, - }, - }, - expected: map[string]*listener{ - "listener-443-6": { - Source: listener4436, - Valid: false, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - Conditions: []conditions.Condition{ - conditions.NewListenerPortUnavailable("Port 444 is not supported for HTTPS, use 443"), - }, - }, - }, - name: "invalid https listener", - }, - { - gateway: &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: gcName, - Listeners: []v1beta1.Listener{ - listener806, - listener4434, - }, - }, - }, - expected: map[string]*listener{ - "listener-80-6": { - Source: listener806, - Valid: false, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - Conditions: []conditions.Condition{ - conditions.NewListenerUnsupportedValue(invalidHostnameMsg), - }, - }, - "listener-443-4": { - Source: listener4434, - Valid: false, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - Conditions: []conditions.Condition{ - conditions.NewListenerUnsupportedValue(invalidHostnameMsg), - }, - }, - }, - name: "invalid hostnames", - }, - { - gateway: &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: gcName, - Listeners: []v1beta1.Listener{ - listener4435, - }, - }, - }, - expected: map[string]*listener{ - "listener-443-5": { - Source: listener4435, - Valid: false, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - Conditions: conditions.NewListenerInvalidCertificateRef("Failed to get the certificate " + - "test/does-not-exist: secret test/does-not-exist does not exist"), - }, - }, - name: "invalid https listener (secret does not exist)", - }, - { - gateway: &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: gcName, - Listeners: []v1beta1.Listener{ - listener801, listener803, - listener4431, listener4432, - }, - }, - }, - expected: map[string]*listener{ - "listener-80-1": { - Source: listener801, - Valid: true, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - }, - "listener-80-3": { - Source: listener803, - Valid: true, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - }, - "listener-443-1": { - Source: listener4431, - Valid: true, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - SecretPath: secretPath, - }, - "listener-443-2": { - Source: listener4432, - Valid: true, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - SecretPath: secretPath, - }, - }, - name: "multiple valid http/https listeners", - }, - { - gateway: &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: gcName, - Listeners: []v1beta1.Listener{ - listener801, listener804, - listener4431, listener4433, - }, - }, - }, - expected: map[string]*listener{ - "listener-80-1": { - Source: listener801, - Valid: false, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - Conditions: conditions.NewListenerConflictedHostname(conflictedHostnamesMsg), - }, - "listener-80-4": { - Source: listener804, - Valid: false, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - Conditions: conditions.NewListenerConflictedHostname(conflictedHostnamesMsg), - }, - "listener-443-1": { - Source: listener4431, - Valid: false, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - Conditions: conditions.NewListenerConflictedHostname(conflictedHostnamesMsg), - }, - "listener-443-3": { - Source: listener4433, - Valid: false, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - Conditions: conditions.NewListenerConflictedHostname(conflictedHostnamesMsg), - }, - }, - name: "collisions", - }, - { - gateway: &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: gcName, - Listeners: []v1beta1.Listener{ - listener801, - listener4431, - }, - Addresses: []v1beta1.GatewayAddress{ - {}, - }, - }, - }, - expected: map[string]*listener{ - "listener-80-1": { - Source: listener801, - Valid: false, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - Conditions: []conditions.Condition{ - conditions.NewListenerUnsupportedAddress("Specifying Gateway addresses is not supported"), - }, - }, - "listener-443-1": { - Source: listener4431, - Valid: false, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - SecretPath: "", - Conditions: []conditions.Condition{ - conditions.NewListenerUnsupportedAddress("Specifying Gateway addresses is not supported"), - }, - }, - }, - name: "gateway addresses are not supported", - }, - { - gateway: nil, - expected: map[string]*listener{}, - name: "no gateway", - }, - { - gateway: &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - }, - Spec: v1beta1.GatewaySpec{ - GatewayClassName: "wrong-class", - Listeners: []v1beta1.Listener{ - listener801, listener804, - }, - }, - }, - expected: map[string]*listener{}, - name: "wrong gatewayclass", - }, - } - - // add secret to store - secretStore := NewSecretStore() - secretStore.Upsert(testSecret) - - secretMemoryMgr := NewSecretDiskMemoryManager(secretsDirectory, secretStore) - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - g := NewGomegaWithT(t) - - result := buildListeners(test.gateway, gcName, secretMemoryMgr) - g.Expect(helpers.Diff(test.expected, result)).To(BeEmpty()) - }) - } -} - -func TestBindRouteToListeners(t *testing.T) { - createRoute := func(hostname string, parentRefs ...v1beta1.ParentReference) *v1beta1.HTTPRoute { - return &v1beta1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: "hr-1", - }, - Spec: v1beta1.HTTPRouteSpec{ - CommonRouteSpec: v1beta1.CommonRouteSpec{ - ParentRefs: parentRefs, - }, - Hostnames: []v1beta1.Hostname{ - v1beta1.Hostname(hostname), - }, - }, - } - } - - hrNonExistingSectionName := createRoute("foo.example.com", v1beta1.ParentReference{ - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), - Name: "gateway", - SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer("listener-80-2")), - }) - - hrEmptySectionName := createRoute("foo.example.com", v1beta1.ParentReference{ - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), - Name: "gateway", - }) - - hrIgnoredGateway := createRoute("foo.example.com", v1beta1.ParentReference{ - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), - Name: "ignored-gateway", - SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer("listener-80-1")), - }) - - hrFoo := createRoute("foo.example.com", v1beta1.ParentReference{ - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), - Name: "gateway", - SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer("listener-80-1")), - }) - - hrFooImplicitNamespace := createRoute("foo.example.com", v1beta1.ParentReference{ - Name: "gateway", - SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer("listener-80-1")), - }) - - hrBar := createRoute("bar.example.com", v1beta1.ParentReference{ - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), - Name: "gateway", - SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer("listener-80-1")), - }) - - // we create a new listener each time because the function under test can modify it - createListener := func() *listener { - return &listener{ - Source: v1beta1.Listener{ - Hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), - }, - Valid: true, - Routes: map[types.NamespacedName]*route{}, - AcceptedHostnames: map[string]struct{}{}, - } - } - - createModifiedListener := func(m func(*listener)) *listener { - l := createListener() - m(l) - return l - } - - gw := &v1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: "gateway", - }, - } - - tests := []struct { - httpRoute *v1beta1.HTTPRoute - gw *v1beta1.Gateway - ignoredGws map[types.NamespacedName]*v1beta1.Gateway - listeners map[string]*listener - expectedRoute *route - expectedListeners map[string]*listener - msg string - expectedIgnored bool - }{ - { - httpRoute: createRoute("foo.example.com"), - gw: gw, - ignoredGws: nil, - listeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - expectedIgnored: true, - expectedRoute: nil, - expectedListeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - msg: "HTTPRoute without parent refs", - }, - { - httpRoute: createRoute("foo.example.com", v1beta1.ParentReference{ - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("test")), - Name: "some-gateway", // wrong gateway - SectionName: (*v1beta1.SectionName)(helpers.GetStringPointer("listener-1")), - }), - gw: gw, - ignoredGws: nil, - listeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - expectedIgnored: true, - expectedRoute: nil, - expectedListeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - msg: "HTTPRoute without good parent refs", - }, - { - httpRoute: hrNonExistingSectionName, - gw: gw, - ignoredGws: nil, - listeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - expectedIgnored: false, - expectedRoute: &route{ - Source: hrNonExistingSectionName, - ValidSectionNameRefs: map[string]struct{}{}, - InvalidSectionNameRefs: map[string]conditions.Condition{ - "listener-80-2": conditions.NewTODO("listener is not found"), - }, - }, - expectedListeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - msg: "HTTPRoute with non-existing section name", - }, - { - httpRoute: hrEmptySectionName, - gw: gw, - ignoredGws: nil, - listeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - expectedIgnored: true, - expectedRoute: nil, - expectedListeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - msg: "HTTPRoute with empty section name", - }, - { - httpRoute: hrFoo, - gw: gw, - ignoredGws: nil, - listeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - expectedIgnored: false, - expectedRoute: &route{ - Source: hrFoo, - ValidSectionNameRefs: map[string]struct{}{ - "listener-80-1": {}, - }, - InvalidSectionNameRefs: map[string]conditions.Condition{}, - }, - expectedListeners: map[string]*listener{ - "listener-80-1": createModifiedListener(func(l *listener) { - l.Routes = map[types.NamespacedName]*route{ - {Namespace: "test", Name: "hr-1"}: { - Source: hrFoo, - ValidSectionNameRefs: map[string]struct{}{ - "listener-80-1": {}, - }, - InvalidSectionNameRefs: map[string]conditions.Condition{}, - }, - } - l.AcceptedHostnames = map[string]struct{}{ - "foo.example.com": {}, - } - }), - }, - msg: "HTTPRoute with one accepted hostname", - }, - { - httpRoute: hrFooImplicitNamespace, - gw: gw, - ignoredGws: nil, - listeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - expectedIgnored: false, - expectedRoute: &route{ - Source: hrFooImplicitNamespace, - ValidSectionNameRefs: map[string]struct{}{ - "listener-80-1": {}, - }, - InvalidSectionNameRefs: map[string]conditions.Condition{}, - }, - expectedListeners: map[string]*listener{ - "listener-80-1": createModifiedListener(func(l *listener) { - l.Routes = map[types.NamespacedName]*route{ - {Namespace: "test", Name: "hr-1"}: { - Source: hrFooImplicitNamespace, - ValidSectionNameRefs: map[string]struct{}{ - "listener-80-1": {}, - }, - InvalidSectionNameRefs: map[string]conditions.Condition{}, - }, - } - l.AcceptedHostnames = map[string]struct{}{ - "foo.example.com": {}, - } - }), - }, - msg: "HTTPRoute with one accepted hostname with implicit namespace in parentRef", - }, - { - httpRoute: hrBar, - gw: gw, - ignoredGws: nil, - listeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - expectedIgnored: false, - expectedRoute: &route{ - Source: hrBar, - ValidSectionNameRefs: map[string]struct{}{}, - InvalidSectionNameRefs: map[string]conditions.Condition{ - "listener-80-1": conditions.NewRouteNoMatchingListenerHostname(), - }, - }, - expectedListeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - msg: "HTTPRoute with zero accepted hostnames", - }, - { - httpRoute: hrIgnoredGateway, - gw: gw, - ignoredGws: map[types.NamespacedName]*v1beta1.Gateway{ - {Namespace: "test", Name: "ignored-gateway"}: {}, - }, - listeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - expectedIgnored: false, - expectedRoute: &route{ - Source: hrIgnoredGateway, - ValidSectionNameRefs: map[string]struct{}{}, - InvalidSectionNameRefs: map[string]conditions.Condition{ - "listener-80-1": conditions.NewTODO("Gateway is ignored"), - }, - }, - expectedListeners: map[string]*listener{ - "listener-80-1": createListener(), - }, - msg: "HTTPRoute with ignored gateway reference", - }, - { - httpRoute: hrFoo, - gw: nil, - ignoredGws: nil, - listeners: nil, - expectedIgnored: true, - expectedRoute: nil, - expectedListeners: nil, - msg: "HTTPRoute when no gateway exists", - }, - { - httpRoute: hrFoo, - gw: gw, - ignoredGws: nil, - listeners: map[string]*listener{ - "listener-80-1": createModifiedListener(func(l *listener) { - l.Valid = false - }), - }, - expectedIgnored: false, - expectedRoute: &route{ - Source: hrFoo, - ValidSectionNameRefs: map[string]struct{}{}, - InvalidSectionNameRefs: map[string]conditions.Condition{ - "listener-80-1": conditions.NewRouteInvalidListener(), - }, - }, - expectedListeners: map[string]*listener{ - "listener-80-1": createModifiedListener(func(l *listener) { - l.Valid = false - }), - }, - msg: "HTTPRoute with invalid listener parentRef", - }, - } - - for _, test := range tests { - ignored, route := bindHTTPRouteToListeners(test.httpRoute, test.gw, test.ignoredGws, test.listeners) - if diff := cmp.Diff(test.expectedIgnored, ignored); diff != "" { - t.Errorf("bindHTTPRouteToListeners() %q mismatch on ignored (-want +got):\n%s", test.msg, diff) - } - if diff := cmp.Diff(test.expectedRoute, route); diff != "" { - t.Errorf("bindHTTPRouteToListeners() %q mismatch on route (-want +got):\n%s", test.msg, diff) - } - if diff := cmp.Diff(test.expectedListeners, test.listeners); diff != "" { - t.Errorf("bindHTTPRouteToListeners() %q mismatch on listeners (-want +got):\n%s", test.msg, diff) - } - } -} - -func TestFindAcceptedHostnames(t *testing.T) { - var listenerHostnameFoo v1beta1.Hostname = "foo.example.com" - var listenerHostnameCafe v1beta1.Hostname = "cafe.example.com" - routeHostnames := []v1beta1.Hostname{"foo.example.com", "bar.example.com"} - - tests := []struct { - listenerHostname *v1beta1.Hostname - msg string - routeHostnames []v1beta1.Hostname - expected []string - }{ - { - listenerHostname: &listenerHostnameFoo, - routeHostnames: routeHostnames, - expected: []string{"foo.example.com"}, - msg: "one match", - }, - { - listenerHostname: &listenerHostnameCafe, - routeHostnames: routeHostnames, - expected: nil, - msg: "no match", - }, - { - listenerHostname: nil, - routeHostnames: routeHostnames, - expected: []string{"foo.example.com", "bar.example.com"}, - msg: "nil listener hostname", - }, - } - - for _, test := range tests { - result := findAcceptedHostnames(test.listenerHostname, test.routeHostnames) - if diff := cmp.Diff(test.expected, result); diff != "" { - t.Errorf("findAcceptedHostnames() %q mismatch (-want +got):\n%s", test.msg, diff) - } - } -} - -func TestGetHostname(t *testing.T) { - var emptyHostname v1beta1.Hostname - var hostname v1beta1.Hostname = "example.com" - - tests := []struct { - h *v1beta1.Hostname - expected string - msg string - }{ - { - h: nil, - expected: "", - msg: "nil hostname", - }, - { - h: &emptyHostname, - expected: "", - msg: "empty hostname", - }, - { - h: &hostname, - expected: string(hostname), - msg: "normal hostname", - }, - } - - for _, test := range tests { - result := getHostname(test.h) - if result != test.expected { - t.Errorf("getHostname() returned %q but expected %q for the case of %q", result, test.expected, test.msg) - } - } -} - -func TestValidateGatewayClass(t *testing.T) { - gc := &v1beta1.GatewayClass{ - Spec: v1beta1.GatewayClassSpec{ - ControllerName: "test.controller", - }, - } - - err := validateGatewayClass(gc, "test.controller") - if err != nil { - t.Errorf("validateGatewayClass() returned unexpected error %v", err) - } - - err = validateGatewayClass(gc, "unmatched.controller") - if err == nil { - t.Errorf("validateGatewayClass() didn't return an error") - } -} diff --git a/internal/state/listener_test.go b/internal/state/listener_test.go deleted file mode 100644 index 3eb58f4dd..000000000 --- a/internal/state/listener_test.go +++ /dev/null @@ -1,236 +0,0 @@ -package state - -import ( - "testing" - - . "github.com/onsi/gomega" - "sigs.k8s.io/gateway-api/apis/v1beta1" - - "github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/conditions" -) - -func TestValidateHTTPListener(t *testing.T) { - tests := []struct { - l v1beta1.Listener - name string - expected []conditions.Condition - }{ - { - l: v1beta1.Listener{ - Port: 80, - }, - expected: nil, - name: "valid", - }, - { - l: v1beta1.Listener{ - Port: 81, - }, - expected: []conditions.Condition{ - conditions.NewListenerPortUnavailable("Port 81 is not supported for HTTP, use 80"), - }, - name: "invalid port", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - g := NewGomegaWithT(t) - - result := validateHTTPListener(test.l) - g.Expect(result).To(Equal(test.expected)) - }) - } -} - -func TestValidateHTTPSListener(t *testing.T) { - gwNs := "gateway-ns" - - validSecretRef := v1beta1.SecretObjectReference{ - Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Secret")), - Name: "secret", - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer(gwNs)), - } - - invalidSecretRefGroup := v1beta1.SecretObjectReference{ - Group: (*v1beta1.Group)(helpers.GetStringPointer("some-group")), - Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Secret")), - Name: "secret", - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer(gwNs)), - } - - invalidSecretRefKind := v1beta1.SecretObjectReference{ - Kind: (*v1beta1.Kind)(helpers.GetStringPointer("ConfigMap")), - Name: "secret", - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer(gwNs)), - } - - invalidSecretRefTNamespace := v1beta1.SecretObjectReference{ - Kind: (*v1beta1.Kind)(helpers.GetStringPointer("Secret")), - Name: "secret", - Namespace: (*v1beta1.Namespace)(helpers.GetStringPointer("diff-ns")), - } - - tests := []struct { - l v1beta1.Listener - name string - expected []conditions.Condition - }{ - { - l: v1beta1.Listener{ - Port: 443, - TLS: &v1beta1.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), - CertificateRefs: []v1beta1.SecretObjectReference{validSecretRef}, - }, - }, - expected: nil, - name: "valid", - }, - { - l: v1beta1.Listener{ - Port: 80, - TLS: &v1beta1.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), - CertificateRefs: []v1beta1.SecretObjectReference{validSecretRef}, - }, - }, - expected: []conditions.Condition{ - conditions.NewListenerPortUnavailable("Port 80 is not supported for HTTPS, use 443"), - }, - name: "invalid port", - }, - { - l: v1beta1.Listener{ - Port: 443, - TLS: &v1beta1.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), - CertificateRefs: []v1beta1.SecretObjectReference{validSecretRef}, - Options: map[v1beta1.AnnotationKey]v1beta1.AnnotationValue{"key": "val"}, - }, - }, - expected: []conditions.Condition{ - conditions.NewListenerUnsupportedValue("tls.options are not supported"), - }, - name: "invalid options", - }, - { - l: v1beta1.Listener{ - Port: 443, - TLS: &v1beta1.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1beta1.TLSModePassthrough), - CertificateRefs: []v1beta1.SecretObjectReference{validSecretRef}, - }, - }, - expected: []conditions.Condition{ - conditions.NewListenerUnsupportedValue(`tls.mode "Passthrough" is not supported, use "Terminate"`), - }, - name: "invalid tls mode", - }, - { - l: v1beta1.Listener{ - Port: 443, - TLS: &v1beta1.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), - CertificateRefs: []v1beta1.SecretObjectReference{invalidSecretRefGroup}, - }, - }, - expected: conditions.NewListenerInvalidCertificateRef(`Group must be empty, got "some-group"`), - name: "invalid cert ref group", - }, - { - l: v1beta1.Listener{ - Port: 443, - TLS: &v1beta1.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), - CertificateRefs: []v1beta1.SecretObjectReference{invalidSecretRefKind}, - }, - }, - expected: conditions.NewListenerInvalidCertificateRef(`Kind must be Secret, got "ConfigMap"`), - name: "invalid cert ref kind", - }, - { - l: v1beta1.Listener{ - Port: 443, - TLS: &v1beta1.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), - CertificateRefs: []v1beta1.SecretObjectReference{invalidSecretRefTNamespace}, - }, - }, - expected: conditions.NewListenerInvalidCertificateRef("Referenced Secret must belong to the same " + - "namespace as the Gateway"), - name: "invalid cert ref namespace", - }, - { - l: v1beta1.Listener{ - Port: 443, - TLS: &v1beta1.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1beta1.TLSModeTerminate), - CertificateRefs: []v1beta1.SecretObjectReference{validSecretRef, validSecretRef}, - }, - }, - expected: []conditions.Condition{ - conditions.NewListenerUnsupportedValue("Only 1 certificateRef is supported, got 2"), - }, - name: "too many cert refs", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - g := NewGomegaWithT(t) - - result := validateHTTPSListener(test.l, gwNs) - g.Expect(result).To(Equal(test.expected)) - }) - } -} - -func TestValidateListenerHostname(t *testing.T) { - tests := []struct { - hostname *v1beta1.Hostname - name string - expectErr bool - }{ - { - hostname: nil, - expectErr: false, - name: "nil hostname", - }, - { - hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("")), - expectErr: false, - name: "empty hostname", - }, - { - hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("foo.example.com")), - expectErr: false, - name: "valid hostname", - }, - { - hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("*.example.com")), - expectErr: true, - name: "wildcard hostname", - }, - { - hostname: (*v1beta1.Hostname)(helpers.GetStringPointer("example$com")), - expectErr: true, - name: "invalid hostname", - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - g := NewGomegaWithT(t) - - err := validateListenerHostname(test.hostname) - - if test.expectErr { - g.Expect(err).ToNot(BeNil()) - } else { - g.Expect(err).To(BeNil()) - } - }) - } -} diff --git a/internal/state/file_manager.go b/internal/state/secrets/file_manager.go similarity index 97% rename from internal/state/file_manager.go rename to internal/state/secrets/file_manager.go index 672855fd4..3daf6d50b 100644 --- a/internal/state/file_manager.go +++ b/internal/state/secrets/file_manager.go @@ -1,4 +1,4 @@ -package state +package secrets import ( "io/fs" diff --git a/internal/state/secrets.go b/internal/state/secrets/secrets.go similarity index 99% rename from internal/state/secrets.go rename to internal/state/secrets/secrets.go index b5d2e12a8..4096aec08 100644 --- a/internal/state/secrets.go +++ b/internal/state/secrets/secrets.go @@ -1,4 +1,4 @@ -package state +package secrets import ( "bytes" diff --git a/internal/state/secrets_test.go b/internal/state/secrets/secrets_test.go similarity index 90% rename from internal/state/secrets_test.go rename to internal/state/secrets/secrets_test.go index e41faac4c..d24e2d8ad 100644 --- a/internal/state/secrets_test.go +++ b/internal/state/secrets/secrets_test.go @@ -1,5 +1,5 @@ // nolint:gosec -package state_test +package secrets_test import ( "errors" @@ -13,8 +13,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/statefakes" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets/secretsfakes" ) var ( @@ -136,8 +136,8 @@ var ( var _ = Describe("SecretDiskMemoryManager", func() { var ( - fakeStore *statefakes.FakeSecretStore - memMgr state.SecretDiskMemoryManager + fakeStore *secretsfakes.FakeSecretStore + memMgr secrets.SecretDiskMemoryManager tmpSecretsDir string ) @@ -146,8 +146,8 @@ var _ = Describe("SecretDiskMemoryManager", func() { tmpSecretsDir = dir Expect(err).ToNot(HaveOccurred(), "failed to create temp directory for tests") - fakeStore = &statefakes.FakeSecretStore{} - memMgr = state.NewSecretDiskMemoryManager(tmpSecretsDir, fakeStore) + fakeStore = &secretsfakes.FakeSecretStore{} + memMgr = secrets.NewSecretDiskMemoryManager(tmpSecretsDir, fakeStore) }) AfterEach(OncePerOrdered, func() { @@ -174,21 +174,21 @@ var _ = Describe("SecretDiskMemoryManager", func() { testRequest(secret1, "", true) }) It("request should return the file path for a valid secret", func() { - fakeStore.GetReturns(&state.Secret{Secret: secret1, Valid: true}) + fakeStore.GetReturns(&secrets.Secret{Secret: secret1, Valid: true}) expectedPath := path.Join(tmpSecretsDir, "test_secret1") testRequest(secret1, expectedPath, false) }) It("request should return the file path for another valid secret", func() { - fakeStore.GetReturns(&state.Secret{Secret: secret2, Valid: true}) + fakeStore.GetReturns(&secrets.Secret{Secret: secret2, Valid: true}) expectedPath := path.Join(tmpSecretsDir, "test_secret2") testRequest(secret2, expectedPath, false) }) It("request should return an error and empty path when secret is invalid", func() { - fakeStore.GetReturns(&state.Secret{Secret: invalidSecretType, Valid: false}) + fakeStore.GetReturns(&secrets.Secret{Secret: invalidSecretType, Valid: false}) testRequest(invalidSecretType, "", true) }) @@ -210,7 +210,7 @@ var _ = Describe("SecretDiskMemoryManager", func() { }) It("request should return the file path for secret after write", func() { - fakeStore.GetReturns(&state.Secret{Secret: secret3, Valid: true}) + fakeStore.GetReturns(&secrets.Secret{Secret: secret3, Valid: true}) expectedPath := path.Join(tmpSecretsDir, "test_secret3") testRequest(secret3, expectedPath, false) @@ -244,20 +244,20 @@ var _ = Describe("SecretDiskMemoryManager", func() { }) Describe("Write all requested secrets", func() { var ( - fakeFileManager *statefakes.FakeFileManager - fakeStore *statefakes.FakeSecretStore + fakeFileManager *secretsfakes.FakeFileManager + fakeStore *secretsfakes.FakeSecretStore fakeDirEntries []fs.DirEntry - memMgr *state.SecretDiskMemoryManagerImpl + memMgr *secrets.SecretDiskMemoryManagerImpl ) BeforeEach(OncePerOrdered, func() { - fakeFileManager = &statefakes.FakeFileManager{} - fakeStore = &statefakes.FakeSecretStore{} - fakeDirEntries = []fs.DirEntry{&statefakes.FakeDirEntry{}} - memMgr = state.NewSecretDiskMemoryManager("", fakeStore, state.WithSecretFileManager(fakeFileManager)) + fakeFileManager = &secretsfakes.FakeFileManager{} + fakeStore = &secretsfakes.FakeSecretStore{} + fakeDirEntries = []fs.DirEntry{&secretsfakes.FakeDirEntry{}} + memMgr = secrets.NewSecretDiskMemoryManager("", fakeStore, secrets.WithSecretFileManager(fakeFileManager)) // populate a requested secret - fakeStore.GetReturns(&state.Secret{Secret: secret1, Valid: true}) + fakeStore.GetReturns(&secrets.Secret{Secret: secret1, Valid: true}) _, err := memMgr.Request(types.NamespacedName{Namespace: secret1.Namespace, Name: secret1.Name}) Expect(err).ToNot(HaveOccurred()) }) @@ -298,11 +298,11 @@ var _ = Describe("SecretDiskMemoryManager", func() { }) var _ = Describe("SecretStore", func() { - var store state.SecretStore + var store secrets.SecretStore var invalidToValidSecret, validToInvalidSecret *apiv1.Secret BeforeEach(OncePerOrdered, func() { - store = state.NewSecretStore() + store = secrets.NewSecretStore() invalidToValidSecret = invalidSecretType.DeepCopy() invalidToValidSecret.Type = apiv1.SecretTypeTLS diff --git a/internal/state/statefakes/fake_dir_entry.go b/internal/state/secrets/secretsfakes/fake_dir_entry.go similarity index 99% rename from internal/state/statefakes/fake_dir_entry.go rename to internal/state/secrets/secretsfakes/fake_dir_entry.go index 3ec2321a1..ee1ba1342 100644 --- a/internal/state/statefakes/fake_dir_entry.go +++ b/internal/state/secrets/secretsfakes/fake_dir_entry.go @@ -1,5 +1,5 @@ // Code generated by counterfeiter. DO NOT EDIT. -package statefakes +package secretsfakes import ( "io/fs" diff --git a/internal/state/statefakes/fake_file_manager.go b/internal/state/secrets/secretsfakes/fake_file_manager.go similarity index 98% rename from internal/state/statefakes/fake_file_manager.go rename to internal/state/secrets/secretsfakes/fake_file_manager.go index dbde4d6b9..3c4e958f3 100644 --- a/internal/state/statefakes/fake_file_manager.go +++ b/internal/state/secrets/secretsfakes/fake_file_manager.go @@ -1,12 +1,12 @@ // Code generated by counterfeiter. DO NOT EDIT. -package statefakes +package secretsfakes import ( "io/fs" "os" "sync" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets" ) type FakeFileManager struct { @@ -425,4 +425,4 @@ func (fake *FakeFileManager) recordInvocation(key string, args []interface{}) { fake.invocations[key] = append(fake.invocations[key], args) } -var _ state.FileManager = new(FakeFileManager) +var _ secrets.FileManager = new(FakeFileManager) diff --git a/internal/state/statefakes/fake_secret_disk_memory_manager.go b/internal/state/secrets/secretsfakes/fake_secret_disk_memory_manager.go similarity index 97% rename from internal/state/statefakes/fake_secret_disk_memory_manager.go rename to internal/state/secrets/secretsfakes/fake_secret_disk_memory_manager.go index d7e604154..3fe9f5943 100644 --- a/internal/state/statefakes/fake_secret_disk_memory_manager.go +++ b/internal/state/secrets/secretsfakes/fake_secret_disk_memory_manager.go @@ -1,10 +1,10 @@ // Code generated by counterfeiter. DO NOT EDIT. -package statefakes +package secretsfakes import ( "sync" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets" "k8s.io/apimachinery/pkg/types" ) @@ -179,4 +179,4 @@ func (fake *FakeSecretDiskMemoryManager) recordInvocation(key string, args []int fake.invocations[key] = append(fake.invocations[key], args) } -var _ state.SecretDiskMemoryManager = new(FakeSecretDiskMemoryManager) +var _ secrets.SecretDiskMemoryManager = new(FakeSecretDiskMemoryManager) diff --git a/internal/state/statefakes/fake_secret_store.go b/internal/state/secrets/secretsfakes/fake_secret_store.go similarity index 88% rename from internal/state/statefakes/fake_secret_store.go rename to internal/state/secrets/secretsfakes/fake_secret_store.go index a74c93c74..d6538e008 100644 --- a/internal/state/statefakes/fake_secret_store.go +++ b/internal/state/secrets/secretsfakes/fake_secret_store.go @@ -1,10 +1,10 @@ // Code generated by counterfeiter. DO NOT EDIT. -package statefakes +package secretsfakes import ( "sync" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/secrets" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) @@ -15,16 +15,16 @@ type FakeSecretStore struct { deleteArgsForCall []struct { arg1 types.NamespacedName } - GetStub func(types.NamespacedName) *state.Secret + GetStub func(types.NamespacedName) *secrets.Secret getMutex sync.RWMutex getArgsForCall []struct { arg1 types.NamespacedName } getReturns struct { - result1 *state.Secret + result1 *secrets.Secret } getReturnsOnCall map[int]struct { - result1 *state.Secret + result1 *secrets.Secret } UpsertStub func(*v1.Secret) upsertMutex sync.RWMutex @@ -67,7 +67,7 @@ func (fake *FakeSecretStore) DeleteArgsForCall(i int) types.NamespacedName { return argsForCall.arg1 } -func (fake *FakeSecretStore) Get(arg1 types.NamespacedName) *state.Secret { +func (fake *FakeSecretStore) Get(arg1 types.NamespacedName) *secrets.Secret { fake.getMutex.Lock() ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)] fake.getArgsForCall = append(fake.getArgsForCall, struct { @@ -92,7 +92,7 @@ func (fake *FakeSecretStore) GetCallCount() int { return len(fake.getArgsForCall) } -func (fake *FakeSecretStore) GetCalls(stub func(types.NamespacedName) *state.Secret) { +func (fake *FakeSecretStore) GetCalls(stub func(types.NamespacedName) *secrets.Secret) { fake.getMutex.Lock() defer fake.getMutex.Unlock() fake.GetStub = stub @@ -105,26 +105,26 @@ func (fake *FakeSecretStore) GetArgsForCall(i int) types.NamespacedName { return argsForCall.arg1 } -func (fake *FakeSecretStore) GetReturns(result1 *state.Secret) { +func (fake *FakeSecretStore) GetReturns(result1 *secrets.Secret) { fake.getMutex.Lock() defer fake.getMutex.Unlock() fake.GetStub = nil fake.getReturns = struct { - result1 *state.Secret + result1 *secrets.Secret }{result1} } -func (fake *FakeSecretStore) GetReturnsOnCall(i int, result1 *state.Secret) { +func (fake *FakeSecretStore) GetReturnsOnCall(i int, result1 *secrets.Secret) { fake.getMutex.Lock() defer fake.getMutex.Unlock() fake.GetStub = nil if fake.getReturnsOnCall == nil { fake.getReturnsOnCall = make(map[int]struct { - result1 *state.Secret + result1 *secrets.Secret }) } fake.getReturnsOnCall[i] = struct { - result1 *state.Secret + result1 *secrets.Secret }{result1} } @@ -188,4 +188,4 @@ func (fake *FakeSecretStore) recordInvocation(key string, args []interface{}) { fake.invocations[key] = append(fake.invocations[key], args) } -var _ state.SecretStore = new(FakeSecretStore) +var _ secrets.SecretStore = new(FakeSecretStore) diff --git a/internal/state/statefakes/fake_change_processor.go b/internal/state/statefakes/fake_change_processor.go index 83f60e7b0..af7a55b62 100644 --- a/internal/state/statefakes/fake_change_processor.go +++ b/internal/state/statefakes/fake_change_processor.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/dataplane" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -22,19 +23,19 @@ type FakeChangeProcessor struct { captureUpsertChangeArgsForCall []struct { arg1 client.Object } - ProcessStub func(context.Context) (bool, state.Configuration, state.Statuses) + ProcessStub func(context.Context) (bool, dataplane.Configuration, state.Statuses) processMutex sync.RWMutex processArgsForCall []struct { arg1 context.Context } processReturns struct { result1 bool - result2 state.Configuration + result2 dataplane.Configuration result3 state.Statuses } processReturnsOnCall map[int]struct { result1 bool - result2 state.Configuration + result2 dataplane.Configuration result3 state.Statuses } invocations map[string][][]interface{} @@ -106,7 +107,7 @@ func (fake *FakeChangeProcessor) CaptureUpsertChangeArgsForCall(i int) client.Ob return argsForCall.arg1 } -func (fake *FakeChangeProcessor) Process(arg1 context.Context) (bool, state.Configuration, state.Statuses) { +func (fake *FakeChangeProcessor) Process(arg1 context.Context) (bool, dataplane.Configuration, state.Statuses) { fake.processMutex.Lock() ret, specificReturn := fake.processReturnsOnCall[len(fake.processArgsForCall)] fake.processArgsForCall = append(fake.processArgsForCall, struct { @@ -131,7 +132,7 @@ func (fake *FakeChangeProcessor) ProcessCallCount() int { return len(fake.processArgsForCall) } -func (fake *FakeChangeProcessor) ProcessCalls(stub func(context.Context) (bool, state.Configuration, state.Statuses)) { +func (fake *FakeChangeProcessor) ProcessCalls(stub func(context.Context) (bool, dataplane.Configuration, state.Statuses)) { fake.processMutex.Lock() defer fake.processMutex.Unlock() fake.ProcessStub = stub @@ -144,31 +145,31 @@ func (fake *FakeChangeProcessor) ProcessArgsForCall(i int) context.Context { return argsForCall.arg1 } -func (fake *FakeChangeProcessor) ProcessReturns(result1 bool, result2 state.Configuration, result3 state.Statuses) { +func (fake *FakeChangeProcessor) ProcessReturns(result1 bool, result2 dataplane.Configuration, result3 state.Statuses) { fake.processMutex.Lock() defer fake.processMutex.Unlock() fake.ProcessStub = nil fake.processReturns = struct { result1 bool - result2 state.Configuration + result2 dataplane.Configuration result3 state.Statuses }{result1, result2, result3} } -func (fake *FakeChangeProcessor) ProcessReturnsOnCall(i int, result1 bool, result2 state.Configuration, result3 state.Statuses) { +func (fake *FakeChangeProcessor) ProcessReturnsOnCall(i int, result1 bool, result2 dataplane.Configuration, result3 state.Statuses) { fake.processMutex.Lock() defer fake.processMutex.Unlock() fake.ProcessStub = nil if fake.processReturnsOnCall == nil { fake.processReturnsOnCall = make(map[int]struct { result1 bool - result2 state.Configuration + result2 dataplane.Configuration result3 state.Statuses }) } fake.processReturnsOnCall[i] = struct { result1 bool - result2 state.Configuration + result2 dataplane.Configuration result3 state.Statuses }{result1, result2, result3} } diff --git a/internal/state/statuses.go b/internal/state/statuses.go index 3cbd48fb9..4a3076831 100644 --- a/internal/state/statuses.go +++ b/internal/state/statuses.go @@ -5,6 +5,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/conditions" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/graph" ) // ListenerStatuses holds the statuses of listeners where the key is the name of a listener in the Gateway resource. @@ -74,8 +75,8 @@ type GatewayClassStatus struct { Valid bool } -// buildStatuses builds statuses from a graph. -func buildStatuses(graph *graph) Statuses { +// buildStatuses builds statuses from a Graph. +func buildStatuses(graph *graph.Graph) Statuses { statuses := Statuses{ HTTPRouteStatuses: make(map[types.NamespacedName]HTTPRouteStatus), IgnoredGatewayStatuses: make(map[types.NamespacedName]IgnoredGatewayStatus), diff --git a/internal/state/statuses_test.go b/internal/state/statuses_test.go index 0a32beff6..d4d5c9459 100644 --- a/internal/state/statuses_test.go +++ b/internal/state/statuses_test.go @@ -10,6 +10,7 @@ import ( "github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/conditions" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/graph" ) func TestBuildStatuses(t *testing.T) { @@ -18,16 +19,16 @@ func TestBuildStatuses(t *testing.T) { Status: metav1.ConditionTrue, } - listeners := map[string]*listener{ + listeners := map[string]*graph.Listener{ "listener-80-1": { Valid: true, - Routes: map[types.NamespacedName]*route{ + Routes: map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "hr-1"}: {}, }, }, } - routes := map[types.NamespacedName]*route{ + routes := map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "hr-1"}: { Source: &v1beta1.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ @@ -43,7 +44,7 @@ func TestBuildStatuses(t *testing.T) { }, } - routesAllRefsInvalid := map[types.NamespacedName]*route{ + routesAllRefsInvalid := map[types.NamespacedName]*graph.Route{ {Namespace: "test", Name: "hr-1"}: { Source: &v1beta1.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ @@ -74,19 +75,19 @@ func TestBuildStatuses(t *testing.T) { } tests := []struct { - graph *graph + graph *graph.Graph expected Statuses name string }{ { - graph: &graph{ - GatewayClass: &gatewayClass{ + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ Source: &v1beta1.GatewayClass{ ObjectMeta: metav1.ObjectMeta{Generation: 1}, }, Valid: true, }, - Gateway: &gateway{ + Gateway: &graph.Gateway{ Source: gw, Listeners: listeners, }, @@ -133,9 +134,9 @@ func TestBuildStatuses(t *testing.T) { name: "normal case", }, { - graph: &graph{ + graph: &graph.Graph{ GatewayClass: nil, - Gateway: &gateway{ + Gateway: &graph.Gateway{ Source: gw, Listeners: listeners, }, @@ -186,15 +187,15 @@ func TestBuildStatuses(t *testing.T) { name: "gatewayclass doesn't exist", }, { - graph: &graph{ - GatewayClass: &gatewayClass{ + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ Source: &v1beta1.GatewayClass{ ObjectMeta: metav1.ObjectMeta{Generation: 1}, }, Valid: false, ErrorMsg: "error", }, - Gateway: &gateway{ + Gateway: &graph.Gateway{ Source: gw, Listeners: listeners, }, @@ -249,8 +250,8 @@ func TestBuildStatuses(t *testing.T) { name: "gatewayclass is not valid", }, { - graph: &graph{ - GatewayClass: &gatewayClass{ + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ Source: &v1beta1.GatewayClass{ ObjectMeta: metav1.ObjectMeta{Generation: 1}, },