From 24664f127ed95db2832c5bdcbbb7a212127e3da9 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Wed, 6 Jul 2022 09:31:20 -0600 Subject: [PATCH 01/42] HTTPS Termination This commit adds support for HTTPS listeners with a TLS mode of Terminate. Multiple HTTPS listeners are supported provided their hostnames do not conflict. Additionally, a gateway can have an HTTP and HTTPS listener with the same hostname. Limitations: - HTTPS listeners must listen on port 443 - Supports a single reference to a Kubernetes Secret of type kubernetes.io/tls - Secret must be in the same namespace as the Gateway - Secret must be created before the HTTPRoutes are created - Secret rotation is not supported --- README.md | 6 + deploy/manifests/default-server-secret.yaml | 9 + deploy/manifests/gateway.yaml | 9 + deploy/manifests/nginx-gateway.yaml | 5 +- deploy/manifests/service/loadbalancer.yaml | 4 + examples/https-termination/README.md | 104 +++++ examples/https-termination/cafe-routes.yaml | 39 ++ examples/https-termination/cafe.yaml | 65 +++ internal/events/loop.go | 54 ++- internal/events/loop_test.go | 79 +++- internal/helpers/helpers.go | 5 + internal/implementations/secret/secret.go | 53 +++ .../implementations/secret/secret_test.go | 64 +++ internal/manager/manager.go | 31 +- internal/nginx/config/generator.go | 29 +- internal/nginx/config/generator_test.go | 117 +++++- internal/nginx/config/http.go | 7 + internal/nginx/config/template.go | 14 + internal/state/change_processor.go | 9 +- internal/state/change_processor_test.go | 250 +++++++++++- internal/state/configuration.go | 142 +++++-- internal/state/configuration_test.go | 230 ++++++++++- internal/state/graph.go | 184 +++++++-- internal/state/graph_test.go | 381 +++++++++++++++++- internal/state/secrets.go | 177 ++++++++ internal/state/secrets_test.go | 318 +++++++++++++++ .../statefakes/fake_secret_memory_manager.go | 182 +++++++++ .../state/statefakes/fake_secret_store.go | 191 +++++++++ pkg/sdk/interfaces.go | 5 + pkg/sdk/secret_controller.go | 72 ++++ 30 files changed, 2667 insertions(+), 168 deletions(-) create mode 100644 deploy/manifests/default-server-secret.yaml create mode 100644 examples/https-termination/README.md create mode 100644 examples/https-termination/cafe-routes.yaml create mode 100644 examples/https-termination/cafe.yaml create mode 100644 internal/implementations/secret/secret.go create mode 100644 internal/implementations/secret/secret_test.go create mode 100644 internal/state/secrets.go create mode 100644 internal/state/secrets_test.go create mode 100644 internal/state/statefakes/fake_secret_memory_manager.go create mode 100644 internal/state/statefakes/fake_secret_store.go create mode 100644 pkg/sdk/secret_controller.go diff --git a/README.md b/README.md index c32e2a2c8c..27a7b0fe97 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,12 @@ You can deploy NGINX Kubernetes Gateway on an existing Kubernetes 1.16+ cluster. kubectl apply -f deploy/manifests/gateway.yaml ``` +1. Create the default server secret: + + ``` + kubectl apply -f deploy/manifests/default-server-secret.yaml + ``` + ## Expose NGINX Kubernetes Gateway You can gain access to NGINX Kubernetes Gateway by creating a `NodePort` Service or a `LoadBalancer` Service. diff --git a/deploy/manifests/default-server-secret.yaml b/deploy/manifests/default-server-secret.yaml new file mode 100644 index 0000000000..c3a52bb017 --- /dev/null +++ b/deploy/manifests/default-server-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: default-server-secret + namespace: nginx-gateway +type: kubernetes.io/tls +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN2akNDQWFZQ0NRRGtpUjZ0djkyazhEQU5CZ2txaGtpRzl3MEJBUXNGQURBaE1SOHdIUVlEVlFRRERCWk8KUjBsT1dFdDFZbVZ5Ym1WMFpYTkhZWFJsZDJGNU1CNFhEVEl5TURjeE1USXlNek16TlZvWERUSXpNRGN4TVRJeQpNek16TlZvd0lURWZNQjBHQTFVRUF3d1dUa2RKVGxoTGRXSmxjbTVsZEdWelIyRjBaWGRoZVRDQ0FTSXdEUVlKCktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUs3c3ZHRFFRQ3JnSUJYUU5UWVQxdzJ2QnFQVGFJY04KRTRkbzJXQjZkSWxETjBOV2RUMGNrT0c1REhEdDFXOFEyeWlQdW0rVG9pbWplZXU0L2tURDlqOFhINlEybm9vLwpFMnY5N1JJc2I2UW5wVUIzSXo4Mjd2SzN6c3ViTERrcUI5WEszT2dYYTRacHFwNUF3Uy9EK21TQ0h1RXZmcGY5CkNNbDNxdlNKb0hEbkZPY3M0YmFQeEZVYmRqeHluYVBHR1Ftd1QvaGgzZ0t2YzJJeXZiSkV3cHdpeGIySS9DckwKc2JZRUFMcWdpMURmNWN3aVArVnlmZ1JRbVFHMWxlMnFxTzNmaWR1SjVxc3pKWDJkbU44b1ZrQUVWSmJuT0dScwpncmpaVVVaSThZelFNZ05HSlhkMmVFZXhtbzZtZDBlZlVSRXkrUGpyZXprYWFvczA3R1h5d3g4Q0F3RUFBVEFOCkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQWV2MDVQb2xZdm9uUFhaUGcvRVpndjllSThHdWdzSEJZZWE0N3ZPZ2UKRU5DN2xiM3h0RDNrTkx1UWdlYkVieVV1cks2cFpJZ2laaXpCU2hDayt6Z1dGbEppU0oreVR2TEthRitLR2NTQwpRcW9pcVZZems5UFRzb1JPUVR3R3hGWkFwd3hkUTRKSThya0YyS0VmRHF0aWNESktYTVQrYUttZ2owTUR1ckxSCnYvTHRVbWZ0UjVSajVyeWEydHN3eE5mN2tzMTdJbGJXN2FSeHN5UExYdkJkNmd3c1B0Y1VNa0xkWEY4TElkdlYKRDNyRkRneTdlUWlXb3FlUTJsZnFQVjVId2t0M21NMWFWM0YrSmsvdXFyNi9tSmcvOE1rWlNBTHlQWmUyMVpFQgo3WnpkQnVVcHNaVUJMUytMaENGVHJyMTh2SGJNamhhSDFtQWFpNmxuQWlIaE13PT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktZd2dnU2lBZ0VBQW9JQkFRQ3U3THhnMEVBcTRDQVYKMERVMkU5Y05yd2FqMDJpSERST0hhTmxnZW5TSlF6ZERWblU5SEpEaHVReHc3ZFZ2RU5zb2o3cHZrNklwbzNucgp1UDVFdy9ZL0Z4K2tOcDZLUHhOci9lMFNMRytrSjZWQWR5TS9OdTd5dDg3TG15dzVLZ2ZWeXR6b0YydUdhYXFlClFNRXZ3L3BrZ2g3aEwzNlgvUWpKZDZyMGlhQnc1eFRuTE9HMmo4UlZHM1k4Y3Ayanhoa0pzRS80WWQ0Q3IzTmkKTXIyeVJNS2NJc1c5aVB3cXk3RzJCQUM2b0l0UTMrWE1Jai9sY240RVVKa0J0Wlh0cXFqdDM0bmJpZWFyTXlWOQpuWmpmS0ZaQUJGU1c1emhrYklLNDJWRkdTUEdNMERJRFJpVjNkbmhIc1pxT3BuZEhuMUVSTXZqNDYzczVHbXFMCk5PeGw4c01mQWdNQkFBRUNnZ0VBZm9CLy96ZTdvQVl6emZLaitMYkNhSWZ5TWxuNkZ1alMvYk5LdVNYMXp5cUgKOWErNTIzY2tJOGx5Z056TzVLSjVDODFkazhGZG5lVTJqODFhUFJyR28zdXlpMHhndlRPK2RQUFBGYnlEQkdFVApkaHB5cUEydkltTGhMNGZKcEpHTDF3WDlXZTlOK0lmRU51dzNpYmFlQnorKzJ6VkF4T1BlRGV6MytoN3BvNXVYCnVaMC9PVzRnNGxKZlphMm5LWWFzbi80Z0lxS1J0M0NCVmxyakZCVVVXUmxIUEFPdjNFaGlIWGRPWXBxNTM0MFEKazBQbkltcS9FajBEbEluN3U4SDdvbE9Ec1YxUUNwSCtJY2srUnlJWlJQVXFMeTZOemNCOGtxL0RBeXdCT0hFbAp3SGlCc21oSm81RGZ3WHBvdER5eWNYU3Q4SFR5T1BQUTZzdFU4TDBidVFLQmdRRGRDdWVrQ1M2d0RnZVBkMXVwCjZUNzRVdktENm1iaGwrVW9pa052cDJZOENtNjBmTGJOYThzNTRXMlZFeUp3M2VFN0haSlE5M0liOWQ1UlZqWnQKUnpVQXp1MXJhV1F4TlJPTEdnZEcxc1NQQjkzRzUzOEROS2JBVWRJT2RVTit0bHhiVEZxMGdveisrZ1REMVBkRgpRcU55VkRsV0c0YWhOdGRYU3VPbU5tQXI5UUtCZ1FES2xyWVNhZWNoT2xGMmZ1NHdQcjZaZURvT1VLYlRIaTVHCkV5WTJtYm1ZQiswWC9ocHFtQXVtK3V5dWtVVkVYam9JY1hnOE0xblQ2eGFhVFpzMlZyeDd2RjBnVU1BVm5qWnEKTkVZd3ErNkZxdC9RMXM1dlc3SDZSM1h3QmlPQlhYYllwUmY0TnNXQ3o0bmZxYXNVR2xybmZqaUo5cXhWN0ZmRwpMTmdTd2pMNlF3S0JnRlBpZW92MjdCL21BeHAvK21wZDJRYldPN0N5T1A3dDdRcFloa1VPS3k4bjZtRldYdTFRCk5oeXVIeThPeHVnOFcraGFUWmVxZ0VSNkp6ZUkxempiYUJMNWRJSnB5WnNmQUY2dXJ3cEVJTzRDMXpoUHpCVEUKVzIvcTNTT2RmdExNay9vVjNPcGFEUFlLbmRwUHJOTTgrZGcrZkUvZ1BGUmNBcGJmRmN1VElTWXRBb0dBYjZ1RgpyejY3RGNEVXVLbWM1L0VlSlFCMW1BQnpCTHFGTFZGTzVoZjBpczRMcmdiK1RyV0M3c2N3QWNYSDFiak82bXFKCnFUMXhEWFJ2b0J5Wkt1bkN1YjRKNDA4L29tcjBlYlJZNEdsVmNFN1JVbytsZVJLbFYxMWVzRERpRDJRU3A3YlIKTUp3WVlWTy9IdytxWXNsb1JHUjZDK3B4OG1iMXR5SnU5R0FoczNzQ2dZQVRaT0pDWWh3RDlobDVjbk1nd3V3eApEQ3Zvbms5b2k5NkQzV2RtbGJvd2l2SFBobzRndUNUN1hrNi9pVU8rZ0pyMnRSN0tzV1N2YS9PQ2JuSTZCcHo2Ci81UzFrZzBycmhDSngyUzVWaTFZNkNjNUUzTmlGWlpLR2pKaUV5ZXMxUFd3ZmQydzJFbXM3OS9aT0NsQ0JsMjUKL0pabHQ2ellReDFCZ2pNMW5UeWZzUT09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K diff --git a/deploy/manifests/gateway.yaml b/deploy/manifests/gateway.yaml index 68294a3524..7be63e319e 100644 --- a/deploy/manifests/gateway.yaml +++ b/deploy/manifests/gateway.yaml @@ -11,3 +11,12 @@ spec: - name: http port: 80 protocol: HTTP + - name: https + port: 443 + protocol: HTTPS + tls: + mode: Terminate + certificateRefs: + - kind: Secret + name: default-server-secret + namespace: nginx-gateway diff --git a/deploy/manifests/nginx-gateway.yaml b/deploy/manifests/nginx-gateway.yaml index d7bedf4c3b..c0017a1b19 100644 --- a/deploy/manifests/nginx-gateway.yaml +++ b/deploy/manifests/nginx-gateway.yaml @@ -13,6 +13,7 @@ rules: - "" resources: - services + - secrets verbs: - list - watch @@ -80,7 +81,7 @@ spec: initContainers: - image: busybox:1.34 # FIXME(pleshakov): use gateway container to init the Config with proper main config name: nginx-config-initializer - command: [ 'sh', '-c', 'echo "load_module /usr/lib/nginx/modules/ngx_http_js_module.so; events {} pid /etc/nginx/nginx.pid; http { include /etc/nginx/conf.d/*.conf; js_import /usr/lib/nginx/modules/njs/httpmatches.js; server { default_type text/html; return 404; } }" > /etc/nginx/nginx.conf && mkdir /etc/nginx/conf.d && chown 1001:0 /etc/nginx/conf.d' ] + command: [ 'sh', '-c', 'echo "load_module /usr/lib/nginx/modules/ngx_http_js_module.so; events {} pid /etc/nginx/nginx.pid; http { include /etc/nginx/conf.d/*.conf; js_import /usr/lib/nginx/modules/njs/httpmatches.js; server { default_type text/html; return 404; } }" > /etc/nginx/nginx.conf && mkdir /etc/nginx/conf.d /etc/nginx/secrets && chown 1001:0 /etc/nginx/conf.d /etc/nginx/secrets' ] volumeMounts: - name: nginx-config mountPath: /etc/nginx @@ -105,6 +106,8 @@ spec: ports: - name: http containerPort: 80 + - name: https + containerPort: 443 volumeMounts: - name: nginx-config mountPath: /etc/nginx diff --git a/deploy/manifests/service/loadbalancer.yaml b/deploy/manifests/service/loadbalancer.yaml index c5f2b0c86c..9fddd17b1a 100644 --- a/deploy/manifests/service/loadbalancer.yaml +++ b/deploy/manifests/service/loadbalancer.yaml @@ -11,5 +11,9 @@ spec: targetPort: 80 protocol: TCP name: http + - port: 443 + targetPort: 443 + protocol: TCP + name: https selector: app: nginx-gateway diff --git a/examples/https-termination/README.md b/examples/https-termination/README.md new file mode 100644 index 0000000000..60db373671 --- /dev/null +++ b/examples/https-termination/README.md @@ -0,0 +1,104 @@ +# HTTPS Termination Example + +In this example we expand on the simple [cafe-example](../cafe-example) by adding HTTPS termination to our routes. + +## Running the Example + +## 1. Deploy NGINX Kubernetes Gateway + +1. Follow the [installation instructions](https://github.com/nginxinc/nginx-kubernetes-gateway/blob/main/README.md#run-nginx-gateway) to deploy NGINX Gateway. + +1. Save the public IP address of NGINX Kubernetes Gateway into a shell variable: + + ``` + GW_IP=XXX.YYY.ZZZ.III + ``` + +1. Save the HTTPS port of NGINX Kubernetes Gateway: + + ``` + GW_HTTPS_PORT=port + ``` + +## 2. Deploy the Cafe Application + +1. Create the coffee and the tea deployments and services: + + ``` + kubectl apply -f cafe.yaml + ``` + +1. Check that the Pods are running in the `default` namespace: + + ``` + kubectl -n default get pods + NAME READY STATUS RESTARTS AGE + coffee-6f4b79b975-2sb28 1/1 Running 0 12s + tea-6fb46d899f-fm7zr 1/1 Running 0 12s + ``` + +## 3. Configure HTTPS Termination and Routing + +HTTPS termination is configured at the gateway level with listeners. You created the following gateway resource in step 1: + +```yaml +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: Gateway +metadata: + name: gateway + namespace: nginx-gateway + labels: + domain: k8s-gateway.nginx.org +spec: + gatewayClassName: nginx + listeners: + - name: http + port: 80 + protocol: HTTP + - name: https + port: 443 + protocol: HTTPS + tls: + mode: Terminate + certificateRefs: + - kind: Secret + name: default-server-secret + namespace: nginx-gateway +``` + +The `https` listener is configured to terminate TLS connections using the `default-server-secret` in the `nginx-gateway` namespace. +To configure HTTPS termination for our cafe application, we will bind the `https` listener to our `HTTPRoutes` in [cafe-routes.yaml](./cafe-routes.yaml) using the [`parentRef`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io%2fv1alpha2.ParentReference) field: + +```yaml + parentRefs: + - name: gateway + namespace: nginx-gateway + sectionName: https +``` + +1. Create the `HTTPRoute` resources: + + ``` + kubectl apply -f cafe-routes.yaml + ``` + +## 4. Test the Application + +To access the application, we will use `curl` to send requests to the `coffee` and `tea` services. +Since our certificate is self-signed, we'll use curl's `--insecure` option to turn off certificate verification. + +To get coffee: + +``` +curl --resolve cafe.example.com:$GW_HTTPS_PORT:$GW_IP https://cafe.example.com:$GW_HTTPS_PORT/coffee --insecure +Server address: 10.12.0.18:80 +Server name: coffee-7586895968-r26zn +``` + +To get tea: + +``` +curl --resolve cafe.example.com:$GW_HTTPS_PORT:$GW_IP https://cafe.example.com:$GW_HTTPS_PORT/tea --insecure +Server address: 10.12.0.19:80 +Server name: tea-7cd44fcb4d-xfw2x +``` diff --git a/examples/https-termination/cafe-routes.yaml b/examples/https-termination/cafe-routes.yaml new file mode 100644 index 0000000000..97db79a937 --- /dev/null +++ b/examples/https-termination/cafe-routes.yaml @@ -0,0 +1,39 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: HTTPRoute +metadata: + name: coffee +spec: + parentRefs: + - name: gateway + namespace: nginx-gateway + sectionName: https + hostnames: + - "cafe.example.com" + rules: + - matches: + - path: + type: PathPrefix + value: /coffee + backendRefs: + - name: coffee + port: 80 +--- +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: HTTPRoute +metadata: + name: tea +spec: + parentRefs: + - name: gateway + namespace: nginx-gateway + sectionName: https + hostnames: + - "cafe.example.com" + rules: + - matches: + - path: + type: PathPrefix + value: /tea + backendRefs: + - name: tea + port: 80 diff --git a/examples/https-termination/cafe.yaml b/examples/https-termination/cafe.yaml new file mode 100644 index 0000000000..2d03ae59ff --- /dev/null +++ b/examples/https-termination/cafe.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: coffee +spec: + replicas: 1 + selector: + matchLabels: + app: coffee + template: + metadata: + labels: + app: coffee + spec: + containers: + - name: coffee + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: coffee +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: coffee +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tea +spec: + replicas: 1 + selector: + matchLabels: + app: tea + template: + metadata: + labels: + app: tea + spec: + containers: + - name: tea + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: tea +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: tea diff --git a/internal/events/loop.go b/internal/events/loop.go index 95f2c4cbdd..d03bfb5e93 100644 --- a/internal/events/loop.go +++ b/internal/events/loop.go @@ -17,20 +17,24 @@ import ( // EventLoop is the main event loop of the Gateway. type EventLoop struct { - processor state.ChangeProcessor - serviceStore state.ServiceStore - generator config.Generator - eventCh <-chan interface{} - logger logr.Logger - nginxFileMgr file.Manager - nginxRuntimeMgr runtime.Manager - statusUpdater status.Updater + processor state.ChangeProcessor + serviceStore state.ServiceStore + secretStore state.SecretStore + secretMemoryManager state.SecretMemoryManager + generator config.Generator + eventCh <-chan interface{} + logger logr.Logger + nginxFileMgr file.Manager + nginxRuntimeMgr runtime.Manager + statusUpdater status.Updater } // NewEventLoop creates a new EventLoop. func NewEventLoop( processor state.ChangeProcessor, serviceStore state.ServiceStore, + secretStore state.SecretStore, + secretMemoryManager state.SecretMemoryManager, generator config.Generator, eventCh <-chan interface{}, logger logr.Logger, @@ -39,14 +43,16 @@ func NewEventLoop( statusUpdater status.Updater, ) *EventLoop { return &EventLoop{ - processor: processor, - serviceStore: serviceStore, - generator: generator, - eventCh: eventCh, - logger: logger.WithName("eventLoop"), - nginxFileMgr: nginxFileMgr, - nginxRuntimeMgr: nginxRuntimeMgr, - statusUpdater: statusUpdater, + processor: processor, + serviceStore: serviceStore, + secretStore: secretStore, + secretMemoryManager: secretMemoryManager, + generator: generator, + eventCh: eventCh, + logger: logger.WithName("eventLoop"), + nginxFileMgr: nginxFileMgr, + nginxRuntimeMgr: nginxRuntimeMgr, + statusUpdater: statusUpdater, } } @@ -92,12 +98,20 @@ func (el *EventLoop) handleEvent(ctx context.Context, event interface{}) { } func (el *EventLoop) updateNginx(ctx context.Context, conf state.Configuration) error { + // Write all secrets (nuke and pave). + // This will remove all secrets in the secrets directory before writing the stored secrets. + // FIXME(kate-osborn): We may want to rethink this approach in the future and write and remove secrets individually. + err := el.secretMemoryManager.WriteAllStoredSecrets() + if err != nil { + return err + } + cfg, warnings := el.generator.Generate(conf) // For now, we keep all http servers in one config // We might rethink that. For example, we can write each server to its file // or group servers in some way. - err := el.nginxFileMgr.WriteHTTPServersConfig("http-servers", cfg) + err = el.nginxFileMgr.WriteHTTPServersConfig("http-servers", cfg) if err != nil { return err } @@ -127,6 +141,9 @@ func (el *EventLoop) propagateUpsert(e *UpsertEvent) { case *apiv1.Service: // FIXME(pleshakov): make sure the affected hosts are updated el.serviceStore.Upsert(r) + case *apiv1.Secret: + // FIXME(kate-osborn): need to handle certificate rotation + el.secretStore.Upsert(r) default: panic(fmt.Errorf("unknown resource type %T", e.Resource)) } @@ -143,6 +160,9 @@ func (el *EventLoop) propagateDelete(e *DeleteEvent) { case *apiv1.Service: // FIXME(pleshakov): make sure the affected hosts are updated el.serviceStore.Delete(e.NamespacedName) + case *apiv1.Secret: + // FIXME(kate-osborn): make sure that affected servers are updated + el.secretStore.Delete(e.NamespacedName) default: panic(fmt.Errorf("unknown resource type %T", e.Type)) } diff --git a/internal/events/loop_test.go b/internal/events/loop_test.go index f364d0ce9f..602faea4ac 100644 --- a/internal/events/loop_test.go +++ b/internal/events/loop_test.go @@ -37,27 +37,43 @@ func (r *unsupportedResource) DeepCopyObject() runtime.Object { var _ = Describe("EventLoop", func() { var ( - fakeProcessor *statefakes.FakeChangeProcessor - fakeServiceStore *statefakes.FakeServiceStore - fakeGenerator *configfakes.FakeGenerator - fakeNginxFimeMgr *filefakes.FakeManager - fakeNginxRuntimeMgr *runtimefakes.FakeManager - fakeStatusUpdater *statusfakes.FakeUpdater - cancel context.CancelFunc - eventCh chan interface{} - errorCh chan error - start func() + fakeProcessor *statefakes.FakeChangeProcessor + fakeServiceStore *statefakes.FakeServiceStore + fakeSecretStore *statefakes.FakeSecretStore + fakeSecretMemoryManager *statefakes.FakeSecretMemoryManager + fakeGenerator *configfakes.FakeGenerator + fakeNginxFimeMgr *filefakes.FakeManager + fakeNginxRuntimeMgr *runtimefakes.FakeManager + fakeStatusUpdater *statusfakes.FakeUpdater + cancel context.CancelFunc + eventCh chan interface{} + errorCh chan error + start func() ) BeforeEach(func() { fakeProcessor = &statefakes.FakeChangeProcessor{} eventCh = make(chan interface{}) fakeServiceStore = &statefakes.FakeServiceStore{} + fakeSecretMemoryManager = &statefakes.FakeSecretMemoryManager{} + fakeSecretStore = &statefakes.FakeSecretStore{} fakeGenerator = &configfakes.FakeGenerator{} fakeNginxFimeMgr = &filefakes.FakeManager{} fakeNginxRuntimeMgr = &runtimefakes.FakeManager{} fakeStatusUpdater = &statusfakes.FakeUpdater{} - ctrl := events.NewEventLoop(fakeProcessor, fakeServiceStore, fakeGenerator, eventCh, zap.New(), fakeNginxFimeMgr, fakeNginxRuntimeMgr, fakeStatusUpdater) + + ctrl := events.NewEventLoop( + fakeProcessor, + fakeServiceStore, + fakeSecretStore, + fakeSecretMemoryManager, + fakeGenerator, + eventCh, + zap.New(), + fakeNginxFimeMgr, + fakeNginxRuntimeMgr, + fakeStatusUpdater, + ) var ctx context.Context ctx, cancel = context.WithCancel(context.Background()) @@ -187,6 +203,47 @@ var _ = Describe("EventLoop", func() { }) }) + Describe("Process Secret events", func() { + BeforeEach(func() { + go start() + }) + + AfterEach(func() { + cancel() + + var err error + Eventually(errorCh).Should(Receive(&err)) + Expect(err).To(BeNil()) + }) + + It("should process upsert event", func() { + secret := &apiv1.Secret{} + + eventCh <- &events.UpsertEvent{ + Resource: secret, + } + + Eventually(fakeSecretStore.UpsertCallCount).Should(Equal(1)) + Expect(fakeSecretStore.UpsertArgsForCall(0)).Should(Equal(secret)) + + Eventually(fakeProcessor.ProcessCallCount).Should(Equal(1)) + }) + + It("should process delete event", func() { + nsname := types.NamespacedName{Namespace: "test", Name: "secret"} + + eventCh <- &events.DeleteEvent{ + NamespacedName: nsname, + Type: &apiv1.Secret{}, + } + + Eventually(fakeSecretStore.DeleteCallCount).Should(Equal(1)) + Expect(fakeSecretStore.DeleteArgsForCall(0)).Should(Equal(nsname)) + + Eventually(fakeProcessor.ProcessCallCount).Should(Equal(1)) + }) + }) + Describe("Edge cases", func() { AfterEach(func() { cancel() diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go index 429cf2cdc3..eafcdf68b1 100644 --- a/internal/helpers/helpers.go +++ b/internal/helpers/helpers.go @@ -41,3 +41,8 @@ func GetHeaderMatchTypePointer(t v1alpha2.HeaderMatchType) *v1alpha2.HeaderMatch func GetQueryParamMatchTypePointer(t v1alpha2.QueryParamMatchType) *v1alpha2.QueryParamMatchType { return &t } + +// GetTLSModePointer takes a TLSModeType and returns a pointer to it. Useful in unit tests when initializing structs. +func GetTLSModePointer(t v1alpha2.TLSModeType) *v1alpha2.TLSModeType { + return &t +} diff --git a/internal/implementations/secret/secret.go b/internal/implementations/secret/secret.go new file mode 100644 index 0000000000..7126899d13 --- /dev/null +++ b/internal/implementations/secret/secret.go @@ -0,0 +1,53 @@ +package secret + +import ( + "github.com/go-logr/logr" + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/nginxinc/nginx-kubernetes-gateway/internal/config" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/events" + "github.com/nginxinc/nginx-kubernetes-gateway/pkg/sdk" +) + +type secretImplementation struct { + conf config.Config + eventCh chan<- interface{} +} + +// NewSecretImplementation creates a new SecretImplementation. +func NewSecretImplementation(cfg config.Config, eventCh chan<- interface{}) sdk.SecretImpl { + return &secretImplementation{ + conf: cfg, + eventCh: eventCh, + } +} + +func (impl *secretImplementation) Logger() logr.Logger { + return impl.conf.Logger +} + +func (impl secretImplementation) Upsert(secret *apiv1.Secret) { + impl.Logger().Info( + "Secret was upserted", + "namespace", secret.Namespace, + "name", secret.Name, + ) + + impl.eventCh <- &events.UpsertEvent{ + Resource: secret, + } +} + +func (impl secretImplementation) Remove(nsname types.NamespacedName) { + impl.Logger().Info( + "Secret was removed", + "namespace", nsname.Namespace, + "name", nsname.Name, + ) + + impl.eventCh <- &events.DeleteEvent{ + NamespacedName: nsname, + Type: &apiv1.Secret{}, + } +} diff --git a/internal/implementations/secret/secret_test.go b/internal/implementations/secret/secret_test.go new file mode 100644 index 0000000000..93860646df --- /dev/null +++ b/internal/implementations/secret/secret_test.go @@ -0,0 +1,64 @@ +package secret_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "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/controller-runtime/pkg/log/zap" + + "github.com/nginxinc/nginx-kubernetes-gateway/internal/config" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/events" + implementation "github.com/nginxinc/nginx-kubernetes-gateway/internal/implementations/secret" + "github.com/nginxinc/nginx-kubernetes-gateway/pkg/sdk" +) + +var _ = Describe("SecretImplementation", func() { + var ( + eventCh chan interface{} + impl sdk.SecretImpl + ) + + BeforeEach(func() { + eventCh = make(chan interface{}) + + impl = implementation.NewSecretImplementation(config.Config{ + Logger: zap.New(), + }, eventCh) + }) + + const secretName = "my-secret" + const secretNamespace = "test" + + Describe("Implementation processes Secret", func() { + It("should process upsert", func() { + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: secretNamespace, + }, + } + + go func() { + impl.Upsert(secret) + }() + + Eventually(eventCh).Should(Receive(Equal(&events.UpsertEvent{Resource: secret}))) + }) + + It("should process remove", func() { + nsname := types.NamespacedName{Name: secretName, Namespace: secretNamespace} + + go func() { + impl.Remove(nsname) + }() + + Eventually(eventCh).Should(Receive(Equal( + &events.DeleteEvent{ + NamespacedName: nsname, + Type: &v1.Secret{}, + }))) + }) + }) +}) diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 89061faa3f..bae5db50dc 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -15,6 +15,7 @@ import ( gw "github.com/nginxinc/nginx-kubernetes-gateway/internal/implementations/gateway" gc "github.com/nginxinc/nginx-kubernetes-gateway/internal/implementations/gatewayclass" hr "github.com/nginxinc/nginx-kubernetes-gateway/internal/implementations/httproute" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/implementations/secret" svc "github.com/nginxinc/nginx-kubernetes-gateway/internal/implementations/service" ngxcfg "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config" "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/file" @@ -27,6 +28,9 @@ import ( // clusterTimeout is a timeout for connections to the Kubernetes API const clusterTimeout = 10 * time.Second +// secretsFolder is the folder that holds all the secrets for NGINX servers. +const secretsFolder = "/etc/nginx/secrets" + var scheme = runtime.NewScheme() func init() { @@ -68,11 +72,20 @@ func Start(cfg config.Config) error { if err != nil { return fmt.Errorf("cannot register service implementation: %w", err) } + err = sdk.RegisterSecretController(mgr, secret.NewSecretImplementation(cfg, eventCh)) + if err != nil { + return fmt.Errorf("cannot register secret implementation: %w", err) + } + + secretStore := state.NewSecretStore() + secretMemoryMgr := state.NewSecretDiskMemoryManager(secretsFolder, secretStore) processor := state.NewChangeProcessorImpl(state.ChangeProcessorConfig{ - GatewayCtlrName: cfg.GatewayCtlrName, - GatewayClassName: cfg.GatewayClassName, + GatewayCtlrName: cfg.GatewayCtlrName, + GatewayClassName: cfg.GatewayClassName, + SecretMemoryManager: secretMemoryMgr, }) + serviceStore := state.NewServiceStore() configGenerator := ngxcfg.NewGeneratorImpl(serviceStore) nginxFileMgr := file.NewManagerImpl() @@ -87,7 +100,19 @@ func Start(cfg config.Config) error { Logger: cfg.Logger.WithName("statusUpdater"), Clock: status.NewRealClock(), }) - eventLoop := events.NewEventLoop(processor, serviceStore, configGenerator, eventCh, cfg.Logger, nginxFileMgr, nginxRuntimeMgr, statusUpdater) + + eventLoop := events.NewEventLoop( + processor, + serviceStore, + secretStore, + secretMemoryMgr, + configGenerator, + eventCh, + cfg.Logger, + nginxFileMgr, + nginxRuntimeMgr, + statusUpdater, + ) err = mgr.Add(eventLoop) if err != nil { diff --git a/internal/nginx/config/generator.go b/internal/nginx/config/generator.go index 77d5236eef..c01d1a638a 100644 --- a/internal/nginx/config/generator.go +++ b/internal/nginx/config/generator.go @@ -40,11 +40,20 @@ func NewGeneratorImpl(serviceStore state.ServiceStore) *GeneratorImpl { func (g *GeneratorImpl) Generate(conf state.Configuration) ([]byte, Warnings) { warnings := newWarnings() + confServers := append(conf.HTTPServers, conf.HTTPSServers...) + servers := httpServers{ - Servers: make([]server, 0, len(conf.HTTPServers)), + // capacity is all the conf servers + default tls termination server + Servers: make([]server, 0, len(confServers)+1), } - for _, s := range conf.HTTPServers { + if len(conf.HTTPSServers) > 0 { + defaultServer := generateDefaultTLSTerminationServer() + + servers.Servers = append(servers.Servers, defaultServer) + } + + for _, s := range confServers { cfg, warns := generate(s, g.serviceStore) servers.Servers = append(servers.Servers, cfg) @@ -54,6 +63,10 @@ func (g *GeneratorImpl) Generate(conf state.Configuration) ([]byte, Warnings) { return g.executor.ExecuteForHTTPServers(servers), warnings } +func generateDefaultTLSTerminationServer() server { + return server{IsDefault: true} +} + func generate(httpServer state.HTTPServer, serviceStore state.ServiceStore) (server, Warnings) { warnings := newWarnings() @@ -102,11 +115,17 @@ func generate(httpServer state.HTTPServer, serviceStore state.ServiceStore) (ser locs = append(locs, pathLoc) } } - - return server{ + s := server{ ServerName: httpServer.Hostname, Locations: locs, - }, warnings + } + if httpServer.SSL != nil { + s.SSL = &ssl{ + Certificate: httpServer.SSL.CertificatePath, + CertificateKey: httpServer.SSL.CertificatePath, + } + } + return s, warnings } func generateProxyPass(address string) string { diff --git a/internal/nginx/config/generator_test.go b/internal/nginx/config/generator_test.go index a27630195b..757fb19f29 100644 --- a/internal/nginx/config/generator_test.go +++ b/internal/nginx/config/generator_test.go @@ -3,6 +3,7 @@ package config import ( "encoding/json" "errors" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -18,21 +19,70 @@ import ( func TestGenerateForHost(t *testing.T) { generator := NewGeneratorImpl(&statefakes.FakeServiceStore{}) - conf := state.Configuration{ - HTTPServers: []state.HTTPServer{ - { - Hostname: "example.com", + testcases := []struct { + conf state.Configuration + expectDefault bool + msg string + }{ + { + conf: state.Configuration{ + HTTPServers: []state.HTTPServer{ + { + Hostname: "example.com", + }, + }, + }, + expectDefault: false, + msg: "only HTTP servers", + }, + { + conf: state.Configuration{ + HTTPSServers: []state.HTTPServer{ + { + Hostname: "example.com", + }, + }, }, + expectDefault: true, + msg: "only HTTPS servers", + }, + { + conf: state.Configuration{ + HTTPServers: []state.HTTPServer{ + { + Hostname: "example.com", + }, + }, + HTTPSServers: []state.HTTPServer{ + { + Hostname: "example.com", + }, + }, + }, + expectDefault: true, + msg: "both HTTP and HTTPS servers", }, } - cfg, warnings := generator.Generate(conf) + for _, tc := range testcases { + cfg, warnings := generator.Generate(tc.conf) - if len(cfg) == 0 { - t.Errorf("Generate() generated empty config") - } - if len(warnings) > 0 { - t.Errorf("Generate() returned unexpected warnings: %v", warnings) + defaultExists := strings.Contains(string(cfg), "default") + + if tc.expectDefault && !defaultExists { + t.Errorf("Generate() did not generate a config with a default TLS termination server") + } + + if !tc.expectDefault && defaultExists { + t.Errorf("Generate() generated a config with a default TLS termination server") + } + + if len(cfg) == 0 { + t.Errorf("Generate() generated empty config") + } + if len(warnings) > 0 { + t.Errorf("Generate() returned unexpected warnings: %v", warnings) + } } } @@ -143,7 +193,9 @@ func TestGenerate(t *testing.T) { }, } - host := state.HTTPServer{ + certPath := "/etc/nginx/secrets/cert" + + httpHost := state.HTTPServer{ Hostname: "example.com", PathRules: []state.PathRule{ { @@ -189,6 +241,9 @@ func TestGenerate(t *testing.T) { }, } + httpsHost := httpHost + httpsHost.SSL = &state.SSL{CertificatePath: certPath} + fakeServiceStore := &statefakes.FakeServiceStore{} fakeServiceStore.ResolveReturns("10.0.0.1", nil) @@ -216,7 +271,7 @@ func TestGenerate(t *testing.T) { const backendAddr = "http://10.0.0.1:80" - expected := server{ + expectedHTTPServer := server{ ServerName: "example.com", Locations: []location{ { @@ -253,17 +308,43 @@ func TestGenerate(t *testing.T) { }, }, } + + expectedHTTPSServer := expectedHTTPServer + expectedHTTPSServer.SSL = &ssl{Certificate: certPath, CertificateKey: certPath} + expectedWarnings := Warnings{ hr: []string{"empty backend refs"}, } - result, warnings := generate(host, fakeServiceStore) - - if diff := cmp.Diff(expected, result); diff != "" { - t.Errorf("generate() mismatch (-want +got):\n%s", diff) + testcases := []struct { + host state.HTTPServer + expWarnings Warnings + expResult server + msg string + }{ + { + host: httpHost, + expWarnings: expectedWarnings, + expResult: expectedHTTPServer, + msg: "http server", + }, + { + host: httpsHost, + expWarnings: expectedWarnings, + expResult: expectedHTTPSServer, + msg: "https server", + }, } - if diff := cmp.Diff(expectedWarnings, warnings); diff != "" { - t.Errorf("generate() mismatch on warnings (-want +got):\n%s", diff) + + for _, tc := range testcases { + result, warnings := generate(tc.host, fakeServiceStore) + + if diff := cmp.Diff(tc.expResult, result); diff != "" { + t.Errorf("generate() mismatch (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tc.expWarnings, warnings); diff != "" { + t.Errorf("generate() mismatch on warnings (-want +got):\n%s", diff) + } } } diff --git a/internal/nginx/config/http.go b/internal/nginx/config/http.go index 0326e680d9..072d9891da 100644 --- a/internal/nginx/config/http.go +++ b/internal/nginx/config/http.go @@ -5,7 +5,9 @@ type httpServers struct { } type server struct { + IsDefault bool ServerName string + SSL *ssl Locations []location } @@ -15,3 +17,8 @@ type location struct { HTTPMatchVar string Internal bool } + +type ssl struct { + Certificate string + CertificateKey string +} diff --git a/internal/nginx/config/template.go b/internal/nginx/config/template.go index 7e17e81833..52b5fb769a 100644 --- a/internal/nginx/config/template.go +++ b/internal/nginx/config/template.go @@ -7,7 +7,20 @@ import ( ) var httpServersTemplate = `{{ range $s := .Servers }} +{{ if $s.IsDefault }} server { + listen 443 ssl default_server; + + ssl_reject_handshake on; +} +{{ else }} +server { + {{ if $s.SSL }} + listen 443 ssl; + ssl_certificate {{ $s.SSL.Certificate }}; + ssl_certificate_key {{ $s.SSL.CertificateKey }}; + {{ end }} + server_name {{ $s.ServerName }}; {{ range $l := $s.Locations }} @@ -30,6 +43,7 @@ server { {{ end }} } {{ end }} +{{ end }} ` // templateExecutor generates NGINX configuration using a template. diff --git a/internal/state/change_processor.go b/internal/state/change_processor.go index 71625e29b7..29c3b4a8e9 100644 --- a/internal/state/change_processor.go +++ b/internal/state/change_processor.go @@ -37,6 +37,8 @@ type ChangeProcessorConfig struct { GatewayCtlrName string // GatewayClassName is the name of the GatewayClass resource. GatewayClassName string + // SecretMemoryManager is the secret memory manager. + SecretMemoryManager SecretMemoryManager } type ChangeProcessorImpl struct { @@ -127,7 +129,12 @@ func (c *ChangeProcessorImpl) Process() (changed bool, conf Configuration, statu c.changed = false - graph := buildGraph(c.store, c.cfg.GatewayCtlrName, c.cfg.GatewayClassName) + graph := buildGraph( + c.store, + c.cfg.GatewayCtlrName, + c.cfg.GatewayClassName, + c.cfg.SecretMemoryManager, + ) conf = buildConfiguration(graph) statuses = buildStatuses(graph) diff --git a/internal/state/change_processor_test.go b/internal/state/change_processor_test.go index 497a119972..cb091463d1 100644 --- a/internal/state/change_processor_test.go +++ b/internal/state/change_processor_test.go @@ -10,13 +10,15 @@ import ( "github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers" "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/statefakes" ) var _ = Describe("ChangeProcessor", func() { Describe("Normal cases of processing changes", func() { const ( - controllerName = "my.controller" - gcName = "test-class" + controllerName = "my.controller" + gcName = "test-class" + certificatePath = "path/to/cert" ) var ( @@ -24,6 +26,7 @@ var _ = Describe("ChangeProcessor", func() { hr1, hr1Updated, hr2 *v1alpha2.HTTPRoute gw1, gw1Updated, gw2 *v1alpha2.Gateway processor state.ChangeProcessor + fakeSecretMemoryMgr *statefakes.FakeSecretMemoryManager ) BeforeEach(OncePerOrdered, func() { @@ -54,6 +57,11 @@ var _ = Describe("ChangeProcessor", func() { Name: v1alpha2.ObjectName(gateway), SectionName: (*v1alpha2.SectionName)(helpers.GetStringPointer("listener-80-1")), }, + { + Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer("test")), + Name: v1alpha2.ObjectName(gateway), + SectionName: (*v1alpha2.SectionName)(helpers.GetStringPointer("listener-443-1")), + }, }, }, Hostnames: []v1alpha2.Hostname{ @@ -97,6 +105,22 @@ var _ = Describe("ChangeProcessor", func() { Port: 80, Protocol: v1alpha2.HTTPProtocolType, }, + { + Name: "listener-443-1", + Hostname: nil, + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{ + { + Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer("test")), + }, + }, + }, + }, }, }, } @@ -109,10 +133,15 @@ var _ = Describe("ChangeProcessor", func() { gw2 = createGateway("gateway-2") + fakeSecretMemoryMgr = &statefakes.FakeSecretMemoryManager{} + processor = state.NewChangeProcessorImpl(state.ChangeProcessorConfig{ - GatewayCtlrName: controllerName, - GatewayClassName: gcName, + GatewayCtlrName: controllerName, + GatewayClassName: gcName, + SecretMemoryManager: fakeSecretMemoryMgr, }) + + fakeSecretMemoryMgr.StoreReturns(certificatePath, nil) }) Describe("Process resources", Ordered, func() { @@ -124,7 +153,6 @@ var _ = Describe("ChangeProcessor", func() { Expect(statuses).To(BeZero()) }) }) - When("GatewayClass doesn't exist", func() { When("Gateways don't exist", func() { It("should return empty configuration and updated statuses after upserting the first HTTPRoute", func() { @@ -155,13 +183,18 @@ var _ = Describe("ChangeProcessor", func() { Valid: false, AttachedRoutes: 1, }, + "listener-443-1": { + Valid: false, + AttachedRoutes: 1, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{}, HTTPRouteStatuses: map[types.NamespacedName]state.HTTPRouteStatus{ {Namespace: "test", Name: "hr-1"}: { ParentStatuses: map[string]state.ParentStatus{ - "listener-80-1": {Attached: false}, + "listener-80-1": {Attached: false}, + "listener-443-1": {Attached: false}, }, }, }, @@ -195,7 +228,26 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + HTTPSServers: []state.HTTPServer{ + { + Hostname: "foo.example.com", + SSL: &state.SSL{CertificatePath: certificatePath}, + PathRules: []state.PathRule{ + { + Path: "/", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: hr1, + }, + }, + }, + }, + }, + }, } + expectedStatuses := state.Statuses{ GatewayClassStatus: &state.GatewayClassStatus{ Valid: true, @@ -208,13 +260,18 @@ var _ = Describe("ChangeProcessor", func() { Valid: true, AttachedRoutes: 1, }, + "listener-443-1": { + Valid: true, + AttachedRoutes: 1, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{}, HTTPRouteStatuses: map[types.NamespacedName]state.HTTPRouteStatus{ {Namespace: "test", Name: "hr-1"}: { ParentStatuses: map[string]state.ParentStatus{ - "listener-80-1": {Attached: true}, + "listener-80-1": {Attached: true}, + "listener-443-1": {Attached: true}, }, }, }, @@ -258,6 +315,24 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + HTTPSServers: []state.HTTPServer{ + { + Hostname: "foo.example.com", + SSL: &state.SSL{CertificatePath: certificatePath}, + PathRules: []state.PathRule{ + { + Path: "/", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: hr1Updated, + }, + }, + }, + }, + }, + }, } expectedStatuses := state.Statuses{ GatewayClassStatus: &state.GatewayClassStatus{ @@ -271,13 +346,18 @@ var _ = Describe("ChangeProcessor", func() { Valid: true, AttachedRoutes: 1, }, + "listener-443-1": { + Valid: true, + AttachedRoutes: 1, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{}, HTTPRouteStatuses: map[types.NamespacedName]state.HTTPRouteStatus{ {Namespace: "test", Name: "hr-1"}: { ParentStatuses: map[string]state.ParentStatus{ - "listener-80-1": {Attached: true}, + "listener-80-1": {Attached: true}, + "listener-443-1": {Attached: true}, }, }, }, @@ -321,6 +401,24 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + HTTPSServers: []state.HTTPServer{ + { + Hostname: "foo.example.com", + SSL: &state.SSL{CertificatePath: certificatePath}, + PathRules: []state.PathRule{ + { + Path: "/", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: hr1Updated, + }, + }, + }, + }, + }, + }, } expectedStatuses := state.Statuses{ GatewayClassStatus: &state.GatewayClassStatus{ @@ -334,13 +432,18 @@ var _ = Describe("ChangeProcessor", func() { Valid: true, AttachedRoutes: 1, }, + "listener-443-1": { + Valid: true, + AttachedRoutes: 1, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{}, HTTPRouteStatuses: map[types.NamespacedName]state.HTTPRouteStatus{ {Namespace: "test", Name: "hr-1"}: { ParentStatuses: map[string]state.ParentStatus{ - "listener-80-1": {Attached: true}, + "listener-80-1": {Attached: true}, + "listener-443-1": {Attached: true}, }, }, }, @@ -384,6 +487,24 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + HTTPSServers: []state.HTTPServer{ + { + Hostname: "foo.example.com", + SSL: &state.SSL{CertificatePath: certificatePath}, + PathRules: []state.PathRule{ + { + Path: "/", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: hr1Updated, + }, + }, + }, + }, + }, + }, } expectedStatuses := state.Statuses{ GatewayClassStatus: &state.GatewayClassStatus{ @@ -397,13 +518,18 @@ var _ = Describe("ChangeProcessor", func() { Valid: true, AttachedRoutes: 1, }, + "listener-443-1": { + Valid: true, + AttachedRoutes: 1, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{}, HTTPRouteStatuses: map[types.NamespacedName]state.HTTPRouteStatus{ {Namespace: "test", Name: "hr-1"}: { ParentStatuses: map[string]state.ParentStatus{ - "listener-80-1": {Attached: true}, + "listener-80-1": {Attached: true}, + "listener-443-1": {Attached: true}, }, }, }, @@ -444,6 +570,26 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + HTTPSServers: []state.HTTPServer{ + { + Hostname: "foo.example.com", + PathRules: []state.PathRule{ + { + Path: "/", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: hr1Updated, + }, + }, + }, + }, + SSL: &state.SSL{ + CertificatePath: certificatePath, + }, + }, + }, } expectedStatuses := state.Statuses{ GatewayClassStatus: &state.GatewayClassStatus{ @@ -457,6 +603,10 @@ var _ = Describe("ChangeProcessor", func() { Valid: true, AttachedRoutes: 1, }, + "listener-443-1": { + Valid: true, + AttachedRoutes: 1, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{ @@ -467,7 +617,8 @@ var _ = Describe("ChangeProcessor", func() { HTTPRouteStatuses: map[types.NamespacedName]state.HTTPRouteStatus{ {Namespace: "test", Name: "hr-1"}: { ParentStatuses: map[string]state.ParentStatus{ - "listener-80-1": {Attached: true}, + "listener-80-1": {Attached: true}, + "listener-443-1": {Attached: true}, }, }, }, @@ -500,6 +651,24 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + HTTPSServers: []state.HTTPServer{ + { + Hostname: "foo.example.com", + SSL: &state.SSL{CertificatePath: certificatePath}, + PathRules: []state.PathRule{ + { + Path: "/", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: hr1Updated, + }, + }, + }, + }, + }, + }, } expectedStatuses := state.Statuses{ GatewayClassStatus: &state.GatewayClassStatus{ @@ -513,6 +682,10 @@ var _ = Describe("ChangeProcessor", func() { Valid: true, AttachedRoutes: 1, }, + "listener-443-1": { + Valid: true, + AttachedRoutes: 1, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{ @@ -523,12 +696,14 @@ var _ = Describe("ChangeProcessor", func() { HTTPRouteStatuses: map[types.NamespacedName]state.HTTPRouteStatus{ {Namespace: "test", Name: "hr-1"}: { ParentStatuses: map[string]state.ParentStatus{ - "listener-80-1": {Attached: true}, + "listener-80-1": {Attached: true}, + "listener-443-1": {Attached: true}, }, }, {Namespace: "test", Name: "hr-2"}: { ParentStatuses: map[string]state.ParentStatus{ - "listener-80-1": {Attached: false}, + "listener-80-1": {Attached: false}, + "listener-443-1": {Attached: false}, }, }, }, @@ -561,6 +736,24 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + HTTPSServers: []state.HTTPServer{ + { + Hostname: "bar.example.com", + SSL: &state.SSL{CertificatePath: certificatePath}, + PathRules: []state.PathRule{ + { + Path: "/", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: hr2, + }, + }, + }, + }, + }, + }, } expectedStatuses := state.Statuses{ GatewayClassStatus: &state.GatewayClassStatus{ @@ -574,13 +767,18 @@ var _ = Describe("ChangeProcessor", func() { Valid: true, AttachedRoutes: 1, }, + "listener-443-1": { + Valid: true, + AttachedRoutes: 1, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{}, HTTPRouteStatuses: map[types.NamespacedName]state.HTTPRouteStatus{ {Namespace: "test", Name: "hr-2"}: { ParentStatuses: map[string]state.ParentStatus{ - "listener-80-1": {Attached: true}, + "listener-80-1": {Attached: true}, + "listener-443-1": {Attached: true}, }, }, }, @@ -596,7 +794,8 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureDeleteChange(&v1alpha2.HTTPRoute{}, types.NamespacedName{Namespace: "test", Name: "hr-2"}) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{}, + HTTPServers: []state.HTTPServer{}, + HTTPSServers: []state.HTTPServer{}, } expectedStatuses := state.Statuses{ GatewayClassStatus: &state.GatewayClassStatus{ @@ -610,6 +809,10 @@ var _ = Describe("ChangeProcessor", func() { Valid: true, AttachedRoutes: 0, }, + "listener-443-1": { + Valid: true, + AttachedRoutes: 0, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{}, @@ -634,6 +837,10 @@ var _ = Describe("ChangeProcessor", func() { Valid: false, AttachedRoutes: 0, }, + "listener-443-1": { + Valid: false, + AttachedRoutes: 0, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{}, @@ -680,13 +887,16 @@ var _ = Describe("ChangeProcessor", func() { Describe("Edge cases with panic", func() { var processor state.ChangeProcessor + var fakeSecretMemoryMgr *statefakes.FakeSecretMemoryManager BeforeEach(func() { - cfg := state.ChangeProcessorConfig{ - GatewayCtlrName: "test.controller", - GatewayClassName: "my-class", - } - processor = state.NewChangeProcessorImpl(cfg) + fakeSecretMemoryMgr = &statefakes.FakeSecretMemoryManager{} + + processor = state.NewChangeProcessorImpl(state.ChangeProcessorConfig{ + GatewayCtlrName: "test.controller", + GatewayClassName: "my-class", + SecretMemoryManager: fakeSecretMemoryMgr, + }) }) DescribeTable("CaptureUpsertChange must panic", diff --git a/internal/state/configuration.go b/internal/state/configuration.go index 11e76b018b..866badfd2c 100644 --- a/internal/state/configuration.go +++ b/internal/state/configuration.go @@ -1,6 +1,7 @@ package state import ( + "fmt" "sort" "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -13,6 +14,9 @@ type Configuration struct { // HTTPServers holds all HTTPServers. // FIXME(pleshakov) We assume that all servers are HTTP and listen on port 80. HTTPServers []HTTPServer + // HTTPSServers holds all HTTPSServers. + // FIXME(kate-osborn) We assume that all HTTPS servers listen on port 443. + HTTPSServers []HTTPServer } // HTTPServer is a virtual server. @@ -21,6 +25,13 @@ type HTTPServer struct { Hostname string // PathRules is a collection of routing rules. PathRules []PathRule + // SSL holds the SSL configuration options fo the server. + SSL *SSL +} + +type SSL struct { + // CertificatePath is the path to the certificate file. + CertificatePath string } // PathRule represents routing rules that share a common path. @@ -49,6 +60,7 @@ func (r *MatchRule) GetMatch() v1alpha2.HTTPRouteMatch { } // 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(graph *graph) Configuration { if graph.GatewayClass == nil || !graph.GatewayClass.Valid { return Configuration{} @@ -58,56 +70,116 @@ func buildConfiguration(graph *graph) Configuration { return Configuration{} } - // FIXME(pleshakov) For now we only handle paths with prefix matches. Handle exact and regex matches - pathRulesForHosts := make(map[string]map[string]PathRule) - + configBuilder := newConfigBuilder() for _, l := range graph.Gateway.Listeners { - for _, r := range l.Routes { - var hostnames []string + configBuilder.upsertListener(l) + } - for _, h := range r.Source.Spec.Hostnames { - if _, exist := l.AcceptedHostnames[string(h)]; exist { - hostnames = append(hostnames, string(h)) - } + return configBuilder.build() +} + +type configBuilder struct { + http *httpServerBuilder + https *httpServerBuilder +} + +func newConfigBuilder() *configBuilder { + return &configBuilder{ + http: newHTTPServerBuilder(), + https: newHTTPServerBuilder(), + } +} + +func (sb *configBuilder) upsertListener(l *listener) { + switch l.Source.Protocol { + case v1alpha2.HTTPProtocolType: + sb.http.upsertListener(l) + case v1alpha2.HTTPSProtocolType: + sb.https.upsertListener(l) + default: + panic(fmt.Sprintf("listener protocol %s not supported", l.Source.Protocol)) + } +} + +func (sb *configBuilder) build() Configuration { + return Configuration{ + HTTPServers: sb.http.build(), + HTTPSServers: sb.https.build(), + } +} + +type httpServerBuilder struct { + rules map[string]map[string]PathRule + listenersForHost map[string]*listener +} + +func newHTTPServerBuilder() *httpServerBuilder { + return &httpServerBuilder{ + rules: make(map[string]map[string]PathRule), + listenersForHost: make(map[string]*listener), + } +} + +func (p *httpServerBuilder) upsertListener(l *listener) { + + for _, r := range l.Routes { + var hostnames []string + + for _, h := range r.Source.Spec.Hostnames { + if _, exist := l.AcceptedHostnames[string(h)]; exist { + hostnames = append(hostnames, string(h)) } + } - for _, h := range hostnames { - if _, exist := pathRulesForHosts[h]; !exist { - pathRulesForHosts[h] = make(map[string]PathRule) - } + for _, h := range hostnames { + p.listenersForHost[h] = l + if _, exist := p.rules[h]; !exist { + p.rules[h] = make(map[string]PathRule) } + } - for i, rule := range r.Source.Spec.Rules { - for _, h := range hostnames { - for j, m := range rule.Matches { - path := getPath(m.Path) + for i, rule := range r.Source.Spec.Rules { + for _, h := range hostnames { + for j, m := range rule.Matches { + path := getPath(m.Path) - rule, exist := pathRulesForHosts[h][path] - if !exist { - rule.Path = path - } + rule, exist := p.rules[h][path] + if !exist { + rule.Path = path + } - rule.MatchRules = append(rule.MatchRules, MatchRule{ - MatchIdx: j, - RuleIdx: i, - Source: r.Source, - }) + rule.MatchRules = append(rule.MatchRules, MatchRule{ + MatchIdx: j, + RuleIdx: i, + Source: r.Source, + }) - pathRulesForHosts[h][path] = rule - } + p.rules[h][path] = rule } } } } +} - httpServers := make([]HTTPServer, 0, len(pathRulesForHosts)) +func (p *httpServerBuilder) build() []HTTPServer { - for h, rules := range pathRulesForHosts { + servers := make([]HTTPServer, 0, len(p.rules)) + + for h, rules := range p.rules { s := HTTPServer{ Hostname: h, PathRules: make([]PathRule, 0, len(rules)), } + l, ok := p.listenersForHost[h] + if !ok { + panic(fmt.Sprintf("no listener found for hostname: %s", h)) + } + + if l.SecretPath != "" { + s.SSL = &SSL{CertificatePath: l.SecretPath} + } + for _, r := range rules { sortMatchRules(r.MatchRules) @@ -119,17 +191,15 @@ func buildConfiguration(graph *graph) Configuration { return s.PathRules[i].Path < s.PathRules[j].Path }) - httpServers = append(httpServers, s) + servers = append(servers, s) } // sort servers for predictable order - sort.Slice(httpServers, func(i, j int) bool { - return httpServers[i].Hostname < httpServers[j].Hostname + sort.Slice(servers, func(i, j int) bool { + return servers[i].Hostname < servers[j].Hostname }) - return Configuration{ - HTTPServers: httpServers, - } + return servers } func getPath(path *v1alpha2.HTTPPathMatch) string { diff --git a/internal/state/configuration_test.go b/internal/state/configuration_test.go index c8f8e5155f..1f95efa992 100644 --- a/internal/state/configuration_test.go +++ b/internal/state/configuration_test.go @@ -12,7 +12,7 @@ import ( ) func TestBuildConfiguration(t *testing.T) { - createRoute := func(name string, hostname string, paths ...string) *v1alpha2.HTTPRoute { + createRoute := func(name string, hostname string, listenerName string, paths ...string) *v1alpha2.HTTPRoute { rules := make([]v1alpha2.HTTPRouteRule, 0, len(paths)) for _, p := range paths { rules = append(rules, v1alpha2.HTTPRouteRule{ @@ -36,7 +36,7 @@ func TestBuildConfiguration(t *testing.T) { { Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer("test")), Name: "gateway", - SectionName: (*v1alpha2.SectionName)(helpers.GetStringPointer("listener-80-1")), + SectionName: (*v1alpha2.SectionName)(helpers.GetStringPointer(listenerName)), }, }, }, @@ -48,7 +48,7 @@ func TestBuildConfiguration(t *testing.T) { } } - hr1 := createRoute("hr-1", "foo.example.com", "/") + hr1 := createRoute("hr-1", "foo.example.com", "listener-80-1", "/") routeHR1 := &route{ Source: hr1, @@ -58,7 +58,7 @@ func TestBuildConfiguration(t *testing.T) { InvalidSectionNameRefs: map[string]struct{}{}, } - hr2 := createRoute("hr-2", "bar.example.com", "/") + hr2 := createRoute("hr-2", "bar.example.com", "listener-80-1", "/") routeHR2 := &route{ Source: hr2, @@ -68,7 +68,27 @@ func TestBuildConfiguration(t *testing.T) { InvalidSectionNameRefs: map[string]struct{}{}, } - hr3 := createRoute("hr-3", "foo.example.com", "/", "/third") + httpsHR1 := createRoute("https-hr-1", "foo.example.com", "listener-443-1", "/") + + httpsRouteHR1 := &route{ + Source: httpsHR1, + ValidSectionNameRefs: map[string]struct{}{ + "listener-443-1": {}, + }, + InvalidSectionNameRefs: map[string]struct{}{}, + } + + httpsHR2 := createRoute("https-hr-2", "bar.example.com", "listener-443-1", "/") + + httpsRouteHR2 := &route{ + Source: httpsHR2, + ValidSectionNameRefs: map[string]struct{}{ + "listener-443-1": {}, + }, + InvalidSectionNameRefs: map[string]struct{}{}, + } + + hr3 := createRoute("hr-3", "foo.example.com", "listener-80-1", "/", "/third") routeHR3 := &route{ Source: hr3, @@ -78,7 +98,27 @@ func TestBuildConfiguration(t *testing.T) { InvalidSectionNameRefs: map[string]struct{}{}, } - hr4 := createRoute("hr-4", "foo.example.com", "/fourth", "/") + httpsHR3 := createRoute("hr-3", "foo.example.com", "listener-443-1", "/", "/third") + + httpsRouteHR3 := &route{ + Source: httpsHR3, + ValidSectionNameRefs: map[string]struct{}{ + "listener-443-1": {}, + }, + InvalidSectionNameRefs: map[string]struct{}{}, + } + + httpsHR4 := createRoute("hr-4", "foo.example.com", "listener-443-1", "/fourth", "/") + + httpsRouteHR4 := &route{ + Source: httpsHR4, + ValidSectionNameRefs: map[string]struct{}{ + "listener-80-1": {}, + }, + InvalidSectionNameRefs: map[string]struct{}{}, + } + + hr4 := createRoute("hr-4", "foo.example.com", "listener-80-1", "/fourth", "/") routeHR4 := &route{ Source: hr4, @@ -88,6 +128,32 @@ func TestBuildConfiguration(t *testing.T) { InvalidSectionNameRefs: map[string]struct{}{}, } + listener80 := v1alpha2.Listener{ + Name: "listener-80-1", + Hostname: nil, + Port: 80, + Protocol: v1alpha2.HTTPProtocolType, + } + + listener443 := v1alpha2.Listener{ + Name: "listener-443-1", + Hostname: nil, + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{ + { + Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer("test")), + }, + }, + }, + } + + secretPath := "/etc/nginx/secrets/secret" + tests := []struct { graph *graph expected Configuration @@ -106,7 +172,8 @@ func TestBuildConfiguration(t *testing.T) { Routes: map[types.NamespacedName]*route{}, }, expected: Configuration{ - HTTPServers: []HTTPServer{}, + HTTPServers: []HTTPServer{}, + HTTPSServers: []HTTPServer{}, }, msg: "no listeners and routes", }, @@ -120,18 +187,27 @@ func TestBuildConfiguration(t *testing.T) { Source: &v1alpha2.Gateway{}, Listeners: map[string]*listener{ "listener-80-1": { + Source: listener80, + Valid: true, + Routes: map[types.NamespacedName]*route{}, + AcceptedHostnames: map[string]struct{}{}, + }, + "listener-443-1": { + Source: listener443, Valid: true, Routes: map[types.NamespacedName]*route{}, AcceptedHostnames: map[string]struct{}{}, + SecretPath: secretPath, }, }, }, Routes: map[types.NamespacedName]*route{}, }, expected: Configuration{ - HTTPServers: []HTTPServer{}, + HTTPServers: []HTTPServer{}, + HTTPSServers: []HTTPServer{}, }, - msg: "listener with no routes", + msg: "http and https listeners with no routes", }, { graph: &graph{ @@ -143,7 +219,8 @@ func TestBuildConfiguration(t *testing.T) { Source: &v1alpha2.Gateway{}, Listeners: map[string]*listener{ "listener-80-1": { - Valid: true, + Source: listener80, + Valid: true, Routes: map[types.NamespacedName]*route{ {Namespace: "test", Name: "hr-1"}: routeHR1, {Namespace: "test", Name: "hr-2"}: routeHR2, @@ -153,11 +230,26 @@ func TestBuildConfiguration(t *testing.T) { "bar.example.com": {}, }, }, + "listener-443-1": { + Source: listener443, + Valid: true, + SecretPath: secretPath, + Routes: map[types.NamespacedName]*route{ + {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, + {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, + }, + AcceptedHostnames: map[string]struct{}{ + "foo.example.com": {}, + "bar.example.com": {}, + }, + }, }, }, Routes: map[types.NamespacedName]*route{ - {Namespace: "test", Name: "hr-1"}: routeHR1, - {Namespace: "test", Name: "hr-2"}: routeHR2, + {Namespace: "test", Name: "hr-1"}: routeHR1, + {Namespace: "test", Name: "hr-2"}: routeHR2, + {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, + {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, }, }, expected: Configuration{ @@ -193,8 +285,46 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, + HTTPSServers: []HTTPServer{ + { + Hostname: "bar.example.com", + PathRules: []PathRule{ + { + Path: "/", + MatchRules: []MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: httpsHR2, + }, + }, + }, + }, + SSL: &SSL{ + CertificatePath: secretPath, + }, + }, + { + Hostname: "foo.example.com", + PathRules: []PathRule{ + { + Path: "/", + MatchRules: []MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: httpsHR1, + }, + }, + }, + }, + SSL: &SSL{ + CertificatePath: secretPath, + }, + }, + }, }, - msg: "one listener with two routes for different hostnames", + msg: "one http and one https listener each with two routes for different hostnames", }, { graph: &graph{ @@ -206,7 +336,8 @@ func TestBuildConfiguration(t *testing.T) { Source: &v1alpha2.Gateway{}, Listeners: map[string]*listener{ "listener-80-1": { - Valid: true, + Source: listener80, + Valid: true, Routes: map[types.NamespacedName]*route{ {Namespace: "test", Name: "hr-3"}: routeHR3, {Namespace: "test", Name: "hr-4"}: routeHR4, @@ -215,11 +346,25 @@ func TestBuildConfiguration(t *testing.T) { "foo.example.com": {}, }, }, + "listener-443-1": { + Source: listener443, + Valid: true, + SecretPath: secretPath, + Routes: map[types.NamespacedName]*route{ + {Namespace: "test", Name: "https-hr-3"}: httpsRouteHR3, + {Namespace: "test", Name: "https-hr-4"}: httpsRouteHR4, + }, + AcceptedHostnames: map[string]struct{}{ + "foo.example.com": {}, + }, + }, }, }, Routes: map[types.NamespacedName]*route{ - {Namespace: "test", Name: "hr-3"}: routeHR3, - {Namespace: "test", Name: "hr-4"}: routeHR4, + {Namespace: "test", Name: "hr-3"}: routeHR3, + {Namespace: "test", Name: "hr-4"}: routeHR4, + {Namespace: "test", Name: "https-hr-3"}: httpsRouteHR3, + {Namespace: "test", Name: "https-hr-4"}: httpsRouteHR4, }, }, expected: Configuration{ @@ -265,8 +410,53 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, + HTTPSServers: []HTTPServer{ + { + Hostname: "foo.example.com", + SSL: &SSL{ + CertificatePath: secretPath, + }, + PathRules: []PathRule{ + { + Path: "/", + MatchRules: []MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: httpsHR3, + }, + { + MatchIdx: 0, + RuleIdx: 1, + Source: httpsHR4, + }, + }, + }, + { + Path: "/fourth", + MatchRules: []MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: httpsHR4, + }, + }, + }, + { + Path: "/third", + MatchRules: []MatchRule{ + { + MatchIdx: 0, + RuleIdx: 1, + Source: httpsHR3, + }, + }, + }, + }, + }, + }, }, - msg: "one listener with two routes with the same hostname with and without collisions", + msg: "one http and one https listener with two routes with the same hostname with and without collisions", }, { graph: &graph{ @@ -279,7 +469,8 @@ func TestBuildConfiguration(t *testing.T) { Source: &v1alpha2.Gateway{}, Listeners: map[string]*listener{ "listener-80-1": { - Valid: true, + Source: listener80, + Valid: true, Routes: map[types.NamespacedName]*route{ {Namespace: "test", Name: "hr-1"}: routeHR1, }, @@ -303,7 +494,8 @@ func TestBuildConfiguration(t *testing.T) { Source: &v1alpha2.Gateway{}, Listeners: map[string]*listener{ "listener-80-1": { - Valid: true, + Source: listener80, + Valid: true, Routes: map[types.NamespacedName]*route{ {Namespace: "test", Name: "hr-1"}: routeHR1, }, diff --git a/internal/state/graph.go b/internal/state/graph.go index 407990e783..a5dc096d7b 100644 --- a/internal/state/graph.go +++ b/internal/state/graph.go @@ -17,13 +17,14 @@ type gateway struct { } // listener represents a listener of the Gateway resource. -// FIXME(pleshakov) For now, we only support HTTP listeners. +// 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. Source v1alpha2.Listener // Valid shows whether the listener is valid. - // FIXME(pleshakov) For now, only capture true/false without any error message. Valid bool + // SecretPath is the path to the secret on disk. + SecretPath string // 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 @@ -70,12 +71,17 @@ type graph struct { } // 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) *graph { +func buildGraph( + store *store, + controllerName string, + gcName string, + secretMemoryMgr SecretMemoryManager, +) *graph { gc := buildGatewayClass(store.gc, controllerName) gw, ignoredGws := processGateways(store.gateways, gcName) - listeners := buildListeners(gw, gcName) + listeners := buildListeners(gw, gcName, secretMemoryMgr) routes := make(map[types.NamespacedName]*route) for _, ghr := range store.httpRoutes { @@ -277,44 +283,174 @@ func findAcceptedHostnames(listenerHostname *v1alpha2.Hostname, routeHostnames [ return result } -func buildListeners(gw *v1alpha2.Gateway, gcName string) map[string]*listener { - // FIXME(pleshakov): For now we require that all HTTP listeners bind to port 80 +func buildListeners(gw *v1alpha2.Gateway, gcName string, secretMemoryMgr SecretMemoryManager) map[string]*listener { listeners := make(map[string]*listener) if gw == nil || string(gw.Spec.GatewayClassName) != gcName { return listeners } - usedListenerHostnames := make(map[string]*listener) + listenerFactory := newListenerConfiguratorFactory(gw, secretMemoryMgr) for _, gl := range gw.Spec.Listeners { - valid := validateListener(gl) + configurator := listenerFactory.getConfiguratorForListener(gl) + listeners[string(gl.Name)] = configurator.configure(gl) + } + + return listeners +} - h := getHostname(gl.Hostname) +type listenerConfigurator interface { + configure(listener v1alpha2.Listener) *listener +} - // FIXME(pleshakov) This check will need to be done per each port once we support multiple ports. - if holder, exist := usedListenerHostnames[h]; exist { - valid = false - holder.Valid = false // all listeners for the same hostname become conflicted +type listenerConfiguratorFactory struct { + https *httpsListenerConfigurator + http *httpListenerConfigurator +} + +func (f *listenerConfiguratorFactory) getConfiguratorForListener(l v1alpha2.Listener) listenerConfigurator { + switch l.Protocol { + case v1alpha2.HTTPProtocolType: + return f.http + case v1alpha2.HTTPSProtocolType: + return f.https + default: + return newInvalidProtocolListenerConfigurator() + } +} + +func newListenerConfiguratorFactory(gw *v1alpha2.Gateway, secretMemoryMgr SecretMemoryManager) *listenerConfiguratorFactory { + return &listenerConfiguratorFactory{ + https: newHTTPSListenerConfigurator(gw, secretMemoryMgr), + http: newHTTPListenerConfigurator(), + } +} + +type httpsListenerConfigurator struct { + gateway *v1alpha2.Gateway + secretMemoryMgr SecretMemoryManager + usedHostnames map[string]*listener +} + +func newHTTPSListenerConfigurator(gateway *v1alpha2.Gateway, secretMemoryMgr SecretMemoryManager) *httpsListenerConfigurator { + return &httpsListenerConfigurator{ + gateway: gateway, + secretMemoryMgr: secretMemoryMgr, + usedHostnames: make(map[string]*listener), + } +} + +func (c *httpsListenerConfigurator) configure(gl v1alpha2.Listener) *listener { + var path string + var err error + + valid := validateHTTPSListener(gl, c.gateway.Namespace) + + if valid { + nsname := types.NamespacedName{ + Namespace: c.gateway.Namespace, + Name: string(gl.TLS.CertificateRefs[0].Name), } - l := &listener{ - Source: gl, - Valid: valid, - Routes: make(map[types.NamespacedName]*route), - AcceptedHostnames: make(map[string]struct{}), + path, err = c.secretMemoryMgr.Store(nsname) + if err != nil { + valid = false } + } - listeners[string(gl.Name)] = l - usedListenerHostnames[h] = l + h := getHostname(gl.Hostname) + + if holder, exist := c.usedHostnames[h]; exist { + valid = false + holder.Valid = false // all listeners for the same hostname become conflicted } - return listeners + l := &listener{ + Source: gl, + Valid: valid, + SecretPath: path, + Routes: make(map[types.NamespacedName]*route), + AcceptedHostnames: make(map[string]struct{}), + } + + c.usedHostnames[h] = l + + return l } -func validateListener(listener v1alpha2.Listener) bool { - // FIXME(pleshakov) For now, only support HTTP on port 80. - return listener.Protocol == v1alpha2.HTTPProtocolType && listener.Port == 80 +type httpListenerConfigurator struct { + usedHostnames map[string]*listener +} + +func newHTTPListenerConfigurator() *httpListenerConfigurator { + return &httpListenerConfigurator{ + usedHostnames: make(map[string]*listener), + } +} + +func (c *httpListenerConfigurator) configure(gl v1alpha2.Listener) *listener { + valid := validateHTTPListener(gl) + + h := getHostname(gl.Hostname) + + if holder, exist := c.usedHostnames[h]; exist { + valid = false + holder.Valid = false // all listeners for the same hostname become conflicted + } + + l := &listener{ + Source: gl, + Valid: valid, + Routes: make(map[types.NamespacedName]*route), + AcceptedHostnames: make(map[string]struct{}), + } + + c.usedHostnames[h] = l + + return l +} + +type invalidProtocolListenerConfigurator struct{} + +func newInvalidProtocolListenerConfigurator() *invalidProtocolListenerConfigurator { + return &invalidProtocolListenerConfigurator{} +} + +func (c *invalidProtocolListenerConfigurator) configure(gl v1alpha2.Listener) *listener { + return &listener{ + Source: gl, + Valid: false, + Routes: make(map[types.NamespacedName]*route), + AcceptedHostnames: make(map[string]struct{}), + } +} + +func validateHTTPListener(listener v1alpha2.Listener) bool { + // FIXME(pleshakov): For now we require that all HTTP listeners bind to port 80 + return listener.Port == 80 +} + +func validateHTTPSListener(listener v1alpha2.Listener, gwNsname string) bool { + // FIXME(kate-osborn): + // 1. For now we require that all HTTPS listeners bind to port 443 + // 2. Only TLSModeTerminate is supported. + if listener.Port != 443 || listener.TLS == nil || *listener.TLS.Mode != v1alpha2.TLSModeTerminate || len(listener.TLS.CertificateRefs) <= 0 { + return false + } + + certRef := listener.TLS.CertificateRefs[0] + // certRef Kind has default of "Secret" so it's safe to directly access the Kind here + if *certRef.Kind != "Secret" { + return false + } + + // secret must be in the same namespace as the gateway + if certRef.Namespace != nil && string(*certRef.Namespace) != gwNsname { + return false + } + + return true } func getHostname(h *v1alpha2.Hostname) string { diff --git a/internal/state/graph_test.go b/internal/state/graph_test.go index e8b0e8315b..85fbf8985b 100644 --- a/internal/state/graph_test.go +++ b/internal/state/graph_test.go @@ -4,6 +4,7 @@ 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/v1alpha2" @@ -11,12 +12,75 @@ import ( "github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers" ) +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) *v1alpha2.HTTPRoute { + + createRoute := func(name string, gatewayName string, listenerName string) *v1alpha2.HTTPRoute { return &v1alpha2.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test", @@ -28,7 +92,7 @@ func TestBuildGraph(t *testing.T) { { Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer("test")), Name: v1alpha2.ObjectName(gatewayName), - SectionName: (*v1alpha2.SectionName)(helpers.GetStringPointer("listener-80-1")), + SectionName: (*v1alpha2.SectionName)(helpers.GetStringPointer(listenerName)), }, }, }, @@ -49,8 +113,10 @@ func TestBuildGraph(t *testing.T) { }, } } - hr1 := createRoute("hr-1", "gateway-1") - hr2 := createRoute("hr-2", "wrong-gateway") + + 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 createGateway := func(name string) *v1alpha2.Gateway { return &v1alpha2.Gateway{ @@ -67,6 +133,23 @@ func TestBuildGraph(t *testing.T) { Port: 80, Protocol: v1alpha2.HTTPProtocolType, }, + + { + Name: "listener-443-1", + Hostname: nil, + Port: 443, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{ + { + Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer("test")), + }, + }, + }, + Protocol: v1alpha2.HTTPSProtocolType, + }, }, }, } @@ -88,6 +171,7 @@ func TestBuildGraph(t *testing.T) { httpRoutes: map[types.NamespacedName]*v1alpha2.HTTPRoute{ {Namespace: "test", Name: "hr-1"}: hr1, {Namespace: "test", Name: "hr-2"}: hr2, + {Namespace: "test", Name: "hr-3"}: hr3, }, } @@ -98,6 +182,15 @@ func TestBuildGraph(t *testing.T) { }, InvalidSectionNameRefs: map[string]struct{}{}, } + + routeHR3 := &route{ + Source: hr3, + ValidSectionNameRefs: map[string]struct{}{ + "listener-443-1": {}, + }, + InvalidSectionNameRefs: map[string]struct{}{}, + } + expected := &graph{ GatewayClass: &gatewayClass{ Source: store.gc, @@ -116,6 +209,17 @@ func TestBuildGraph(t *testing.T) { "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]*v1alpha2.Gateway{ @@ -123,10 +227,17 @@ func TestBuildGraph(t *testing.T) { }, Routes: map[types.NamespacedName]*route{ {Namespace: "test", Name: "hr-1"}: routeHR1, + {Namespace: "test", Name: "hr-3"}: routeHR3, }, } - result := buildGraph(store, controllerName, gcName) + // add test secret to store + secretStore := NewSecretStore() + secretStore.Upsert(testSecret) + + secretMemoryMgr := NewSecretDiskMemoryManager(secretsDirectory, secretStore) + + result := buildGraph(store, controllerName, gcName, secretMemoryMgr) if diff := cmp.Diff(expected, result); diff != "" { t.Errorf("buildGraph() mismatch (-want +got):\n%s", diff) } @@ -289,6 +400,45 @@ func TestBuildListeners(t *testing.T) { Protocol: v1alpha2.HTTPProtocolType, } + gatewayTLSConfig := &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{ + { + Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer("test")), + }, + }, + } + // https listeners + listener4431 := v1alpha2.Listener{ + Name: "listener-443-1", + Hostname: (*v1alpha2.Hostname)(helpers.GetStringPointer("foo.example.com")), + Port: 443, + TLS: gatewayTLSConfig, + Protocol: v1alpha2.HTTPSProtocolType, + } + listener4432 := v1alpha2.Listener{ + Name: "listener-443-2", + Hostname: (*v1alpha2.Hostname)(helpers.GetStringPointer("bar.example.com")), + Port: 443, + TLS: gatewayTLSConfig, + Protocol: v1alpha2.HTTPSProtocolType, + } + listener4433 := v1alpha2.Listener{ + Name: "listener-443-3", + Hostname: (*v1alpha2.Hostname)(helpers.GetStringPointer("foo.example.com")), + Port: 443, + TLS: gatewayTLSConfig, + Protocol: v1alpha2.HTTPSProtocolType, + } + listener4434 := v1alpha2.Listener{ + Name: "listener-443-4", + Hostname: (*v1alpha2.Hostname)(helpers.GetStringPointer("foo.example.com")), + Port: 443, + TLS: nil, // invalid https listener; missing tls config + Protocol: v1alpha2.HTTPSProtocolType, + } tests := []struct { gateway *v1alpha2.Gateway expected map[string]*listener @@ -296,12 +446,16 @@ func TestBuildListeners(t *testing.T) { }{ { gateway: &v1alpha2.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, Spec: v1alpha2.GatewaySpec{ GatewayClassName: gcName, Listeners: []v1alpha2.Listener{ listener801, }, }, + Status: v1alpha2.GatewayStatus{}, }, expected: map[string]*listener{ "listener-80-1": { @@ -311,10 +465,36 @@ func TestBuildListeners(t *testing.T) { AcceptedHostnames: map[string]struct{}{}, }, }, - msg: "valid listener", + msg: "valid http listener", }, { gateway: &v1alpha2.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1alpha2.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1alpha2.Listener{ + listener4431, + }, + }, + }, + expected: map[string]*listener{ + "listener-443-1": { + Source: listener4431, + Valid: true, + Routes: map[types.NamespacedName]*route{}, + AcceptedHostnames: map[string]struct{}{}, + SecretPath: secretPath, + }, + }, + msg: "valid https listener", + }, + { + gateway: &v1alpha2.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, Spec: v1alpha2.GatewaySpec{ GatewayClassName: gcName, Listeners: []v1alpha2.Listener{ @@ -330,14 +510,40 @@ func TestBuildListeners(t *testing.T) { AcceptedHostnames: map[string]struct{}{}, }, }, - msg: "invalid listener", + msg: "invalid listener protocol", + }, + { + gateway: &v1alpha2.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, + Spec: v1alpha2.GatewaySpec{ + GatewayClassName: gcName, + Listeners: []v1alpha2.Listener{ + listener4434, + }, + }, + }, + expected: map[string]*listener{ + "listener-443-4": { + Source: listener4434, + Valid: false, + Routes: map[types.NamespacedName]*route{}, + AcceptedHostnames: map[string]struct{}{}, + }, + }, + msg: "invalid https listener (tls config missing)", }, { gateway: &v1alpha2.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, Spec: v1alpha2.GatewaySpec{ GatewayClassName: gcName, Listeners: []v1alpha2.Listener{ listener801, listener803, + listener4431, listener4432, }, }, }, @@ -354,15 +560,33 @@ func TestBuildListeners(t *testing.T) { 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, + }, }, - msg: "two valid Listeners", + msg: "multiple valid http/https listeners", }, { gateway: &v1alpha2.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, Spec: v1alpha2.GatewaySpec{ GatewayClassName: gcName, Listeners: []v1alpha2.Listener{ listener801, listener804, + listener4431, listener4433, }, }, }, @@ -379,6 +603,20 @@ func TestBuildListeners(t *testing.T) { Routes: map[types.NamespacedName]*route{}, AcceptedHostnames: map[string]struct{}{}, }, + "listener-443-1": { + Source: listener4431, + Valid: false, + Routes: map[types.NamespacedName]*route{}, + AcceptedHostnames: map[string]struct{}{}, + SecretPath: secretPath, + }, + "listener-443-3": { + Source: listener4433, + Valid: false, + Routes: map[types.NamespacedName]*route{}, + AcceptedHostnames: map[string]struct{}{}, + SecretPath: secretPath, + }, }, msg: "collision", }, @@ -389,6 +627,9 @@ func TestBuildListeners(t *testing.T) { }, { gateway: &v1alpha2.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + }, Spec: v1alpha2.GatewaySpec{ GatewayClassName: "wrong-class", Listeners: []v1alpha2.Listener{ @@ -401,8 +642,15 @@ func TestBuildListeners(t *testing.T) { }, } + // add secret to store + secretStore := NewSecretStore() + secretStore.Upsert(testSecret) + + secretMemoryMgr := NewSecretDiskMemoryManager(secretsDirectory, secretStore) + for _, test := range tests { - result := buildListeners(test.gateway, gcName) + result := buildListeners(test.gateway, gcName, secretMemoryMgr) + if diff := cmp.Diff(test.expected, result); diff != "" { t.Errorf("buildListeners() %q mismatch (-want +got):\n%s", test.msg, diff) } @@ -736,7 +984,7 @@ func TestFindAcceptedHostnames(t *testing.T) { } -func TestValidateListener(t *testing.T) { +func TestValidateHTTPListener(t *testing.T) { tests := []struct { l v1alpha2.Listener expected bool @@ -758,18 +1006,125 @@ func TestValidateListener(t *testing.T) { expected: false, msg: "invalid port", }, + } + + for _, test := range tests { + result := validateHTTPListener(test.l) + if result != test.expected { + t.Errorf("validateListener() returned %v but expected %v for the case of %q", result, test.expected, test.msg) + } + } +} + +func TestValidateHTTPSListener(t *testing.T) { + gwNs := "gateway-ns" + + validSecretRef := &v1alpha2.SecretObjectReference{ + Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer(gwNs)), + } + + invalidSecretRefType := &v1alpha2.SecretObjectReference{ + Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("ConfigMap")), + Name: "secret", + Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer(gwNs)), + } + + invalidSecretRefTNamespace := &v1alpha2.SecretObjectReference{ + Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer("diff-ns")), + } + + tests := []struct { + l v1alpha2.Listener + expected bool + msg string + }{ + { + l: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{validSecretRef}, + }, + }, + expected: true, + msg: "valid", + }, { l: v1alpha2.Listener{ Port: 80, - Protocol: v1alpha2.TCPProtocolType, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{validSecretRef}, + }, + }, + expected: false, + msg: "invalid port", + }, + { + l: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + }, + }, + expected: false, + msg: "invalid - no cert ref", + }, + { + l: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModePassthrough), + CertificateRefs: []*v1alpha2.SecretObjectReference{validSecretRef}, + }, + }, + expected: false, + msg: "invalid tls mode", + }, + { + l: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{invalidSecretRefType}, + }, + }, + expected: false, + msg: "invalid cert ref kind", + }, + { + l: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{invalidSecretRefTNamespace}, + }, + }, + expected: false, + msg: "invalid cert ref namespace", + }, + { + l: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, }, expected: false, - msg: "invalid protocol", + msg: "invalid - no tls config", }, } for _, test := range tests { - result := validateListener(test.l) + result := validateHTTPSListener(test.l, gwNs) if result != test.expected { t.Errorf("validateListener() returned %v but expected %v for the case of %q", result, test.expected, test.msg) } diff --git a/internal/state/secrets.go b/internal/state/secrets.go new file mode 100644 index 0000000000..1d3212b2cd --- /dev/null +++ b/internal/state/secrets.go @@ -0,0 +1,177 @@ +package state + +import ( + "bytes" + "crypto/tls" + "fmt" + "io/ioutil" + "os" + "path" + + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" +) + +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . SecretStore +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . SecretMemoryManager + +// tlsSecretFileMode defines the default file mode for files with TLS Secrets. +const tlsSecretFileMode = 0o600 + +type SecretStore interface { + // Upsert upserts the secret into the store. + Upsert(secret *apiv1.Secret) + // Delete deletes the secret from the store. + Delete(nsname types.NamespacedName) + // Get gets the secret from the store. + Get(nsname types.NamespacedName) *Secret +} + +type secretStore struct { + secrets map[types.NamespacedName]*Secret +} + +// Secret is the internal representation of a Kubernetes Secret. +type Secret struct { + // Secret is the Kubernetes Secret object. + Secret *apiv1.Secret + // Valid is whether the Kubernetes Secret is valid. + Valid bool +} + +func NewSecretStore() *secretStore { + return &secretStore{ + secrets: make(map[types.NamespacedName]*Secret), + } +} + +func (s secretStore) Upsert(secret *apiv1.Secret) { + nsname := types.NamespacedName{ + Namespace: secret.Namespace, + Name: secret.Name, + } + + valid := isSecretValid(secret) + s.secrets[nsname] = &Secret{Secret: secret, Valid: valid} +} + +func (s secretStore) Delete(nsname types.NamespacedName) { + delete(s.secrets, nsname) +} + +func (s secretStore) Get(nsname types.NamespacedName) *Secret { + return s.secrets[nsname] +} + +type SecretMemoryManager interface { + // Store stores the secret in memory so that it can be written to disk before reloading NGINX. + // Returns the path to the secret and an error if the secret does not exist in the cache or the secret is invalid. + Store(nsname types.NamespacedName) (string, error) + // WriteAllStoredSecrets writes all stored secrets to disk. + WriteAllStoredSecrets() error +} + +type secretDiskMemoryManager struct { + storedSecrets map[types.NamespacedName]storedSecret + secretCache SecretStore + secretDirectory string +} + +type storedSecret struct { + secret *apiv1.Secret + path string +} + +func NewSecretDiskMemoryManager(secretDirectory string, secretStore SecretStore) *secretDiskMemoryManager { + return &secretDiskMemoryManager{ + storedSecrets: make(map[types.NamespacedName]storedSecret), + secretCache: secretStore, + secretDirectory: secretDirectory, + } +} + +func (s *secretDiskMemoryManager) Store(nsname types.NamespacedName) (string, error) { + secret := s.secretCache.Get(nsname) + if secret == nil { + return "", fmt.Errorf("secret %s does not exist", nsname) + } + + if !secret.Valid { + return "", fmt.Errorf("secret %s is not valid; must be of type %s and contain a valid X509 key pair", nsname, apiv1.SecretTypeTLS) + } + + ss := storedSecret{ + secret: secret.Secret, + path: path.Join(s.secretDirectory, generateFilepathForSecret(nsname)), + } + + s.storedSecrets[nsname] = ss + + return ss.path, nil +} + +func (s *secretDiskMemoryManager) WriteAllStoredSecrets() error { + // Remove all existing secrets from secrets directory + dir, err := ioutil.ReadDir(s.secretDirectory) + if err != nil { + return fmt.Errorf("failed to remove all secrets from %s: %w", s.secretDirectory, err) + } + + for _, d := range dir { + filepath := path.Join(s.secretDirectory, d.Name()) + if err := os.Remove(filepath); err != nil { + return fmt.Errorf("failed to remove secret %s: %w", filepath, err) + } + } + + // Write all secrets to secrets directory + for nsname, ss := range s.storedSecrets { + + file, err := os.Create(ss.path) + if err != nil { + return fmt.Errorf("failed to create file %s for secret %s: %w", ss.path, nsname, err) + } + + if err = file.Chmod(tlsSecretFileMode); err != nil { + return fmt.Errorf("failed to change mode of file %s for secret %s: %w", ss.path, nsname, err) + } + + contents := generateCertAndKeyFileContent(ss.secret) + + _, err = file.Write(contents) + if err != nil { + return fmt.Errorf("failed to write secret %s to file %s: %w", nsname, ss.path, err) + } + + } + + // reset stored secrets + s.storedSecrets = make(map[types.NamespacedName]storedSecret) + + return nil +} + +func isSecretValid(secret *apiv1.Secret) bool { + if secret.Type != apiv1.SecretTypeTLS { + return false + } + + // A TLS Secret is guaranteed to have these data fields. + _, err := tls.X509KeyPair(secret.Data[apiv1.TLSCertKey], secret.Data[apiv1.TLSPrivateKeyKey]) + + return err == nil +} + +func generateCertAndKeyFileContent(secret *apiv1.Secret) []byte { + var res bytes.Buffer + + res.Write(secret.Data[apiv1.TLSCertKey]) + res.WriteString("\n") + res.Write(secret.Data[apiv1.TLSPrivateKeyKey]) + + return res.Bytes() +} + +func generateFilepathForSecret(nsname types.NamespacedName) string { + return nsname.Namespace + "-" + nsname.Name +} diff --git a/internal/state/secrets_test.go b/internal/state/secrets_test.go new file mode 100644 index 0000000000..14eb10bae2 --- /dev/null +++ b/internal/state/secrets_test.go @@ -0,0 +1,318 @@ +package state_test + +import ( + "io/ioutil" + "os" + "path" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiv1 "k8s.io/api/core/v1" + 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" +) + +var ( + cert = []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-----`) + + key = []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-----`) + + invalidCert = []byte(`-----BEGIN CERTIFICATE----- +-----END CERTIFICATE-----`) + + invalidKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +-----END RSA PRIVATE KEY-----`) +) + +var ( + secret1 = &apiv1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "secret1", + }, + Data: map[string][]byte{ + apiv1.TLSCertKey: cert, + apiv1.TLSPrivateKeyKey: key, + }, + Type: apiv1.SecretTypeTLS, + } + + secret2 = &apiv1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "secret2", + }, + Data: map[string][]byte{ + apiv1.TLSCertKey: cert, + apiv1.TLSPrivateKeyKey: key, + }, + Type: apiv1.SecretTypeTLS, + } + + secret3 = &apiv1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "secret3", + }, + Data: map[string][]byte{ + apiv1.TLSCertKey: cert, + apiv1.TLSPrivateKeyKey: key, + }, + Type: apiv1.SecretTypeTLS, + } + + invalidSecretType = &apiv1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "invalid-type", + }, + Data: map[string][]byte{ + apiv1.TLSCertKey: cert, + apiv1.TLSPrivateKeyKey: key, + }, + Type: apiv1.SecretTypeDockercfg, + } + invalidSecretKey = &apiv1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "invalid-key", + }, + Data: map[string][]byte{ + apiv1.TLSCertKey: cert, + apiv1.TLSPrivateKeyKey: invalidKey, + }, + Type: apiv1.SecretTypeTLS, + } +) + +var _ = Describe("SecretMemoryManager", func() { + var ( + fakeStore *statefakes.FakeSecretStore + memMgr state.SecretMemoryManager + tmpSecretsDir string + ) + + BeforeEach(OncePerOrdered, func() { + dir, err := os.MkdirTemp("", "secrets-test") + tmpSecretsDir = dir + Expect(err).ToNot(HaveOccurred(), "failed to create temp directory for tests") + + fakeStore = &statefakes.FakeSecretStore{} + memMgr = state.NewSecretDiskMemoryManager(tmpSecretsDir, fakeStore) + }) + + AfterEach(OncePerOrdered, func() { + Expect(os.RemoveAll(tmpSecretsDir)).To(Succeed()) + }) + + Describe("Manages secrets on disk", Ordered, func() { + testStore := func(s *apiv1.Secret, expPath string, expErr bool) { + nsname := types.NamespacedName{Namespace: s.Namespace, Name: s.Name} + actualPath, err := memMgr.Store(nsname) + + if expErr { + Expect(err).To(HaveOccurred()) + Expect(actualPath).To(BeEmpty()) + } else { + Expect(err).ToNot(HaveOccurred()) + Expect(actualPath).To(Equal(expPath)) + } + } + + It("should return an error and empty path when secret does not exist", func() { + fakeStore.GetReturns(nil) + + testStore(secret1, "", true) + }) + It("should store a valid secret", func() { + fakeStore.GetReturns(&state.Secret{Secret: secret1, Valid: true}) + expectedPath := path.Join(tmpSecretsDir, "test-secret1") + + testStore(secret1, expectedPath, false) + }) + + It("should store another valid secret", func() { + fakeStore.GetReturns(&state.Secret{Secret: secret2, Valid: true}) + expectedPath := path.Join(tmpSecretsDir, "test-secret2") + + testStore(secret2, expectedPath, false) + }) + + It("should return an error and empty path when secret is invalid", func() { + fakeStore.GetReturns(&state.Secret{Secret: invalidSecretType, Valid: false}) + + testStore(invalidSecretType, "", true) + }) + + It("should write all stored secrets", func() { + err := memMgr.WriteAllStoredSecrets() + Expect(err).ToNot(HaveOccurred()) + + expectedFileNames := []string{"test-secret1", "test-secret2"} + + // read all files from directory + dir, err := ioutil.ReadDir(tmpSecretsDir) + Expect(err).ToNot(HaveOccurred()) + + // test that the files exist that we expect + Expect(dir).To(HaveLen(2)) + actualFilenames := []string{dir[0].Name(), dir[1].Name()} + Expect(actualFilenames).To(ConsistOf(expectedFileNames)) + }) + + It("should store secret after write", func() { + fakeStore.GetReturns(&state.Secret{Secret: secret3, Valid: true}) + expectedPath := path.Join(tmpSecretsDir, "test-secret3") + + testStore(secret3, expectedPath, false) + }) + + It("should write all stored secrets", func() { + err := memMgr.WriteAllStoredSecrets() + Expect(err).ToNot(HaveOccurred()) + + // read all files from directory + dir, err := ioutil.ReadDir(tmpSecretsDir) + Expect(err).ToNot(HaveOccurred()) + + // only the secrets stored after the last write should be written to disk. + Expect(dir).To(HaveLen(1)) + Expect(dir[0].Name()).To(Equal("test-secret3")) + }) + }) +}) + +var _ = Describe("SecretStore", func() { + var store state.SecretStore + var invalidToValidSecret, validToInvalidSecret *apiv1.Secret + + BeforeEach(OncePerOrdered, func() { + store = state.NewSecretStore() + + invalidToValidSecret = invalidSecretType.DeepCopy() + invalidToValidSecret.Type = apiv1.SecretTypeTLS + + validToInvalidSecret = secret1.DeepCopy() + validToInvalidSecret.Data[apiv1.TLSCertKey] = invalidCert + + }) + + Describe("handles CRUD events on secrets", Ordered, func() { + testUpsert := func(s *apiv1.Secret, valid bool) { + store.Upsert(s) + + nsname := types.NamespacedName{Namespace: s.Namespace, Name: s.Name} + actualSecret := store.Get(nsname) + if valid { + Expect(actualSecret.Valid).To(BeTrue()) + } + Expect(actualSecret.Secret).To(Equal(s)) + } + + testDelete := func(nsname types.NamespacedName) { + store.Delete(nsname) + + s := store.Get(nsname) + Expect(s).To(BeNil()) + } + + It("adds a new valid secret", func() { + testUpsert(secret1, true) + }) + It("adds another new valid secret", func() { + testUpsert(secret2, true) + }) + It("adds a secret with an invalid type", func() { + testUpsert(invalidSecretType, false) + }) + It("adds a secret with an invalid key", func() { + testUpsert(invalidSecretKey, false) + }) + It("deletes an invalid secret", func() { + nsname := types.NamespacedName{Namespace: "test", Name: "invalid-key"} + + testDelete(nsname) + }) + It("updates an invalid secret to valid", func() { + testUpsert(invalidToValidSecret, true) + }) + It("updates an valid secret to invalid (invalid cert)", func() { + testUpsert(validToInvalidSecret, false) + }) + It("deletes a secret", func() { + nsname := types.NamespacedName{Namespace: "test", Name: "invalid-type"} + + testDelete(nsname) + }) + It("deletes a secret", func() { + nsname := types.NamespacedName{Namespace: "test", Name: "secret1"} + + testDelete(nsname) + }) + It("gets remaining secret", func() { + nsname := types.NamespacedName{Namespace: "test", Name: "secret2"} + + s := store.Get(nsname) + Expect(s.Valid).To(BeTrue()) + Expect(s.Secret).To(Equal(secret2)) + }) + It("deletes final secret", func() { + nsname := types.NamespacedName{Namespace: "test", Name: "secret2"} + + testDelete(nsname) + }) + It("does not panic when secret is deleted that does not exist", func() { + nsname := types.NamespacedName{Namespace: "test", Name: "dne"} + + store.Delete(nsname) + }) + }) +}) diff --git a/internal/state/statefakes/fake_secret_memory_manager.go b/internal/state/statefakes/fake_secret_memory_manager.go new file mode 100644 index 0000000000..76c7484d56 --- /dev/null +++ b/internal/state/statefakes/fake_secret_memory_manager.go @@ -0,0 +1,182 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package statefakes + +import ( + "sync" + + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + "k8s.io/apimachinery/pkg/types" +) + +type FakeSecretMemoryManager struct { + StoreStub func(types.NamespacedName) (string, error) + storeMutex sync.RWMutex + storeArgsForCall []struct { + arg1 types.NamespacedName + } + storeReturns struct { + result1 string + result2 error + } + storeReturnsOnCall map[int]struct { + result1 string + result2 error + } + WriteAllStoredSecretsStub func() error + writeAllStoredSecretsMutex sync.RWMutex + writeAllStoredSecretsArgsForCall []struct { + } + writeAllStoredSecretsReturns struct { + result1 error + } + writeAllStoredSecretsReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeSecretMemoryManager) Store(arg1 types.NamespacedName) (string, error) { + fake.storeMutex.Lock() + ret, specificReturn := fake.storeReturnsOnCall[len(fake.storeArgsForCall)] + fake.storeArgsForCall = append(fake.storeArgsForCall, struct { + arg1 types.NamespacedName + }{arg1}) + stub := fake.StoreStub + fakeReturns := fake.storeReturns + fake.recordInvocation("Store", []interface{}{arg1}) + fake.storeMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSecretMemoryManager) StoreCallCount() int { + fake.storeMutex.RLock() + defer fake.storeMutex.RUnlock() + return len(fake.storeArgsForCall) +} + +func (fake *FakeSecretMemoryManager) StoreCalls(stub func(types.NamespacedName) (string, error)) { + fake.storeMutex.Lock() + defer fake.storeMutex.Unlock() + fake.StoreStub = stub +} + +func (fake *FakeSecretMemoryManager) StoreArgsForCall(i int) types.NamespacedName { + fake.storeMutex.RLock() + defer fake.storeMutex.RUnlock() + argsForCall := fake.storeArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeSecretMemoryManager) StoreReturns(result1 string, result2 error) { + fake.storeMutex.Lock() + defer fake.storeMutex.Unlock() + fake.StoreStub = nil + fake.storeReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeSecretMemoryManager) StoreReturnsOnCall(i int, result1 string, result2 error) { + fake.storeMutex.Lock() + defer fake.storeMutex.Unlock() + fake.StoreStub = nil + if fake.storeReturnsOnCall == nil { + fake.storeReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.storeReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeSecretMemoryManager) WriteAllStoredSecrets() error { + fake.writeAllStoredSecretsMutex.Lock() + ret, specificReturn := fake.writeAllStoredSecretsReturnsOnCall[len(fake.writeAllStoredSecretsArgsForCall)] + fake.writeAllStoredSecretsArgsForCall = append(fake.writeAllStoredSecretsArgsForCall, struct { + }{}) + stub := fake.WriteAllStoredSecretsStub + fakeReturns := fake.writeAllStoredSecretsReturns + fake.recordInvocation("WriteAllStoredSecrets", []interface{}{}) + fake.writeAllStoredSecretsMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeSecretMemoryManager) WriteAllStoredSecretsCallCount() int { + fake.writeAllStoredSecretsMutex.RLock() + defer fake.writeAllStoredSecretsMutex.RUnlock() + return len(fake.writeAllStoredSecretsArgsForCall) +} + +func (fake *FakeSecretMemoryManager) WriteAllStoredSecretsCalls(stub func() error) { + fake.writeAllStoredSecretsMutex.Lock() + defer fake.writeAllStoredSecretsMutex.Unlock() + fake.WriteAllStoredSecretsStub = stub +} + +func (fake *FakeSecretMemoryManager) WriteAllStoredSecretsReturns(result1 error) { + fake.writeAllStoredSecretsMutex.Lock() + defer fake.writeAllStoredSecretsMutex.Unlock() + fake.WriteAllStoredSecretsStub = nil + fake.writeAllStoredSecretsReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSecretMemoryManager) WriteAllStoredSecretsReturnsOnCall(i int, result1 error) { + fake.writeAllStoredSecretsMutex.Lock() + defer fake.writeAllStoredSecretsMutex.Unlock() + fake.WriteAllStoredSecretsStub = nil + if fake.writeAllStoredSecretsReturnsOnCall == nil { + fake.writeAllStoredSecretsReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.writeAllStoredSecretsReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeSecretMemoryManager) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.storeMutex.RLock() + defer fake.storeMutex.RUnlock() + fake.writeAllStoredSecretsMutex.RLock() + defer fake.writeAllStoredSecretsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeSecretMemoryManager) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ state.SecretMemoryManager = new(FakeSecretMemoryManager) diff --git a/internal/state/statefakes/fake_secret_store.go b/internal/state/statefakes/fake_secret_store.go new file mode 100644 index 0000000000..a74c93c747 --- /dev/null +++ b/internal/state/statefakes/fake_secret_store.go @@ -0,0 +1,191 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package statefakes + +import ( + "sync" + + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" +) + +type FakeSecretStore struct { + DeleteStub func(types.NamespacedName) + deleteMutex sync.RWMutex + deleteArgsForCall []struct { + arg1 types.NamespacedName + } + GetStub func(types.NamespacedName) *state.Secret + getMutex sync.RWMutex + getArgsForCall []struct { + arg1 types.NamespacedName + } + getReturns struct { + result1 *state.Secret + } + getReturnsOnCall map[int]struct { + result1 *state.Secret + } + UpsertStub func(*v1.Secret) + upsertMutex sync.RWMutex + upsertArgsForCall []struct { + arg1 *v1.Secret + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeSecretStore) Delete(arg1 types.NamespacedName) { + fake.deleteMutex.Lock() + fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { + arg1 types.NamespacedName + }{arg1}) + stub := fake.DeleteStub + fake.recordInvocation("Delete", []interface{}{arg1}) + fake.deleteMutex.Unlock() + if stub != nil { + fake.DeleteStub(arg1) + } +} + +func (fake *FakeSecretStore) DeleteCallCount() int { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return len(fake.deleteArgsForCall) +} + +func (fake *FakeSecretStore) DeleteCalls(stub func(types.NamespacedName)) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = stub +} + +func (fake *FakeSecretStore) DeleteArgsForCall(i int) types.NamespacedName { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + argsForCall := fake.deleteArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeSecretStore) Get(arg1 types.NamespacedName) *state.Secret { + fake.getMutex.Lock() + ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)] + fake.getArgsForCall = append(fake.getArgsForCall, struct { + arg1 types.NamespacedName + }{arg1}) + stub := fake.GetStub + fakeReturns := fake.getReturns + fake.recordInvocation("Get", []interface{}{arg1}) + fake.getMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeSecretStore) GetCallCount() int { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + return len(fake.getArgsForCall) +} + +func (fake *FakeSecretStore) GetCalls(stub func(types.NamespacedName) *state.Secret) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = stub +} + +func (fake *FakeSecretStore) GetArgsForCall(i int) types.NamespacedName { + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + argsForCall := fake.getArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeSecretStore) GetReturns(result1 *state.Secret) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + fake.getReturns = struct { + result1 *state.Secret + }{result1} +} + +func (fake *FakeSecretStore) GetReturnsOnCall(i int, result1 *state.Secret) { + fake.getMutex.Lock() + defer fake.getMutex.Unlock() + fake.GetStub = nil + if fake.getReturnsOnCall == nil { + fake.getReturnsOnCall = make(map[int]struct { + result1 *state.Secret + }) + } + fake.getReturnsOnCall[i] = struct { + result1 *state.Secret + }{result1} +} + +func (fake *FakeSecretStore) Upsert(arg1 *v1.Secret) { + fake.upsertMutex.Lock() + fake.upsertArgsForCall = append(fake.upsertArgsForCall, struct { + arg1 *v1.Secret + }{arg1}) + stub := fake.UpsertStub + fake.recordInvocation("Upsert", []interface{}{arg1}) + fake.upsertMutex.Unlock() + if stub != nil { + fake.UpsertStub(arg1) + } +} + +func (fake *FakeSecretStore) UpsertCallCount() int { + fake.upsertMutex.RLock() + defer fake.upsertMutex.RUnlock() + return len(fake.upsertArgsForCall) +} + +func (fake *FakeSecretStore) UpsertCalls(stub func(*v1.Secret)) { + fake.upsertMutex.Lock() + defer fake.upsertMutex.Unlock() + fake.UpsertStub = stub +} + +func (fake *FakeSecretStore) UpsertArgsForCall(i int) *v1.Secret { + fake.upsertMutex.RLock() + defer fake.upsertMutex.RUnlock() + argsForCall := fake.upsertArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeSecretStore) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + fake.getMutex.RLock() + defer fake.getMutex.RUnlock() + fake.upsertMutex.RLock() + defer fake.upsertMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeSecretStore) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ state.SecretStore = new(FakeSecretStore) diff --git a/pkg/sdk/interfaces.go b/pkg/sdk/interfaces.go index adbe11fca4..4cc83b5c42 100644 --- a/pkg/sdk/interfaces.go +++ b/pkg/sdk/interfaces.go @@ -33,3 +33,8 @@ type ServiceImpl interface { Upsert(svc *apiv1.Service) Remove(nsname types.NamespacedName) } + +type SecretImpl interface { + Upsert(secret *apiv1.Secret) + Remove(name types.NamespacedName) +} diff --git a/pkg/sdk/secret_controller.go b/pkg/sdk/secret_controller.go new file mode 100644 index 0000000000..107538a7dc --- /dev/null +++ b/pkg/sdk/secret_controller.go @@ -0,0 +1,72 @@ +package sdk + +import ( + "context" + + apiv1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctlr "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type secretReconciler struct { + client.Client + scheme *runtime.Scheme + impl SecretImpl +} + +var ignoredNamespaces = map[string]struct{}{ + "kube-system": {}, + "kube-public": {}, +} + +// RegisterSecretController registers the SecretController in the manager. +func RegisterSecretController(mgr manager.Manager, impl SecretImpl) error { + r := &secretReconciler{ + Client: mgr.GetClient(), + scheme: mgr.GetScheme(), + impl: impl, + } + + return ctlr.NewControllerManagedBy(mgr). + For(&apiv1.Secret{}). + WithEventFilter(predicate.NewPredicateFuncs(func(obj client.Object) bool { + _, isIgnoredNS := ignoredNamespaces[obj.GetNamespace()] + return !isIgnoredNS + })). + Complete(r) +} + +func (r *secretReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + log := log.FromContext(ctx).WithValues("secret", req.NamespacedName) + + log.V(3).Info("Reconciling Secret") + + found := true + var secret apiv1.Secret + err := r.Get(ctx, req.NamespacedName, &secret) + if err != nil { + if !apierrors.IsNotFound(err) { + log.Error(err, "Failed to get Secret") + return reconcile.Result{}, err + } + found = false + } + + if !found { + log.V(3).Info("Removing Secret") + + r.impl.Remove(req.NamespacedName) + return reconcile.Result{}, nil + } + + log.V(3).Info("Upserting Secret") + + r.impl.Upsert(&secret) + return reconcile.Result{}, nil +} From e33be3a5ccabcc9a18bb96115a80f7b69f6d698c Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Tue, 12 Jul 2022 10:56:51 -0600 Subject: [PATCH 02/42] Add nolint for test certs --- internal/state/graph_test.go | 1 + internal/state/secrets_test.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/state/graph_test.go b/internal/state/graph_test.go index 85fbf8985b..d023678d0d 100644 --- a/internal/state/graph_test.go +++ b/internal/state/graph_test.go @@ -1,3 +1,4 @@ +// nolint:gosec package state import ( diff --git a/internal/state/secrets_test.go b/internal/state/secrets_test.go index 14eb10bae2..1727b0f43f 100644 --- a/internal/state/secrets_test.go +++ b/internal/state/secrets_test.go @@ -1,3 +1,4 @@ +// nolint:gosec package state_test import ( @@ -36,7 +37,6 @@ PpuwvuatqjO1208BjfE/cZHIg8Hw9mvW9x9C+IQMIMDE7b/g6OcK7LGTLwlFxvA8 7WjEequnayIphMhKRXVf1N349eN98Ez38fOTHTPbdJjFA/PcC+Gyme+iGt5OQdFh yRE= -----END CERTIFICATE-----`) - key = []byte(`-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAqeip+7MvNadI7if3MpPrwgJpbH47RTNprmTStCrXnKhtn41k G+ecUNWBQNlzksBwGL0ncCNo2a7lvKl1wi9q2+AmQhccKVRAq3MXY5t7XlYkV1bG From 514c63f635befd073fbc83904726d68866048200 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Tue, 12 Jul 2022 11:33:03 -0600 Subject: [PATCH 03/42] Another nolint --- internal/manager/manager.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/manager/manager.go b/internal/manager/manager.go index bae5db50dc..683677b57a 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -29,6 +29,7 @@ import ( const clusterTimeout = 10 * time.Second // secretsFolder is the folder that holds all the secrets for NGINX servers. +// nolint:gosec const secretsFolder = "/etc/nginx/secrets" var scheme = runtime.NewScheme() From 79b1daae0a0dad6f01c179246eb59c34fb4ff92d Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Tue, 12 Jul 2022 11:41:32 -0600 Subject: [PATCH 04/42] Last nolint --- internal/state/configuration_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/state/configuration_test.go b/internal/state/configuration_test.go index 1f95efa992..c317a2fb28 100644 --- a/internal/state/configuration_test.go +++ b/internal/state/configuration_test.go @@ -152,6 +152,7 @@ func TestBuildConfiguration(t *testing.T) { }, } + // nolint:gosec secretPath := "/etc/nginx/secrets/secret" tests := []struct { From 23f91c82db4f688d9507f27f8e9c8a2a8fc83e2c Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Tue, 12 Jul 2022 11:49:13 -0600 Subject: [PATCH 05/42] Lint warning fixes --- internal/state/secrets.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/internal/state/secrets.go b/internal/state/secrets.go index 1d3212b2cd..3d060f21ff 100644 --- a/internal/state/secrets.go +++ b/internal/state/secrets.go @@ -18,6 +18,7 @@ import ( // tlsSecretFileMode defines the default file mode for files with TLS Secrets. const tlsSecretFileMode = 0o600 +// SecretStore stores secrets. type SecretStore interface { // Upsert upserts the secret into the store. Upsert(secret *apiv1.Secret) @@ -27,7 +28,7 @@ type SecretStore interface { Get(nsname types.NamespacedName) *Secret } -type secretStore struct { +type SecretStoreImpl struct { secrets map[types.NamespacedName]*Secret } @@ -39,13 +40,13 @@ type Secret struct { Valid bool } -func NewSecretStore() *secretStore { - return &secretStore{ +func NewSecretStore() *SecretStoreImpl { + return &SecretStoreImpl{ secrets: make(map[types.NamespacedName]*Secret), } } -func (s secretStore) Upsert(secret *apiv1.Secret) { +func (s SecretStoreImpl) Upsert(secret *apiv1.Secret) { nsname := types.NamespacedName{ Namespace: secret.Namespace, Name: secret.Name, @@ -55,11 +56,11 @@ func (s secretStore) Upsert(secret *apiv1.Secret) { s.secrets[nsname] = &Secret{Secret: secret, Valid: valid} } -func (s secretStore) Delete(nsname types.NamespacedName) { +func (s SecretStoreImpl) Delete(nsname types.NamespacedName) { delete(s.secrets, nsname) } -func (s secretStore) Get(nsname types.NamespacedName) *Secret { +func (s SecretStoreImpl) Get(nsname types.NamespacedName) *Secret { return s.secrets[nsname] } @@ -71,7 +72,7 @@ type SecretMemoryManager interface { WriteAllStoredSecrets() error } -type secretDiskMemoryManager struct { +type SecretDiskMemoryManager struct { storedSecrets map[types.NamespacedName]storedSecret secretCache SecretStore secretDirectory string @@ -82,15 +83,15 @@ type storedSecret struct { path string } -func NewSecretDiskMemoryManager(secretDirectory string, secretStore SecretStore) *secretDiskMemoryManager { - return &secretDiskMemoryManager{ +func NewSecretDiskMemoryManager(secretDirectory string, secretStore SecretStore) *SecretDiskMemoryManager { + return &SecretDiskMemoryManager{ storedSecrets: make(map[types.NamespacedName]storedSecret), secretCache: secretStore, secretDirectory: secretDirectory, } } -func (s *secretDiskMemoryManager) Store(nsname types.NamespacedName) (string, error) { +func (s *SecretDiskMemoryManager) Store(nsname types.NamespacedName) (string, error) { secret := s.secretCache.Get(nsname) if secret == nil { return "", fmt.Errorf("secret %s does not exist", nsname) @@ -110,7 +111,7 @@ func (s *secretDiskMemoryManager) Store(nsname types.NamespacedName) (string, er return ss.path, nil } -func (s *secretDiskMemoryManager) WriteAllStoredSecrets() error { +func (s *SecretDiskMemoryManager) WriteAllStoredSecrets() error { // Remove all existing secrets from secrets directory dir, err := ioutil.ReadDir(s.secretDirectory) if err != nil { From 62fdc19a2ffb34707cd1845c851edba3994790dd Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 14 Jul 2022 14:37:55 -0600 Subject: [PATCH 06/42] Add event loop config object --- internal/events/loop.go | 99 +++++++++++++++++------------------- internal/events/loop_test.go | 24 ++++----- internal/manager/manager.go | 24 ++++----- 3 files changed, 70 insertions(+), 77 deletions(-) diff --git a/internal/events/loop.go b/internal/events/loop.go index d03bfb5e93..164e6e9d26 100644 --- a/internal/events/loop.go +++ b/internal/events/loop.go @@ -15,45 +15,38 @@ import ( "github.com/nginxinc/nginx-kubernetes-gateway/internal/status" ) +// EventLoopConfig holds configuration parameters for EventLoop. +type EventLoopConfig struct { + // Processor is the state ChangeProcessor. + Processor state.ChangeProcessor + // ServiceStore is the state ServiceStore. + ServiceStore state.ServiceStore + // SecretStore is the state SecretStore. + SecretStore state.SecretStore + // SecretMemoryManager is the state SecretMemoryManager. + SecretMemoryManager state.SecretMemoryManager + // Generator is the nginx config Generator. + Generator config.Generator + // EventCh is a read-only channel for events. + EventCh <-chan interface{} + // Logger is the logger to be used by the EventLoop. + Logger logr.Logger + // NginxFileMgr is the file Manager for nginx. + NginxFileMgr file.Manager + // NginxRuntimeMgr manages nginx runtime. + NginxRuntimeMgr runtime.Manager + // StatusUpdater updates statuses on Kubernetes resources. + StatusUpdater status.Updater +} + // EventLoop is the main event loop of the Gateway. type EventLoop struct { - processor state.ChangeProcessor - serviceStore state.ServiceStore - secretStore state.SecretStore - secretMemoryManager state.SecretMemoryManager - generator config.Generator - eventCh <-chan interface{} - logger logr.Logger - nginxFileMgr file.Manager - nginxRuntimeMgr runtime.Manager - statusUpdater status.Updater + cfg EventLoopConfig } // NewEventLoop creates a new EventLoop. -func NewEventLoop( - processor state.ChangeProcessor, - serviceStore state.ServiceStore, - secretStore state.SecretStore, - secretMemoryManager state.SecretMemoryManager, - generator config.Generator, - eventCh <-chan interface{}, - logger logr.Logger, - nginxFileMgr file.Manager, - nginxRuntimeMgr runtime.Manager, - statusUpdater status.Updater, -) *EventLoop { - return &EventLoop{ - processor: processor, - serviceStore: serviceStore, - secretStore: secretStore, - secretMemoryManager: secretMemoryManager, - generator: generator, - eventCh: eventCh, - logger: logger.WithName("eventLoop"), - nginxFileMgr: nginxFileMgr, - nginxRuntimeMgr: nginxRuntimeMgr, - statusUpdater: statusUpdater, - } +func NewEventLoop(cfg EventLoopConfig) *EventLoop { + return &EventLoop{cfg: cfg} } // Start starts the EventLoop. @@ -67,7 +60,7 @@ func (el *EventLoop) Start(ctx context.Context) error { // although we always return nil, Start must return it to satisfy // "sigs.k8s.io/controller-runtime/pkg/manager".Runnable return nil - case e := <-el.eventCh: + case e := <-el.cfg.EventCh: el.handleEvent(ctx, e) } } @@ -84,34 +77,34 @@ func (el *EventLoop) handleEvent(ctx context.Context, event interface{}) { panic(fmt.Errorf("unknown event type %T", e)) } - changed, conf, statuses := el.processor.Process() + changed, conf, statuses := el.cfg.Processor.Process() if !changed { return } err := el.updateNginx(ctx, conf) if err != nil { - el.logger.Error(err, "Failed to update NGINX configuration") + el.cfg.Logger.Error(err, "Failed to update NGINX configuration") } - el.statusUpdater.Update(ctx, statuses) + el.cfg.StatusUpdater.Update(ctx, statuses) } func (el *EventLoop) updateNginx(ctx context.Context, conf state.Configuration) error { // Write all secrets (nuke and pave). // This will remove all secrets in the secrets directory before writing the stored secrets. // FIXME(kate-osborn): We may want to rethink this approach in the future and write and remove secrets individually. - err := el.secretMemoryManager.WriteAllStoredSecrets() + err := el.cfg.SecretMemoryManager.WriteAllStoredSecrets() if err != nil { return err } - cfg, warnings := el.generator.Generate(conf) + cfg, warnings := el.cfg.Generator.Generate(conf) // For now, we keep all http servers in one config // We might rethink that. For example, we can write each server to its file // or group servers in some way. - err = el.nginxFileMgr.WriteHTTPServersConfig("http-servers", cfg) + err = el.cfg.NginxFileMgr.WriteHTTPServersConfig("http-servers", cfg) if err != nil { return err } @@ -119,7 +112,7 @@ func (el *EventLoop) updateNginx(ctx context.Context, conf state.Configuration) for obj, objWarnings := range warnings { for _, w := range objWarnings { // FIXME(pleshakov): report warnings via Object status - el.logger.Info("got warning while generating config", + el.cfg.Logger.Info("got warning while generating config", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "namespace", obj.GetNamespace(), "name", obj.GetName(), @@ -127,23 +120,23 @@ func (el *EventLoop) updateNginx(ctx context.Context, conf state.Configuration) } } - return el.nginxRuntimeMgr.Reload(ctx) + return el.cfg.NginxRuntimeMgr.Reload(ctx) } func (el *EventLoop) propagateUpsert(e *UpsertEvent) { switch r := e.Resource.(type) { case *v1alpha2.GatewayClass: - el.processor.CaptureUpsertChange(r) + el.cfg.Processor.CaptureUpsertChange(r) case *v1alpha2.Gateway: - el.processor.CaptureUpsertChange(r) + el.cfg.Processor.CaptureUpsertChange(r) case *v1alpha2.HTTPRoute: - el.processor.CaptureUpsertChange(r) + el.cfg.Processor.CaptureUpsertChange(r) case *apiv1.Service: // FIXME(pleshakov): make sure the affected hosts are updated - el.serviceStore.Upsert(r) + el.cfg.ServiceStore.Upsert(r) case *apiv1.Secret: // FIXME(kate-osborn): need to handle certificate rotation - el.secretStore.Upsert(r) + el.cfg.SecretStore.Upsert(r) default: panic(fmt.Errorf("unknown resource type %T", e.Resource)) } @@ -152,17 +145,17 @@ func (el *EventLoop) propagateUpsert(e *UpsertEvent) { func (el *EventLoop) propagateDelete(e *DeleteEvent) { switch e.Type.(type) { case *v1alpha2.GatewayClass: - el.processor.CaptureDeleteChange(e.Type, e.NamespacedName) + el.cfg.Processor.CaptureDeleteChange(e.Type, e.NamespacedName) case *v1alpha2.Gateway: - el.processor.CaptureDeleteChange(e.Type, e.NamespacedName) + el.cfg.Processor.CaptureDeleteChange(e.Type, e.NamespacedName) case *v1alpha2.HTTPRoute: - el.processor.CaptureDeleteChange(e.Type, e.NamespacedName) + el.cfg.Processor.CaptureDeleteChange(e.Type, e.NamespacedName) case *apiv1.Service: // FIXME(pleshakov): make sure the affected hosts are updated - el.serviceStore.Delete(e.NamespacedName) + el.cfg.ServiceStore.Delete(e.NamespacedName) case *apiv1.Secret: // FIXME(kate-osborn): make sure that affected servers are updated - el.secretStore.Delete(e.NamespacedName) + el.cfg.SecretStore.Delete(e.NamespacedName) default: panic(fmt.Errorf("unknown resource type %T", e.Type)) } diff --git a/internal/events/loop_test.go b/internal/events/loop_test.go index 602faea4ac..77ca532cf3 100644 --- a/internal/events/loop_test.go +++ b/internal/events/loop_test.go @@ -62,18 +62,18 @@ var _ = Describe("EventLoop", func() { fakeNginxRuntimeMgr = &runtimefakes.FakeManager{} fakeStatusUpdater = &statusfakes.FakeUpdater{} - ctrl := events.NewEventLoop( - fakeProcessor, - fakeServiceStore, - fakeSecretStore, - fakeSecretMemoryManager, - fakeGenerator, - eventCh, - zap.New(), - fakeNginxFimeMgr, - fakeNginxRuntimeMgr, - fakeStatusUpdater, - ) + ctrl := events.NewEventLoop(events.EventLoopConfig{ + Processor: fakeProcessor, + ServiceStore: fakeServiceStore, + SecretStore: fakeSecretStore, + SecretMemoryManager: fakeSecretMemoryManager, + Generator: fakeGenerator, + EventCh: eventCh, + Logger: zap.New(), + NginxFileMgr: fakeNginxFimeMgr, + NginxRuntimeMgr: fakeNginxRuntimeMgr, + StatusUpdater: fakeStatusUpdater, + }) var ctx context.Context ctx, cancel = context.WithCancel(context.Background()) diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 683677b57a..6842016612 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -102,18 +102,18 @@ func Start(cfg config.Config) error { Clock: status.NewRealClock(), }) - eventLoop := events.NewEventLoop( - processor, - serviceStore, - secretStore, - secretMemoryMgr, - configGenerator, - eventCh, - cfg.Logger, - nginxFileMgr, - nginxRuntimeMgr, - statusUpdater, - ) + eventLoop := events.NewEventLoop(events.EventLoopConfig{ + Processor: processor, + ServiceStore: serviceStore, + SecretStore: secretStore, + SecretMemoryManager: secretMemoryMgr, + Generator: configGenerator, + EventCh: eventCh, + Logger: cfg.Logger.WithName("eventLoop"), + NginxFileMgr: nginxFileMgr, + NginxRuntimeMgr: nginxRuntimeMgr, + StatusUpdater: statusUpdater, + }) err = mgr.Add(eventLoop) if err != nil { From 365abb3f509f1d9ffe40aef0104ee112e55a1ebc Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 14 Jul 2022 14:38:45 -0600 Subject: [PATCH 07/42] Const block --- internal/manager/manager.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 6842016612..140a203a08 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -25,12 +25,13 @@ import ( "github.com/nginxinc/nginx-kubernetes-gateway/pkg/sdk" ) -// clusterTimeout is a timeout for connections to the Kubernetes API -const clusterTimeout = 10 * time.Second - -// secretsFolder is the folder that holds all the secrets for NGINX servers. -// nolint:gosec -const secretsFolder = "/etc/nginx/secrets" +const ( + // clusterTimeout is a timeout for connections to the Kubernetes API + clusterTimeout = 10 * time.Second + // secretsFolder is the folder that holds all the secrets for NGINX servers. + // nolint:gosec + secretsFolder = "/etc/nginx/secrets" +) var scheme = runtime.NewScheme() From 209288dc9a4000f2f23ce41c254c4d5f5b174e2d Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 14 Jul 2022 14:42:13 -0600 Subject: [PATCH 08/42] Add listener file to state package --- internal/state/graph.go | 203 ++++--------------------------------- internal/state/listener.go | 175 ++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 186 deletions(-) create mode 100644 internal/state/listener.go diff --git a/internal/state/graph.go b/internal/state/graph.go index a5dc096d7b..156b50bd1a 100644 --- a/internal/state/graph.go +++ b/internal/state/graph.go @@ -16,22 +16,6 @@ type gateway struct { 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. - Source v1alpha2.Listener - // Valid shows whether the listener is valid. - Valid bool - // SecretPath is the path to the secret on disk. - SecretPath string - // 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{} -} - // route represents an HTTPRoute. type route struct { // Source is the source resource of the route. @@ -157,6 +141,23 @@ func buildGatewayClass(gc *v1alpha2.GatewayClass, controllerName string) *gatewa } } +func buildListeners(gw *v1alpha2.Gateway, gcName string, secretMemoryMgr SecretMemoryManager) 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. @@ -283,176 +284,6 @@ func findAcceptedHostnames(listenerHostname *v1alpha2.Hostname, routeHostnames [ return result } -func buildListeners(gw *v1alpha2.Gateway, gcName string, secretMemoryMgr SecretMemoryManager) 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 v1alpha2.Listener) *listener -} - -type listenerConfiguratorFactory struct { - https *httpsListenerConfigurator - http *httpListenerConfigurator -} - -func (f *listenerConfiguratorFactory) getConfiguratorForListener(l v1alpha2.Listener) listenerConfigurator { - switch l.Protocol { - case v1alpha2.HTTPProtocolType: - return f.http - case v1alpha2.HTTPSProtocolType: - return f.https - default: - return newInvalidProtocolListenerConfigurator() - } -} - -func newListenerConfiguratorFactory(gw *v1alpha2.Gateway, secretMemoryMgr SecretMemoryManager) *listenerConfiguratorFactory { - return &listenerConfiguratorFactory{ - https: newHTTPSListenerConfigurator(gw, secretMemoryMgr), - http: newHTTPListenerConfigurator(), - } -} - -type httpsListenerConfigurator struct { - gateway *v1alpha2.Gateway - secretMemoryMgr SecretMemoryManager - usedHostnames map[string]*listener -} - -func newHTTPSListenerConfigurator(gateway *v1alpha2.Gateway, secretMemoryMgr SecretMemoryManager) *httpsListenerConfigurator { - return &httpsListenerConfigurator{ - gateway: gateway, - secretMemoryMgr: secretMemoryMgr, - usedHostnames: make(map[string]*listener), - } -} - -func (c *httpsListenerConfigurator) configure(gl v1alpha2.Listener) *listener { - var path string - var err error - - valid := validateHTTPSListener(gl, c.gateway.Namespace) - - if valid { - nsname := types.NamespacedName{ - Namespace: c.gateway.Namespace, - Name: string(gl.TLS.CertificateRefs[0].Name), - } - - path, err = c.secretMemoryMgr.Store(nsname) - if err != nil { - valid = false - } - } - - h := getHostname(gl.Hostname) - - if holder, exist := c.usedHostnames[h]; exist { - valid = false - holder.Valid = false // all listeners for the same hostname become conflicted - } - - l := &listener{ - Source: gl, - Valid: valid, - SecretPath: path, - Routes: make(map[types.NamespacedName]*route), - AcceptedHostnames: make(map[string]struct{}), - } - - c.usedHostnames[h] = l - - return l -} - -type httpListenerConfigurator struct { - usedHostnames map[string]*listener -} - -func newHTTPListenerConfigurator() *httpListenerConfigurator { - return &httpListenerConfigurator{ - usedHostnames: make(map[string]*listener), - } -} - -func (c *httpListenerConfigurator) configure(gl v1alpha2.Listener) *listener { - valid := validateHTTPListener(gl) - - h := getHostname(gl.Hostname) - - if holder, exist := c.usedHostnames[h]; exist { - valid = false - holder.Valid = false // all listeners for the same hostname become conflicted - } - - l := &listener{ - Source: gl, - Valid: valid, - Routes: make(map[types.NamespacedName]*route), - AcceptedHostnames: make(map[string]struct{}), - } - - c.usedHostnames[h] = l - - return l -} - -type invalidProtocolListenerConfigurator struct{} - -func newInvalidProtocolListenerConfigurator() *invalidProtocolListenerConfigurator { - return &invalidProtocolListenerConfigurator{} -} - -func (c *invalidProtocolListenerConfigurator) configure(gl v1alpha2.Listener) *listener { - return &listener{ - Source: gl, - Valid: false, - Routes: make(map[types.NamespacedName]*route), - AcceptedHostnames: make(map[string]struct{}), - } -} - -func validateHTTPListener(listener v1alpha2.Listener) bool { - // FIXME(pleshakov): For now we require that all HTTP listeners bind to port 80 - return listener.Port == 80 -} - -func validateHTTPSListener(listener v1alpha2.Listener, gwNsname string) bool { - // FIXME(kate-osborn): - // 1. For now we require that all HTTPS listeners bind to port 443 - // 2. Only TLSModeTerminate is supported. - if listener.Port != 443 || listener.TLS == nil || *listener.TLS.Mode != v1alpha2.TLSModeTerminate || len(listener.TLS.CertificateRefs) <= 0 { - return false - } - - certRef := listener.TLS.CertificateRefs[0] - // certRef Kind has default of "Secret" so it's safe to directly access the Kind here - if *certRef.Kind != "Secret" { - return false - } - - // secret must be in the same namespace as the gateway - if certRef.Namespace != nil && string(*certRef.Namespace) != gwNsname { - return false - } - - return true -} - func getHostname(h *v1alpha2.Hostname) string { if h == nil { return "" diff --git a/internal/state/listener.go b/internal/state/listener.go new file mode 100644 index 0000000000..34d0b4f631 --- /dev/null +++ b/internal/state/listener.go @@ -0,0 +1,175 @@ +package state + +import ( + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +// 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. + Source v1alpha2.Listener + // Valid shows whether the listener is valid. + Valid bool + // SecretPath is the path to the secret on disk. + SecretPath string + // 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{} +} + +type listenerConfigurator interface { + configure(listener v1alpha2.Listener) *listener +} + +type listenerConfiguratorFactory struct { + https *httpsListenerConfigurator + http *httpListenerConfigurator +} + +func (f *listenerConfiguratorFactory) getConfiguratorForListener(l v1alpha2.Listener) listenerConfigurator { + switch l.Protocol { + case v1alpha2.HTTPProtocolType: + return f.http + case v1alpha2.HTTPSProtocolType: + return f.https + default: + return newInvalidProtocolListenerConfigurator() + } +} + +func newListenerConfiguratorFactory(gw *v1alpha2.Gateway, secretMemoryMgr SecretMemoryManager) *listenerConfiguratorFactory { + return &listenerConfiguratorFactory{ + https: newHTTPSListenerConfigurator(gw, secretMemoryMgr), + http: newHTTPListenerConfigurator(), + } +} + +type httpsListenerConfigurator struct { + gateway *v1alpha2.Gateway + secretMemoryMgr SecretMemoryManager + usedHostnames map[string]*listener +} + +func newHTTPSListenerConfigurator(gateway *v1alpha2.Gateway, secretMemoryMgr SecretMemoryManager) *httpsListenerConfigurator { + return &httpsListenerConfigurator{ + gateway: gateway, + secretMemoryMgr: secretMemoryMgr, + usedHostnames: make(map[string]*listener), + } +} + +func (c *httpsListenerConfigurator) configure(gl v1alpha2.Listener) *listener { + var path string + var err error + + valid := validateHTTPSListener(gl, c.gateway.Namespace) + + if valid { + nsname := types.NamespacedName{ + Namespace: c.gateway.Namespace, + Name: string(gl.TLS.CertificateRefs[0].Name), + } + + path, err = c.secretMemoryMgr.Store(nsname) + if err != nil { + valid = false + } + } + + h := getHostname(gl.Hostname) + + if holder, exist := c.usedHostnames[h]; exist { + valid = false + holder.Valid = false // all listeners for the same hostname become conflicted + } + + l := &listener{ + Source: gl, + Valid: valid, + SecretPath: path, + Routes: make(map[types.NamespacedName]*route), + AcceptedHostnames: make(map[string]struct{}), + } + + c.usedHostnames[h] = l + + return l +} + +type httpListenerConfigurator struct { + usedHostnames map[string]*listener +} + +func newHTTPListenerConfigurator() *httpListenerConfigurator { + return &httpListenerConfigurator{ + usedHostnames: make(map[string]*listener), + } +} + +func (c *httpListenerConfigurator) configure(gl v1alpha2.Listener) *listener { + valid := validateHTTPListener(gl) + + h := getHostname(gl.Hostname) + + if holder, exist := c.usedHostnames[h]; exist { + valid = false + holder.Valid = false // all listeners for the same hostname become conflicted + } + + l := &listener{ + Source: gl, + Valid: valid, + Routes: make(map[types.NamespacedName]*route), + AcceptedHostnames: make(map[string]struct{}), + } + + c.usedHostnames[h] = l + + return l +} + +type invalidProtocolListenerConfigurator struct{} + +func newInvalidProtocolListenerConfigurator() *invalidProtocolListenerConfigurator { + return &invalidProtocolListenerConfigurator{} +} + +func (c *invalidProtocolListenerConfigurator) configure(gl v1alpha2.Listener) *listener { + return &listener{ + Source: gl, + Valid: false, + Routes: make(map[types.NamespacedName]*route), + AcceptedHostnames: make(map[string]struct{}), + } +} + +func validateHTTPListener(listener v1alpha2.Listener) bool { + // FIXME(pleshakov): For now we require that all HTTP listeners bind to port 80 + return listener.Port == 80 +} + +func validateHTTPSListener(listener v1alpha2.Listener, gwNsname string) bool { + // FIXME(kate-osborn): + // 1. For now we require that all HTTPS listeners bind to port 443 + // 2. Only TLSModeTerminate is supported. + if listener.Port != 443 || listener.TLS == nil || *listener.TLS.Mode != v1alpha2.TLSModeTerminate || len(listener.TLS.CertificateRefs) <= 0 { + return false + } + + certRef := listener.TLS.CertificateRefs[0] + // certRef Kind has default of "Secret" so it's safe to directly access the Kind here + if *certRef.Kind != "Secret" { + return false + } + + // secret must be in the same namespace as the gateway + if certRef.Namespace != nil && string(*certRef.Namespace) != gwNsname { + return false + } + + return true +} From fa8592106e74f80e015bcad3f6b4b01eba7f4e01 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 14 Jul 2022 14:45:17 -0600 Subject: [PATCH 09/42] Fix secret filepath --- internal/state/graph_test.go | 2 +- internal/state/secrets.go | 2 +- internal/state/secrets_test.go | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/state/graph_test.go b/internal/state/graph_test.go index d023678d0d..f86e4ac84d 100644 --- a/internal/state/graph_test.go +++ b/internal/state/graph_test.go @@ -71,7 +71,7 @@ cpLlHMAqbLJ8WYGJCkhiWxyal6hYTyWY4cVkC0xtTl/hUE9IeNKo } var ( - secretPath = "/etc/nginx/secrets/test-secret" + secretPath = "/etc/nginx/secrets/test_secret" secretsDirectory = "/etc/nginx/secrets" ) diff --git a/internal/state/secrets.go b/internal/state/secrets.go index 3d060f21ff..265e45ba6e 100644 --- a/internal/state/secrets.go +++ b/internal/state/secrets.go @@ -174,5 +174,5 @@ func generateCertAndKeyFileContent(secret *apiv1.Secret) []byte { } func generateFilepathForSecret(nsname types.NamespacedName) string { - return nsname.Namespace + "-" + nsname.Name + return nsname.Namespace + "_" + nsname.Name } diff --git a/internal/state/secrets_test.go b/internal/state/secrets_test.go index 1727b0f43f..d454a6bfb3 100644 --- a/internal/state/secrets_test.go +++ b/internal/state/secrets_test.go @@ -174,14 +174,14 @@ var _ = Describe("SecretMemoryManager", func() { }) It("should store a valid secret", func() { fakeStore.GetReturns(&state.Secret{Secret: secret1, Valid: true}) - expectedPath := path.Join(tmpSecretsDir, "test-secret1") + expectedPath := path.Join(tmpSecretsDir, "test_secret1") testStore(secret1, expectedPath, false) }) It("should store another valid secret", func() { fakeStore.GetReturns(&state.Secret{Secret: secret2, Valid: true}) - expectedPath := path.Join(tmpSecretsDir, "test-secret2") + expectedPath := path.Join(tmpSecretsDir, "test_secret2") testStore(secret2, expectedPath, false) }) @@ -196,7 +196,7 @@ var _ = Describe("SecretMemoryManager", func() { err := memMgr.WriteAllStoredSecrets() Expect(err).ToNot(HaveOccurred()) - expectedFileNames := []string{"test-secret1", "test-secret2"} + expectedFileNames := []string{"test_secret1", "test_secret2"} // read all files from directory dir, err := ioutil.ReadDir(tmpSecretsDir) @@ -210,7 +210,7 @@ var _ = Describe("SecretMemoryManager", func() { It("should store secret after write", func() { fakeStore.GetReturns(&state.Secret{Secret: secret3, Valid: true}) - expectedPath := path.Join(tmpSecretsDir, "test-secret3") + expectedPath := path.Join(tmpSecretsDir, "test_secret3") testStore(secret3, expectedPath, false) }) @@ -225,7 +225,7 @@ var _ = Describe("SecretMemoryManager", func() { // only the secrets stored after the last write should be written to disk. Expect(dir).To(HaveLen(1)) - Expect(dir[0].Name()).To(Equal("test-secret3")) + Expect(dir[0].Name()).To(Equal("test_secret3")) }) }) }) From 8da6df2a1823564c3a4aefaad6f99ccf236c9857 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 14 Jul 2022 14:45:54 -0600 Subject: [PATCH 10/42] Remove ignored namespaces --- pkg/sdk/secret_controller.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pkg/sdk/secret_controller.go b/pkg/sdk/secret_controller.go index 107538a7dc..9f682d0893 100644 --- a/pkg/sdk/secret_controller.go +++ b/pkg/sdk/secret_controller.go @@ -10,7 +10,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -20,11 +19,6 @@ type secretReconciler struct { impl SecretImpl } -var ignoredNamespaces = map[string]struct{}{ - "kube-system": {}, - "kube-public": {}, -} - // RegisterSecretController registers the SecretController in the manager. func RegisterSecretController(mgr manager.Manager, impl SecretImpl) error { r := &secretReconciler{ @@ -35,10 +29,6 @@ func RegisterSecretController(mgr manager.Manager, impl SecretImpl) error { return ctlr.NewControllerManagedBy(mgr). For(&apiv1.Secret{}). - WithEventFilter(predicate.NewPredicateFuncs(func(obj client.Object) bool { - _, isIgnoredNS := ignoredNamespaces[obj.GetNamespace()] - return !isIgnoredNS - })). Complete(r) } From 374a6ecc29d8f93ccc695fb0e0e55a46b0d615d0 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 14 Jul 2022 14:46:33 -0600 Subject: [PATCH 11/42] Fix len comparison --- internal/state/listener.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/state/listener.go b/internal/state/listener.go index 34d0b4f631..4e42521aeb 100644 --- a/internal/state/listener.go +++ b/internal/state/listener.go @@ -156,7 +156,7 @@ func validateHTTPSListener(listener v1alpha2.Listener, gwNsname string) bool { // FIXME(kate-osborn): // 1. For now we require that all HTTPS listeners bind to port 443 // 2. Only TLSModeTerminate is supported. - if listener.Port != 443 || listener.TLS == nil || *listener.TLS.Mode != v1alpha2.TLSModeTerminate || len(listener.TLS.CertificateRefs) <= 0 { + if listener.Port != 443 || listener.TLS == nil || *listener.TLS.Mode != v1alpha2.TLSModeTerminate || len(listener.TLS.CertificateRefs) == 0 { return false } From d4bcc0abe472fcc37235981384864e3d8784babf Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 14 Jul 2022 14:49:00 -0600 Subject: [PATCH 12/42] Indent if/else/end block in template --- internal/nginx/config/template.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/nginx/config/template.go b/internal/nginx/config/template.go index 52b5fb769a..612c63d1ff 100644 --- a/internal/nginx/config/template.go +++ b/internal/nginx/config/template.go @@ -7,13 +7,13 @@ import ( ) var httpServersTemplate = `{{ range $s := .Servers }} -{{ if $s.IsDefault }} + {{ if $s.IsDefault }} server { listen 443 ssl default_server; ssl_reject_handshake on; } -{{ else }} + {{ else }} server { {{ if $s.SSL }} listen 443 ssl; @@ -42,7 +42,7 @@ server { } {{ end }} } -{{ end }} + {{ end }} {{ end }} ` From e8a5194818a94f55e5151d1be30964e56db39bf7 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 14 Jul 2022 14:50:34 -0600 Subject: [PATCH 13/42] rules -> rulesPerHost --- internal/state/configuration.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/state/configuration.go b/internal/state/configuration.go index 866badfd2c..c35ba5f560 100644 --- a/internal/state/configuration.go +++ b/internal/state/configuration.go @@ -109,13 +109,13 @@ func (sb *configBuilder) build() Configuration { } type httpServerBuilder struct { - rules map[string]map[string]PathRule + rulesPerHost map[string]map[string]PathRule listenersForHost map[string]*listener } func newHTTPServerBuilder() *httpServerBuilder { return &httpServerBuilder{ - rules: make(map[string]map[string]PathRule), + rulesPerHost: make(map[string]map[string]PathRule), listenersForHost: make(map[string]*listener), } } @@ -133,8 +133,8 @@ func (p *httpServerBuilder) upsertListener(l *listener) { for _, h := range hostnames { p.listenersForHost[h] = l - if _, exist := p.rules[h]; !exist { - p.rules[h] = make(map[string]PathRule) + if _, exist := p.rulesPerHost[h]; !exist { + p.rulesPerHost[h] = make(map[string]PathRule) } } @@ -143,7 +143,7 @@ func (p *httpServerBuilder) upsertListener(l *listener) { for j, m := range rule.Matches { path := getPath(m.Path) - rule, exist := p.rules[h][path] + rule, exist := p.rulesPerHost[h][path] if !exist { rule.Path = path } @@ -154,7 +154,7 @@ func (p *httpServerBuilder) upsertListener(l *listener) { Source: r.Source, }) - p.rules[h][path] = rule + p.rulesPerHost[h][path] = rule } } } @@ -163,9 +163,9 @@ func (p *httpServerBuilder) upsertListener(l *listener) { func (p *httpServerBuilder) build() []HTTPServer { - servers := make([]HTTPServer, 0, len(p.rules)) + servers := make([]HTTPServer, 0, len(p.rulesPerHost)) - for h, rules := range p.rules { + for h, rules := range p.rulesPerHost { s := HTTPServer{ Hostname: h, PathRules: make([]PathRule, 0, len(rules)), From 643129b5c0e35d04fbd48924b67100d754086c52 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 14 Jul 2022 14:51:04 -0600 Subject: [PATCH 14/42] secretCache -> secretStore --- internal/state/secrets.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/state/secrets.go b/internal/state/secrets.go index 265e45ba6e..c0203cd5d8 100644 --- a/internal/state/secrets.go +++ b/internal/state/secrets.go @@ -74,7 +74,7 @@ type SecretMemoryManager interface { type SecretDiskMemoryManager struct { storedSecrets map[types.NamespacedName]storedSecret - secretCache SecretStore + secretStore SecretStore secretDirectory string } @@ -86,13 +86,13 @@ type storedSecret struct { func NewSecretDiskMemoryManager(secretDirectory string, secretStore SecretStore) *SecretDiskMemoryManager { return &SecretDiskMemoryManager{ storedSecrets: make(map[types.NamespacedName]storedSecret), - secretCache: secretStore, + secretStore: secretStore, secretDirectory: secretDirectory, } } func (s *SecretDiskMemoryManager) Store(nsname types.NamespacedName) (string, error) { - secret := s.secretCache.Get(nsname) + secret := s.secretStore.Get(nsname) if secret == nil { return "", fmt.Errorf("secret %s does not exist", nsname) } From 1f1bdbf4bac5e7c269af777630c37369b7199b52 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 14 Jul 2022 14:56:54 -0600 Subject: [PATCH 15/42] SecretMemoryManager -> SecretDiskMemoryManager --- internal/events/loop.go | 2 +- internal/events/loop_test.go | 4 +-- internal/state/change_processor.go | 2 +- internal/state/change_processor_test.go | 8 ++--- internal/state/graph.go | 4 +-- internal/state/listener.go | 6 ++-- internal/state/secrets.go | 14 ++++----- internal/state/secrets_test.go | 4 +-- ....go => fake_secret_disk_memory_manager.go} | 30 +++++++++---------- 9 files changed, 37 insertions(+), 37 deletions(-) rename internal/state/statefakes/{fake_secret_memory_manager.go => fake_secret_disk_memory_manager.go} (76%) diff --git a/internal/events/loop.go b/internal/events/loop.go index 164e6e9d26..17ac1acbc6 100644 --- a/internal/events/loop.go +++ b/internal/events/loop.go @@ -24,7 +24,7 @@ type EventLoopConfig struct { // SecretStore is the state SecretStore. SecretStore state.SecretStore // SecretMemoryManager is the state SecretMemoryManager. - SecretMemoryManager state.SecretMemoryManager + SecretMemoryManager state.SecretDiskMemoryManager // Generator is the nginx config Generator. Generator config.Generator // EventCh is a read-only channel for events. diff --git a/internal/events/loop_test.go b/internal/events/loop_test.go index 77ca532cf3..120c07b20f 100644 --- a/internal/events/loop_test.go +++ b/internal/events/loop_test.go @@ -40,7 +40,7 @@ var _ = Describe("EventLoop", func() { fakeProcessor *statefakes.FakeChangeProcessor fakeServiceStore *statefakes.FakeServiceStore fakeSecretStore *statefakes.FakeSecretStore - fakeSecretMemoryManager *statefakes.FakeSecretMemoryManager + fakeSecretMemoryManager *statefakes.FakeSecretDiskMemoryManager fakeGenerator *configfakes.FakeGenerator fakeNginxFimeMgr *filefakes.FakeManager fakeNginxRuntimeMgr *runtimefakes.FakeManager @@ -55,7 +55,7 @@ var _ = Describe("EventLoop", func() { fakeProcessor = &statefakes.FakeChangeProcessor{} eventCh = make(chan interface{}) fakeServiceStore = &statefakes.FakeServiceStore{} - fakeSecretMemoryManager = &statefakes.FakeSecretMemoryManager{} + fakeSecretMemoryManager = &statefakes.FakeSecretDiskMemoryManager{} fakeSecretStore = &statefakes.FakeSecretStore{} fakeGenerator = &configfakes.FakeGenerator{} fakeNginxFimeMgr = &filefakes.FakeManager{} diff --git a/internal/state/change_processor.go b/internal/state/change_processor.go index 29c3b4a8e9..150647ece6 100644 --- a/internal/state/change_processor.go +++ b/internal/state/change_processor.go @@ -38,7 +38,7 @@ type ChangeProcessorConfig struct { // GatewayClassName is the name of the GatewayClass resource. GatewayClassName string // SecretMemoryManager is the secret memory manager. - SecretMemoryManager SecretMemoryManager + SecretMemoryManager SecretDiskMemoryManager } type ChangeProcessorImpl struct { diff --git a/internal/state/change_processor_test.go b/internal/state/change_processor_test.go index cb091463d1..9a3d9b0245 100644 --- a/internal/state/change_processor_test.go +++ b/internal/state/change_processor_test.go @@ -26,7 +26,7 @@ var _ = Describe("ChangeProcessor", func() { hr1, hr1Updated, hr2 *v1alpha2.HTTPRoute gw1, gw1Updated, gw2 *v1alpha2.Gateway processor state.ChangeProcessor - fakeSecretMemoryMgr *statefakes.FakeSecretMemoryManager + fakeSecretMemoryMgr *statefakes.FakeSecretDiskMemoryManager ) BeforeEach(OncePerOrdered, func() { @@ -133,7 +133,7 @@ var _ = Describe("ChangeProcessor", func() { gw2 = createGateway("gateway-2") - fakeSecretMemoryMgr = &statefakes.FakeSecretMemoryManager{} + fakeSecretMemoryMgr = &statefakes.FakeSecretDiskMemoryManager{} processor = state.NewChangeProcessorImpl(state.ChangeProcessorConfig{ GatewayCtlrName: controllerName, @@ -887,10 +887,10 @@ var _ = Describe("ChangeProcessor", func() { Describe("Edge cases with panic", func() { var processor state.ChangeProcessor - var fakeSecretMemoryMgr *statefakes.FakeSecretMemoryManager + var fakeSecretMemoryMgr *statefakes.FakeSecretDiskMemoryManager BeforeEach(func() { - fakeSecretMemoryMgr = &statefakes.FakeSecretMemoryManager{} + fakeSecretMemoryMgr = &statefakes.FakeSecretDiskMemoryManager{} processor = state.NewChangeProcessorImpl(state.ChangeProcessorConfig{ GatewayCtlrName: "test.controller", diff --git a/internal/state/graph.go b/internal/state/graph.go index 156b50bd1a..80525cc53f 100644 --- a/internal/state/graph.go +++ b/internal/state/graph.go @@ -59,7 +59,7 @@ func buildGraph( store *store, controllerName string, gcName string, - secretMemoryMgr SecretMemoryManager, + secretMemoryMgr SecretDiskMemoryManager, ) *graph { gc := buildGatewayClass(store.gc, controllerName) @@ -141,7 +141,7 @@ func buildGatewayClass(gc *v1alpha2.GatewayClass, controllerName string) *gatewa } } -func buildListeners(gw *v1alpha2.Gateway, gcName string, secretMemoryMgr SecretMemoryManager) map[string]*listener { +func buildListeners(gw *v1alpha2.Gateway, gcName string, secretMemoryMgr SecretDiskMemoryManager) map[string]*listener { listeners := make(map[string]*listener) if gw == nil || string(gw.Spec.GatewayClassName) != gcName { diff --git a/internal/state/listener.go b/internal/state/listener.go index 4e42521aeb..c6de1bff6e 100644 --- a/internal/state/listener.go +++ b/internal/state/listener.go @@ -41,7 +41,7 @@ func (f *listenerConfiguratorFactory) getConfiguratorForListener(l v1alpha2.List } } -func newListenerConfiguratorFactory(gw *v1alpha2.Gateway, secretMemoryMgr SecretMemoryManager) *listenerConfiguratorFactory { +func newListenerConfiguratorFactory(gw *v1alpha2.Gateway, secretMemoryMgr SecretDiskMemoryManager) *listenerConfiguratorFactory { return &listenerConfiguratorFactory{ https: newHTTPSListenerConfigurator(gw, secretMemoryMgr), http: newHTTPListenerConfigurator(), @@ -50,11 +50,11 @@ func newListenerConfiguratorFactory(gw *v1alpha2.Gateway, secretMemoryMgr Secret type httpsListenerConfigurator struct { gateway *v1alpha2.Gateway - secretMemoryMgr SecretMemoryManager + secretMemoryMgr SecretDiskMemoryManager usedHostnames map[string]*listener } -func newHTTPSListenerConfigurator(gateway *v1alpha2.Gateway, secretMemoryMgr SecretMemoryManager) *httpsListenerConfigurator { +func newHTTPSListenerConfigurator(gateway *v1alpha2.Gateway, secretMemoryMgr SecretDiskMemoryManager) *httpsListenerConfigurator { return &httpsListenerConfigurator{ gateway: gateway, secretMemoryMgr: secretMemoryMgr, diff --git a/internal/state/secrets.go b/internal/state/secrets.go index c0203cd5d8..884ba4a9fd 100644 --- a/internal/state/secrets.go +++ b/internal/state/secrets.go @@ -13,7 +13,7 @@ import ( ) //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . SecretStore -//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . SecretMemoryManager +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . SecretDiskMemoryManager // tlsSecretFileMode defines the default file mode for files with TLS Secrets. const tlsSecretFileMode = 0o600 @@ -64,7 +64,7 @@ func (s SecretStoreImpl) Get(nsname types.NamespacedName) *Secret { return s.secrets[nsname] } -type SecretMemoryManager interface { +type SecretDiskMemoryManager interface { // Store stores the secret in memory so that it can be written to disk before reloading NGINX. // Returns the path to the secret and an error if the secret does not exist in the cache or the secret is invalid. Store(nsname types.NamespacedName) (string, error) @@ -72,7 +72,7 @@ type SecretMemoryManager interface { WriteAllStoredSecrets() error } -type SecretDiskMemoryManager struct { +type SecretDiskMemoryManagerImpl struct { storedSecrets map[types.NamespacedName]storedSecret secretStore SecretStore secretDirectory string @@ -83,15 +83,15 @@ type storedSecret struct { path string } -func NewSecretDiskMemoryManager(secretDirectory string, secretStore SecretStore) *SecretDiskMemoryManager { - return &SecretDiskMemoryManager{ +func NewSecretDiskMemoryManager(secretDirectory string, secretStore SecretStore) *SecretDiskMemoryManagerImpl { + return &SecretDiskMemoryManagerImpl{ storedSecrets: make(map[types.NamespacedName]storedSecret), secretStore: secretStore, secretDirectory: secretDirectory, } } -func (s *SecretDiskMemoryManager) Store(nsname types.NamespacedName) (string, error) { +func (s *SecretDiskMemoryManagerImpl) Store(nsname types.NamespacedName) (string, error) { secret := s.secretStore.Get(nsname) if secret == nil { return "", fmt.Errorf("secret %s does not exist", nsname) @@ -111,7 +111,7 @@ func (s *SecretDiskMemoryManager) Store(nsname types.NamespacedName) (string, er return ss.path, nil } -func (s *SecretDiskMemoryManager) WriteAllStoredSecrets() error { +func (s *SecretDiskMemoryManagerImpl) WriteAllStoredSecrets() error { // Remove all existing secrets from secrets directory dir, err := ioutil.ReadDir(s.secretDirectory) if err != nil { diff --git a/internal/state/secrets_test.go b/internal/state/secrets_test.go index d454a6bfb3..8a82ba92c3 100644 --- a/internal/state/secrets_test.go +++ b/internal/state/secrets_test.go @@ -133,10 +133,10 @@ var ( } ) -var _ = Describe("SecretMemoryManager", func() { +var _ = Describe("SecretDiskMemoryManager", func() { var ( fakeStore *statefakes.FakeSecretStore - memMgr state.SecretMemoryManager + memMgr state.SecretDiskMemoryManager tmpSecretsDir string ) diff --git a/internal/state/statefakes/fake_secret_memory_manager.go b/internal/state/statefakes/fake_secret_disk_memory_manager.go similarity index 76% rename from internal/state/statefakes/fake_secret_memory_manager.go rename to internal/state/statefakes/fake_secret_disk_memory_manager.go index 76c7484d56..e10b46fb1e 100644 --- a/internal/state/statefakes/fake_secret_memory_manager.go +++ b/internal/state/statefakes/fake_secret_disk_memory_manager.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/types" ) -type FakeSecretMemoryManager struct { +type FakeSecretDiskMemoryManager struct { StoreStub func(types.NamespacedName) (string, error) storeMutex sync.RWMutex storeArgsForCall []struct { @@ -36,7 +36,7 @@ type FakeSecretMemoryManager struct { invocationsMutex sync.RWMutex } -func (fake *FakeSecretMemoryManager) Store(arg1 types.NamespacedName) (string, error) { +func (fake *FakeSecretDiskMemoryManager) Store(arg1 types.NamespacedName) (string, error) { fake.storeMutex.Lock() ret, specificReturn := fake.storeReturnsOnCall[len(fake.storeArgsForCall)] fake.storeArgsForCall = append(fake.storeArgsForCall, struct { @@ -55,26 +55,26 @@ func (fake *FakeSecretMemoryManager) Store(arg1 types.NamespacedName) (string, e return fakeReturns.result1, fakeReturns.result2 } -func (fake *FakeSecretMemoryManager) StoreCallCount() int { +func (fake *FakeSecretDiskMemoryManager) StoreCallCount() int { fake.storeMutex.RLock() defer fake.storeMutex.RUnlock() return len(fake.storeArgsForCall) } -func (fake *FakeSecretMemoryManager) StoreCalls(stub func(types.NamespacedName) (string, error)) { +func (fake *FakeSecretDiskMemoryManager) StoreCalls(stub func(types.NamespacedName) (string, error)) { fake.storeMutex.Lock() defer fake.storeMutex.Unlock() fake.StoreStub = stub } -func (fake *FakeSecretMemoryManager) StoreArgsForCall(i int) types.NamespacedName { +func (fake *FakeSecretDiskMemoryManager) StoreArgsForCall(i int) types.NamespacedName { fake.storeMutex.RLock() defer fake.storeMutex.RUnlock() argsForCall := fake.storeArgsForCall[i] return argsForCall.arg1 } -func (fake *FakeSecretMemoryManager) StoreReturns(result1 string, result2 error) { +func (fake *FakeSecretDiskMemoryManager) StoreReturns(result1 string, result2 error) { fake.storeMutex.Lock() defer fake.storeMutex.Unlock() fake.StoreStub = nil @@ -84,7 +84,7 @@ func (fake *FakeSecretMemoryManager) StoreReturns(result1 string, result2 error) }{result1, result2} } -func (fake *FakeSecretMemoryManager) StoreReturnsOnCall(i int, result1 string, result2 error) { +func (fake *FakeSecretDiskMemoryManager) StoreReturnsOnCall(i int, result1 string, result2 error) { fake.storeMutex.Lock() defer fake.storeMutex.Unlock() fake.StoreStub = nil @@ -100,7 +100,7 @@ func (fake *FakeSecretMemoryManager) StoreReturnsOnCall(i int, result1 string, r }{result1, result2} } -func (fake *FakeSecretMemoryManager) WriteAllStoredSecrets() error { +func (fake *FakeSecretDiskMemoryManager) WriteAllStoredSecrets() error { fake.writeAllStoredSecretsMutex.Lock() ret, specificReturn := fake.writeAllStoredSecretsReturnsOnCall[len(fake.writeAllStoredSecretsArgsForCall)] fake.writeAllStoredSecretsArgsForCall = append(fake.writeAllStoredSecretsArgsForCall, struct { @@ -118,19 +118,19 @@ func (fake *FakeSecretMemoryManager) WriteAllStoredSecrets() error { return fakeReturns.result1 } -func (fake *FakeSecretMemoryManager) WriteAllStoredSecretsCallCount() int { +func (fake *FakeSecretDiskMemoryManager) WriteAllStoredSecretsCallCount() int { fake.writeAllStoredSecretsMutex.RLock() defer fake.writeAllStoredSecretsMutex.RUnlock() return len(fake.writeAllStoredSecretsArgsForCall) } -func (fake *FakeSecretMemoryManager) WriteAllStoredSecretsCalls(stub func() error) { +func (fake *FakeSecretDiskMemoryManager) WriteAllStoredSecretsCalls(stub func() error) { fake.writeAllStoredSecretsMutex.Lock() defer fake.writeAllStoredSecretsMutex.Unlock() fake.WriteAllStoredSecretsStub = stub } -func (fake *FakeSecretMemoryManager) WriteAllStoredSecretsReturns(result1 error) { +func (fake *FakeSecretDiskMemoryManager) WriteAllStoredSecretsReturns(result1 error) { fake.writeAllStoredSecretsMutex.Lock() defer fake.writeAllStoredSecretsMutex.Unlock() fake.WriteAllStoredSecretsStub = nil @@ -139,7 +139,7 @@ func (fake *FakeSecretMemoryManager) WriteAllStoredSecretsReturns(result1 error) }{result1} } -func (fake *FakeSecretMemoryManager) WriteAllStoredSecretsReturnsOnCall(i int, result1 error) { +func (fake *FakeSecretDiskMemoryManager) WriteAllStoredSecretsReturnsOnCall(i int, result1 error) { fake.writeAllStoredSecretsMutex.Lock() defer fake.writeAllStoredSecretsMutex.Unlock() fake.WriteAllStoredSecretsStub = nil @@ -153,7 +153,7 @@ func (fake *FakeSecretMemoryManager) WriteAllStoredSecretsReturnsOnCall(i int, r }{result1} } -func (fake *FakeSecretMemoryManager) Invocations() map[string][][]interface{} { +func (fake *FakeSecretDiskMemoryManager) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() fake.storeMutex.RLock() @@ -167,7 +167,7 @@ func (fake *FakeSecretMemoryManager) Invocations() map[string][][]interface{} { return copiedInvocations } -func (fake *FakeSecretMemoryManager) recordInvocation(key string, args []interface{}) { +func (fake *FakeSecretDiskMemoryManager) recordInvocation(key string, args []interface{}) { fake.invocationsMutex.Lock() defer fake.invocationsMutex.Unlock() if fake.invocations == nil { @@ -179,4 +179,4 @@ func (fake *FakeSecretMemoryManager) recordInvocation(key string, args []interfa fake.invocations[key] = append(fake.invocations[key], args) } -var _ state.SecretMemoryManager = new(FakeSecretMemoryManager) +var _ state.SecretDiskMemoryManager = new(FakeSecretDiskMemoryManager) From ed16702d063720879807c52217dfa3f770661046 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 14 Jul 2022 15:43:18 -0600 Subject: [PATCH 16/42] store -> request --- internal/state/change_processor_test.go | 2 +- internal/state/listener.go | 2 +- internal/state/secrets.go | 32 ++++---- internal/state/secrets_test.go | 2 +- .../fake_secret_disk_memory_manager.go | 78 +++++++++---------- 5 files changed, 58 insertions(+), 58 deletions(-) diff --git a/internal/state/change_processor_test.go b/internal/state/change_processor_test.go index 9a3d9b0245..baf9f2d75c 100644 --- a/internal/state/change_processor_test.go +++ b/internal/state/change_processor_test.go @@ -141,7 +141,7 @@ var _ = Describe("ChangeProcessor", func() { SecretMemoryManager: fakeSecretMemoryMgr, }) - fakeSecretMemoryMgr.StoreReturns(certificatePath, nil) + fakeSecretMemoryMgr.RequestReturns(certificatePath, nil) }) Describe("Process resources", Ordered, func() { diff --git a/internal/state/listener.go b/internal/state/listener.go index c6de1bff6e..38a6501921 100644 --- a/internal/state/listener.go +++ b/internal/state/listener.go @@ -74,7 +74,7 @@ func (c *httpsListenerConfigurator) configure(gl v1alpha2.Listener) *listener { Name: string(gl.TLS.CertificateRefs[0].Name), } - path, err = c.secretMemoryMgr.Store(nsname) + path, err = c.secretMemoryMgr.Request(nsname) if err != nil { valid = false } diff --git a/internal/state/secrets.go b/internal/state/secrets.go index 884ba4a9fd..adf1ca215b 100644 --- a/internal/state/secrets.go +++ b/internal/state/secrets.go @@ -65,33 +65,33 @@ func (s SecretStoreImpl) Get(nsname types.NamespacedName) *Secret { } type SecretDiskMemoryManager interface { - // Store stores the secret in memory so that it can be written to disk before reloading NGINX. - // Returns the path to the secret and an error if the secret does not exist in the cache or the secret is invalid. - Store(nsname types.NamespacedName) (string, error) - // WriteAllStoredSecrets writes all stored secrets to disk. + // Request marks the secret as requested so that it can be written to disk before reloading NGINX. + // Returns the path to the secret and an error if the secret does not exist in the secret store or the secret is invalid. + Request(nsname types.NamespacedName) (string, error) + // WriteAllStoredSecrets writes all requested secrets to disk. WriteAllStoredSecrets() error } type SecretDiskMemoryManagerImpl struct { - storedSecrets map[types.NamespacedName]storedSecret - secretStore SecretStore - secretDirectory string + requestedSecrets map[types.NamespacedName]requestedSecret + secretStore SecretStore + secretDirectory string } -type storedSecret struct { +type requestedSecret struct { secret *apiv1.Secret path string } func NewSecretDiskMemoryManager(secretDirectory string, secretStore SecretStore) *SecretDiskMemoryManagerImpl { return &SecretDiskMemoryManagerImpl{ - storedSecrets: make(map[types.NamespacedName]storedSecret), - secretStore: secretStore, - secretDirectory: secretDirectory, + requestedSecrets: make(map[types.NamespacedName]requestedSecret), + secretStore: secretStore, + secretDirectory: secretDirectory, } } -func (s *SecretDiskMemoryManagerImpl) Store(nsname types.NamespacedName) (string, error) { +func (s *SecretDiskMemoryManagerImpl) Request(nsname types.NamespacedName) (string, error) { secret := s.secretStore.Get(nsname) if secret == nil { return "", fmt.Errorf("secret %s does not exist", nsname) @@ -101,12 +101,12 @@ func (s *SecretDiskMemoryManagerImpl) Store(nsname types.NamespacedName) (string return "", fmt.Errorf("secret %s is not valid; must be of type %s and contain a valid X509 key pair", nsname, apiv1.SecretTypeTLS) } - ss := storedSecret{ + ss := requestedSecret{ secret: secret.Secret, path: path.Join(s.secretDirectory, generateFilepathForSecret(nsname)), } - s.storedSecrets[nsname] = ss + s.requestedSecrets[nsname] = ss return ss.path, nil } @@ -126,7 +126,7 @@ func (s *SecretDiskMemoryManagerImpl) WriteAllStoredSecrets() error { } // Write all secrets to secrets directory - for nsname, ss := range s.storedSecrets { + for nsname, ss := range s.requestedSecrets { file, err := os.Create(ss.path) if err != nil { @@ -147,7 +147,7 @@ func (s *SecretDiskMemoryManagerImpl) WriteAllStoredSecrets() error { } // reset stored secrets - s.storedSecrets = make(map[types.NamespacedName]storedSecret) + s.requestedSecrets = make(map[types.NamespacedName]requestedSecret) return nil } diff --git a/internal/state/secrets_test.go b/internal/state/secrets_test.go index 8a82ba92c3..35e062ca78 100644 --- a/internal/state/secrets_test.go +++ b/internal/state/secrets_test.go @@ -156,7 +156,7 @@ var _ = Describe("SecretDiskMemoryManager", func() { Describe("Manages secrets on disk", Ordered, func() { testStore := func(s *apiv1.Secret, expPath string, expErr bool) { nsname := types.NamespacedName{Namespace: s.Namespace, Name: s.Name} - actualPath, err := memMgr.Store(nsname) + actualPath, err := memMgr.Request(nsname) if expErr { Expect(err).To(HaveOccurred()) diff --git a/internal/state/statefakes/fake_secret_disk_memory_manager.go b/internal/state/statefakes/fake_secret_disk_memory_manager.go index e10b46fb1e..d97425ba36 100644 --- a/internal/state/statefakes/fake_secret_disk_memory_manager.go +++ b/internal/state/statefakes/fake_secret_disk_memory_manager.go @@ -9,16 +9,16 @@ import ( ) type FakeSecretDiskMemoryManager struct { - StoreStub func(types.NamespacedName) (string, error) - storeMutex sync.RWMutex - storeArgsForCall []struct { + RequestStub func(types.NamespacedName) (string, error) + requestMutex sync.RWMutex + requestArgsForCall []struct { arg1 types.NamespacedName } - storeReturns struct { + requestReturns struct { result1 string result2 error } - storeReturnsOnCall map[int]struct { + requestReturnsOnCall map[int]struct { result1 string result2 error } @@ -36,16 +36,16 @@ type FakeSecretDiskMemoryManager struct { invocationsMutex sync.RWMutex } -func (fake *FakeSecretDiskMemoryManager) Store(arg1 types.NamespacedName) (string, error) { - fake.storeMutex.Lock() - ret, specificReturn := fake.storeReturnsOnCall[len(fake.storeArgsForCall)] - fake.storeArgsForCall = append(fake.storeArgsForCall, struct { +func (fake *FakeSecretDiskMemoryManager) Request(arg1 types.NamespacedName) (string, error) { + fake.requestMutex.Lock() + ret, specificReturn := fake.requestReturnsOnCall[len(fake.requestArgsForCall)] + fake.requestArgsForCall = append(fake.requestArgsForCall, struct { arg1 types.NamespacedName }{arg1}) - stub := fake.StoreStub - fakeReturns := fake.storeReturns - fake.recordInvocation("Store", []interface{}{arg1}) - fake.storeMutex.Unlock() + stub := fake.RequestStub + fakeReturns := fake.requestReturns + fake.recordInvocation("Request", []interface{}{arg1}) + fake.requestMutex.Unlock() if stub != nil { return stub(arg1) } @@ -55,46 +55,46 @@ func (fake *FakeSecretDiskMemoryManager) Store(arg1 types.NamespacedName) (strin return fakeReturns.result1, fakeReturns.result2 } -func (fake *FakeSecretDiskMemoryManager) StoreCallCount() int { - fake.storeMutex.RLock() - defer fake.storeMutex.RUnlock() - return len(fake.storeArgsForCall) +func (fake *FakeSecretDiskMemoryManager) RequestCallCount() int { + fake.requestMutex.RLock() + defer fake.requestMutex.RUnlock() + return len(fake.requestArgsForCall) } -func (fake *FakeSecretDiskMemoryManager) StoreCalls(stub func(types.NamespacedName) (string, error)) { - fake.storeMutex.Lock() - defer fake.storeMutex.Unlock() - fake.StoreStub = stub +func (fake *FakeSecretDiskMemoryManager) RequestCalls(stub func(types.NamespacedName) (string, error)) { + fake.requestMutex.Lock() + defer fake.requestMutex.Unlock() + fake.RequestStub = stub } -func (fake *FakeSecretDiskMemoryManager) StoreArgsForCall(i int) types.NamespacedName { - fake.storeMutex.RLock() - defer fake.storeMutex.RUnlock() - argsForCall := fake.storeArgsForCall[i] +func (fake *FakeSecretDiskMemoryManager) RequestArgsForCall(i int) types.NamespacedName { + fake.requestMutex.RLock() + defer fake.requestMutex.RUnlock() + argsForCall := fake.requestArgsForCall[i] return argsForCall.arg1 } -func (fake *FakeSecretDiskMemoryManager) StoreReturns(result1 string, result2 error) { - fake.storeMutex.Lock() - defer fake.storeMutex.Unlock() - fake.StoreStub = nil - fake.storeReturns = struct { +func (fake *FakeSecretDiskMemoryManager) RequestReturns(result1 string, result2 error) { + fake.requestMutex.Lock() + defer fake.requestMutex.Unlock() + fake.RequestStub = nil + fake.requestReturns = struct { result1 string result2 error }{result1, result2} } -func (fake *FakeSecretDiskMemoryManager) StoreReturnsOnCall(i int, result1 string, result2 error) { - fake.storeMutex.Lock() - defer fake.storeMutex.Unlock() - fake.StoreStub = nil - if fake.storeReturnsOnCall == nil { - fake.storeReturnsOnCall = make(map[int]struct { +func (fake *FakeSecretDiskMemoryManager) RequestReturnsOnCall(i int, result1 string, result2 error) { + fake.requestMutex.Lock() + defer fake.requestMutex.Unlock() + fake.RequestStub = nil + if fake.requestReturnsOnCall == nil { + fake.requestReturnsOnCall = make(map[int]struct { result1 string result2 error }) } - fake.storeReturnsOnCall[i] = struct { + fake.requestReturnsOnCall[i] = struct { result1 string result2 error }{result1, result2} @@ -156,8 +156,8 @@ func (fake *FakeSecretDiskMemoryManager) WriteAllStoredSecretsReturnsOnCall(i in func (fake *FakeSecretDiskMemoryManager) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() - fake.storeMutex.RLock() - defer fake.storeMutex.RUnlock() + fake.requestMutex.RLock() + defer fake.requestMutex.RUnlock() fake.writeAllStoredSecretsMutex.RLock() defer fake.writeAllStoredSecretsMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} From 5a9c672b01959b421429271e5475c44d681942de Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 14 Jul 2022 16:41:37 -0600 Subject: [PATCH 17/42] Do not use default server secret --- README.md | 6 --- deploy/manifests/default-server-secret.yaml | 9 ---- examples/https-termination/README.md | 60 ++++++++------------- examples/https-termination/cafe-secret.yaml | 9 ++++ examples/https-termination/gateway.yaml | 19 +++++++ 5 files changed, 51 insertions(+), 52 deletions(-) delete mode 100644 deploy/manifests/default-server-secret.yaml create mode 100644 examples/https-termination/cafe-secret.yaml create mode 100644 examples/https-termination/gateway.yaml diff --git a/README.md b/README.md index 27a7b0fe97..c32e2a2c8c 100644 --- a/README.md +++ b/README.md @@ -99,12 +99,6 @@ You can deploy NGINX Kubernetes Gateway on an existing Kubernetes 1.16+ cluster. kubectl apply -f deploy/manifests/gateway.yaml ``` -1. Create the default server secret: - - ``` - kubectl apply -f deploy/manifests/default-server-secret.yaml - ``` - ## Expose NGINX Kubernetes Gateway You can gain access to NGINX Kubernetes Gateway by creating a `NodePort` Service or a `LoadBalancer` Service. diff --git a/deploy/manifests/default-server-secret.yaml b/deploy/manifests/default-server-secret.yaml deleted file mode 100644 index c3a52bb017..0000000000 --- a/deploy/manifests/default-server-secret.yaml +++ /dev/null @@ -1,9 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: default-server-secret - namespace: nginx-gateway -type: kubernetes.io/tls -data: - tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUN2akNDQWFZQ0NRRGtpUjZ0djkyazhEQU5CZ2txaGtpRzl3MEJBUXNGQURBaE1SOHdIUVlEVlFRRERCWk8KUjBsT1dFdDFZbVZ5Ym1WMFpYTkhZWFJsZDJGNU1CNFhEVEl5TURjeE1USXlNek16TlZvWERUSXpNRGN4TVRJeQpNek16TlZvd0lURWZNQjBHQTFVRUF3d1dUa2RKVGxoTGRXSmxjbTVsZEdWelIyRjBaWGRoZVRDQ0FTSXdEUVlKCktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUs3c3ZHRFFRQ3JnSUJYUU5UWVQxdzJ2QnFQVGFJY04KRTRkbzJXQjZkSWxETjBOV2RUMGNrT0c1REhEdDFXOFEyeWlQdW0rVG9pbWplZXU0L2tURDlqOFhINlEybm9vLwpFMnY5N1JJc2I2UW5wVUIzSXo4Mjd2SzN6c3ViTERrcUI5WEszT2dYYTRacHFwNUF3Uy9EK21TQ0h1RXZmcGY5CkNNbDNxdlNKb0hEbkZPY3M0YmFQeEZVYmRqeHluYVBHR1Ftd1QvaGgzZ0t2YzJJeXZiSkV3cHdpeGIySS9DckwKc2JZRUFMcWdpMURmNWN3aVArVnlmZ1JRbVFHMWxlMnFxTzNmaWR1SjVxc3pKWDJkbU44b1ZrQUVWSmJuT0dScwpncmpaVVVaSThZelFNZ05HSlhkMmVFZXhtbzZtZDBlZlVSRXkrUGpyZXprYWFvczA3R1h5d3g4Q0F3RUFBVEFOCkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQWV2MDVQb2xZdm9uUFhaUGcvRVpndjllSThHdWdzSEJZZWE0N3ZPZ2UKRU5DN2xiM3h0RDNrTkx1UWdlYkVieVV1cks2cFpJZ2laaXpCU2hDayt6Z1dGbEppU0oreVR2TEthRitLR2NTQwpRcW9pcVZZems5UFRzb1JPUVR3R3hGWkFwd3hkUTRKSThya0YyS0VmRHF0aWNESktYTVQrYUttZ2owTUR1ckxSCnYvTHRVbWZ0UjVSajVyeWEydHN3eE5mN2tzMTdJbGJXN2FSeHN5UExYdkJkNmd3c1B0Y1VNa0xkWEY4TElkdlYKRDNyRkRneTdlUWlXb3FlUTJsZnFQVjVId2t0M21NMWFWM0YrSmsvdXFyNi9tSmcvOE1rWlNBTHlQWmUyMVpFQgo3WnpkQnVVcHNaVUJMUytMaENGVHJyMTh2SGJNamhhSDFtQWFpNmxuQWlIaE13PT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= - tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktZd2dnU2lBZ0VBQW9JQkFRQ3U3THhnMEVBcTRDQVYKMERVMkU5Y05yd2FqMDJpSERST0hhTmxnZW5TSlF6ZERWblU5SEpEaHVReHc3ZFZ2RU5zb2o3cHZrNklwbzNucgp1UDVFdy9ZL0Z4K2tOcDZLUHhOci9lMFNMRytrSjZWQWR5TS9OdTd5dDg3TG15dzVLZ2ZWeXR6b0YydUdhYXFlClFNRXZ3L3BrZ2g3aEwzNlgvUWpKZDZyMGlhQnc1eFRuTE9HMmo4UlZHM1k4Y3Ayanhoa0pzRS80WWQ0Q3IzTmkKTXIyeVJNS2NJc1c5aVB3cXk3RzJCQUM2b0l0UTMrWE1Jai9sY240RVVKa0J0Wlh0cXFqdDM0bmJpZWFyTXlWOQpuWmpmS0ZaQUJGU1c1emhrYklLNDJWRkdTUEdNMERJRFJpVjNkbmhIc1pxT3BuZEhuMUVSTXZqNDYzczVHbXFMCk5PeGw4c01mQWdNQkFBRUNnZ0VBZm9CLy96ZTdvQVl6emZLaitMYkNhSWZ5TWxuNkZ1alMvYk5LdVNYMXp5cUgKOWErNTIzY2tJOGx5Z056TzVLSjVDODFkazhGZG5lVTJqODFhUFJyR28zdXlpMHhndlRPK2RQUFBGYnlEQkdFVApkaHB5cUEydkltTGhMNGZKcEpHTDF3WDlXZTlOK0lmRU51dzNpYmFlQnorKzJ6VkF4T1BlRGV6MytoN3BvNXVYCnVaMC9PVzRnNGxKZlphMm5LWWFzbi80Z0lxS1J0M0NCVmxyakZCVVVXUmxIUEFPdjNFaGlIWGRPWXBxNTM0MFEKazBQbkltcS9FajBEbEluN3U4SDdvbE9Ec1YxUUNwSCtJY2srUnlJWlJQVXFMeTZOemNCOGtxL0RBeXdCT0hFbAp3SGlCc21oSm81RGZ3WHBvdER5eWNYU3Q4SFR5T1BQUTZzdFU4TDBidVFLQmdRRGRDdWVrQ1M2d0RnZVBkMXVwCjZUNzRVdktENm1iaGwrVW9pa052cDJZOENtNjBmTGJOYThzNTRXMlZFeUp3M2VFN0haSlE5M0liOWQ1UlZqWnQKUnpVQXp1MXJhV1F4TlJPTEdnZEcxc1NQQjkzRzUzOEROS2JBVWRJT2RVTit0bHhiVEZxMGdveisrZ1REMVBkRgpRcU55VkRsV0c0YWhOdGRYU3VPbU5tQXI5UUtCZ1FES2xyWVNhZWNoT2xGMmZ1NHdQcjZaZURvT1VLYlRIaTVHCkV5WTJtYm1ZQiswWC9ocHFtQXVtK3V5dWtVVkVYam9JY1hnOE0xblQ2eGFhVFpzMlZyeDd2RjBnVU1BVm5qWnEKTkVZd3ErNkZxdC9RMXM1dlc3SDZSM1h3QmlPQlhYYllwUmY0TnNXQ3o0bmZxYXNVR2xybmZqaUo5cXhWN0ZmRwpMTmdTd2pMNlF3S0JnRlBpZW92MjdCL21BeHAvK21wZDJRYldPN0N5T1A3dDdRcFloa1VPS3k4bjZtRldYdTFRCk5oeXVIeThPeHVnOFcraGFUWmVxZ0VSNkp6ZUkxempiYUJMNWRJSnB5WnNmQUY2dXJ3cEVJTzRDMXpoUHpCVEUKVzIvcTNTT2RmdExNay9vVjNPcGFEUFlLbmRwUHJOTTgrZGcrZkUvZ1BGUmNBcGJmRmN1VElTWXRBb0dBYjZ1RgpyejY3RGNEVXVLbWM1L0VlSlFCMW1BQnpCTHFGTFZGTzVoZjBpczRMcmdiK1RyV0M3c2N3QWNYSDFiak82bXFKCnFUMXhEWFJ2b0J5Wkt1bkN1YjRKNDA4L29tcjBlYlJZNEdsVmNFN1JVbytsZVJLbFYxMWVzRERpRDJRU3A3YlIKTUp3WVlWTy9IdytxWXNsb1JHUjZDK3B4OG1iMXR5SnU5R0FoczNzQ2dZQVRaT0pDWWh3RDlobDVjbk1nd3V3eApEQ3Zvbms5b2k5NkQzV2RtbGJvd2l2SFBobzRndUNUN1hrNi9pVU8rZ0pyMnRSN0tzV1N2YS9PQ2JuSTZCcHo2Ci81UzFrZzBycmhDSngyUzVWaTFZNkNjNUUzTmlGWlpLR2pKaUV5ZXMxUFd3ZmQydzJFbXM3OS9aT0NsQ0JsMjUKL0pabHQ2ellReDFCZ2pNMW5UeWZzUT09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K diff --git a/examples/https-termination/README.md b/examples/https-termination/README.md index 60db373671..bc3f6dac0e 100644 --- a/examples/https-termination/README.md +++ b/examples/https-termination/README.md @@ -39,49 +39,35 @@ In this example we expand on the simple [cafe-example](../cafe-example) by addin ## 3. Configure HTTPS Termination and Routing -HTTPS termination is configured at the gateway level with listeners. You created the following gateway resource in step 1: - -```yaml -apiVersion: gateway.networking.k8s.io/v1alpha2 -kind: Gateway -metadata: - name: gateway - namespace: nginx-gateway - labels: - domain: k8s-gateway.nginx.org -spec: - gatewayClassName: nginx - listeners: - - name: http - port: 80 - protocol: HTTP - - name: https - port: 443 - protocol: HTTPS - tls: - mode: Terminate - certificateRefs: - - kind: Secret - name: default-server-secret - namespace: nginx-gateway -``` - -The `https` listener is configured to terminate TLS connections using the `default-server-secret` in the `nginx-gateway` namespace. -To configure HTTPS termination for our cafe application, we will bind the `https` listener to our `HTTPRoutes` in [cafe-routes.yaml](./cafe-routes.yaml) using the [`parentRef`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io%2fv1alpha2.ParentReference) field: - -```yaml - parentRefs: - - name: gateway - namespace: nginx-gateway - sectionName: https -``` +1. Create a secret with a TLS certificate and key: + ``` + kubectl apply -f cafe-secret.yaml + ``` -1. Create the `HTTPRoute` resources: + The TLS certificate and key in this secret which be used to terminate the TLS connections for the cafe application. + **Important**: This certificate and key are for demo purposes only. +1. Create the `Gateway` resource: + ``` + kubectl apply -f gateway.yaml + ``` + + This [gateway](./gateway.yaml) configures an `https` listener is to terminate TLS connections using the `cafe-secret` we created in the step 1. + +1. Create the `HTTPRoute` resources: ``` kubectl apply -f cafe-routes.yaml ``` + To configure HTTPS termination for our cafe application, we will bind the `https` listener to our `HTTPRoutes` in [cafe-routes.yaml](./cafe-routes.yaml) using the [`parentRef`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io%2fv1alpha2.ParentReference) field: + + ```yaml + parentRefs: + - name: gateway + namespace: nginx-gateway + sectionName: https + ``` + ## 4. Test the Application To access the application, we will use `curl` to send requests to the `coffee` and `tea` services. diff --git a/examples/https-termination/cafe-secret.yaml b/examples/https-termination/cafe-secret.yaml new file mode 100644 index 0000000000..a81954b8db --- /dev/null +++ b/examples/https-termination/cafe-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: cafe-secret + namespace: default +type: kubernetes.io/tls +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNzakNDQVpvQ0NRQzdCdVdXdWRtRkNEQU5CZ2txaGtpRzl3MEJBUXNGQURBYk1Sa3dGd1lEVlFRRERCQmoKWVdabExtVjRZVzF3YkdVdVkyOXRNQjRYRFRJeU1EY3hOREl4TlRJek9Wb1hEVEl6TURjeE5ESXhOVEl6T1ZvdwpHekVaTUJjR0ExVUVBd3dRWTJGbVpTNWxlR0Z0Y0d4bExtTnZiVENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFECmdnRVBBRENDQVFvQ2dnRUJBTHFZMnRHNFc5aStFYzJhdnV4Q2prb2tnUUx1ek10U1Rnc1RNaEhuK3ZRUmxIam8KVzFLRnMvQVdlS25UUStyTWVKVWNseis4M3QwRGtyRThwUisxR2NKSE50WlNMb0NEYUlRN0Nhck5nY1daS0o4Qgo1WDNnVS9YeVJHZjI2c1REd2xzU3NkSEQ1U2U3K2Vab3NPcTdHTVF3K25HR2NVZ0VtL1Q1UEMvY05PWE0zZWxGClRPL051MStoMzROVG9BbDNQdTF2QlpMcDNQVERtQ0thaEROV0NWbUJQUWpNNFI4VERsbFhhMHQ5Z1o1MTRSRzUKWHlZWTNtdzZpUzIrR1dYVXllMjFuWVV4UEhZbDV4RHY0c0FXaGRXbElweHlZQlNCRURjczN6QlI2bFF1OWkxZAp0R1k4dGJ3blVmcUVUR3NZdWxzc05qcU95V1VEcFdJelhibHhJZVVDQXdFQUFUQU5CZ2txaGtpRzl3MEJBUXNGCkFBT0NBUUVBcjkrZWJ0U1dzSnhLTGtLZlRkek1ISFhOd2Y5ZXFVbHNtTXZmMGdBdWVKTUpUR215dG1iWjlpbXQKL2RnWlpYVE9hTElHUG9oZ3BpS0l5eVVRZVdGQ2F0NHRxWkNPVWRhbUloOGk0Q1h6QVJYVHNvcUNOenNNLzZMRQphM25XbFZyS2lmZHYrWkxyRi8vblc0VVNvOEoxaCtQeDljY0tpRDZZU0RVUERDRGh1RUtFWXcvbHpoUDJVOXNmCnl6cEJKVGQ4enFyM3paTjNGWWlITmgzYlRhQS82di9jU2lyamNTK1EwQXg4RWpzQzYxRjRVMTc4QzdWNWRCKzQKcmtPTy9QNlA0UFlWNTRZZHMvRjE2WkZJTHFBNENCYnExRExuYWRxamxyN3NPbzl2ZzNnWFNMYXBVVkdtZ2todAp6VlZPWG1mU0Z4OS90MDBHUi95bUdPbERJbWlXMGc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzZtTnJSdUZ2WXZoSE4KbXI3c1FvNUtKSUVDN3N6TFVrNExFeklSNS9yMEVaUjQ2RnRTaGJQd0ZuaXAwMFBxekhpVkhKYy92TjdkQTVLeApQS1VmdFJuQ1J6YldVaTZBZzJpRU93bXF6WUhGbVNpZkFlVjk0RlAxOGtSbjl1ckV3OEpiRXJIUncrVW51L25tCmFMRHF1eGpFTVBweGhuRklCSnYwK1R3djNEVGx6TjNwUlV6dnpidGZvZCtEVTZBSmR6N3Rid1dTNmR6MHc1Z2kKbW9RelZnbFpnVDBJek9FZkV3NVpWMnRMZllHZWRlRVJ1VjhtR041c09va3R2aGxsMU1udHRaMkZNVHgySmVjUQo3K0xBRm9YVnBTS2NjbUFVZ1JBM0xOOHdVZXBVTHZZdFhiUm1QTFc4SjFINmhFeHJHTHBiTERZNmpzbGxBNlZpCk0xMjVjU0hsQWdNQkFBRUNnZ0VBQnpaRE50bmVTdWxGdk9HZlFYaHRFWGFKdWZoSzJBenRVVVpEcUNlRUxvekQKWlV6dHdxbkNRNlJLczUyandWNTN4cU9kUU94bTNMbjNvSHdNa2NZcEliWW82MjJ2dUczYnkwaVEzaFlsVHVMVgpqQmZCcS9UUXFlL2NMdngvSkczQWhFNmJxdFRjZFlXeGFmTmY2eUtpR1dzZk11WVVXTWs4MGVJVUxuRmZaZ1pOCklYNTlSOHlqdE9CVm9Sa3hjYTVoMW1ZTDFsSlJNM3ZqVHNHTHFybmpOTjNBdWZ3ZGRpK1VDbGZVL2l0K1EvZkUKV216aFFoTlRpNVFkRWJLVStOTnYvNnYvb2JvandNb25HVVBCdEFTUE05cmxFemIralQ1WHdWQjgvLzRGY3VoSwoyVzNpcjhtNHVlQ1JHSVlrbGxlLzhuQmZ0eVhiVkNocVRyZFBlaGlPM1FLQmdRRGlrR3JTOTc3cjg3Y1JPOCtQClpoeXltNXo4NVIzTHVVbFNTazJiOTI1QlhvakpZL2RRZDVTdFVsSWE4OUZKZnNWc1JRcEhHaTFCYzBMaTY1YjIKazR0cE5xcVFoUmZ1UVh0UG9GYXRuQzlPRnJVTXJXbDVJN0ZFejZnNkNQMVBXMEg5d2hPemFKZUdpZVpNYjlYTQoybDdSSFZOcC9jTDlYbmhNMnN0Q1lua2Iwd0tCZ1FEUzF4K0crakEyUVNtRVFWNXA1RnRONGcyamsyZEFjMEhNClRIQ2tTazFDRjhkR0Z2UWtsWm5ZbUt0dXFYeXNtekJGcnZKdmt2eUhqbUNYYTducXlpajBEdDZtODViN3BGcVAKQWxtajdtbXI3Z1pUeG1ZMXBhRWFLMXY4SDNINGtRNVl3MWdrTWRybVJHcVAvaTBGaDVpaGtSZS9DOUtGTFVkSQpDcnJjTzhkUVp3S0JnSHA1MzRXVWNCMVZibzFlYStIMUxXWlFRUmxsTWlwRFM2TzBqeWZWSmtFb1BZSEJESnp2ClIrdzZLREJ4eFoyWmJsZ05LblV0YlhHSVFZd3lGelhNcFB5SGxNVHpiZkJhYmJLcDFyR2JVT2RCMXpXM09PRkgKcmppb21TUm1YNmxhaDk0SjRHU0lFZ0drNGw1SHhxZ3JGRDZ2UDd4NGRjUktJWFpLZ0w2dVJSSUpBb0dCQU1CVApaL2p5WStRNTBLdEtEZHUrYU9ORW4zaGxUN3hrNXRKN3NBek5rbWdGMU10RXlQUk9Xd1pQVGFJbWpRbk9qbHdpCldCZ2JGcXg0M2ZlQ1Z4ZXJ6V3ZEM0txaWJVbWpCTkNMTGtYeGh3ZEVteFQwVit2NzZGYzgwaTNNYVdSNnZZR08KditwVVovL0F6UXdJcWZ6dlVmV2ZxdStrMHlhVXhQOGNlcFBIRyt0bEFvR0FmQUtVVWhqeFU0Ym5vVzVwVUhKegpwWWZXZXZ5TW54NWZyT2VsSmRmNzlvNGMvMHhVSjh1eFBFWDFkRmNrZW96dHNpaVFTNkN6MENRY09XVWxtSkRwCnVrdERvVzM3VmNSQU1BVjY3NlgxQVZlM0UwNm5aL2g2Tkd4Z28rT042Q3pwL0lkMkJPUm9IMFAxa2RjY1NLT3kKMUtFZlNnb1B0c1N1eEpBZXdUZmxDMXc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K diff --git a/examples/https-termination/gateway.yaml b/examples/https-termination/gateway.yaml new file mode 100644 index 0000000000..d67aa65cf2 --- /dev/null +++ b/examples/https-termination/gateway.yaml @@ -0,0 +1,19 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: Gateway +metadata: + name: gateway + namespace: default + labels: + domain: k8s-gateway.nginx.org +spec: + gatewayClassName: nginx + listeners: + - name: https + port: 443 + protocol: HTTPS + tls: + mode: Terminate + certificateRefs: + - kind: Secret + name: cafe-secret + namespace: default From 57a81ddad56494e9310fdd970b6ad0a6b52a396f Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Mon, 18 Jul 2022 10:10:31 -0600 Subject: [PATCH 18/42] Fix test httproute names --- internal/state/configuration_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/state/configuration_test.go b/internal/state/configuration_test.go index c317a2fb28..6cb4e8c826 100644 --- a/internal/state/configuration_test.go +++ b/internal/state/configuration_test.go @@ -98,7 +98,7 @@ func TestBuildConfiguration(t *testing.T) { InvalidSectionNameRefs: map[string]struct{}{}, } - httpsHR3 := createRoute("hr-3", "foo.example.com", "listener-443-1", "/", "/third") + httpsHR3 := createRoute("https-hr-3", "foo.example.com", "listener-443-1", "/", "/third") httpsRouteHR3 := &route{ Source: httpsHR3, @@ -108,20 +108,20 @@ func TestBuildConfiguration(t *testing.T) { InvalidSectionNameRefs: map[string]struct{}{}, } - httpsHR4 := createRoute("hr-4", "foo.example.com", "listener-443-1", "/fourth", "/") + hr4 := createRoute("hr-4", "foo.example.com", "listener-80-1", "/fourth", "/") - httpsRouteHR4 := &route{ - Source: httpsHR4, + routeHR4 := &route{ + Source: hr4, ValidSectionNameRefs: map[string]struct{}{ "listener-80-1": {}, }, InvalidSectionNameRefs: map[string]struct{}{}, } - hr4 := createRoute("hr-4", "foo.example.com", "listener-80-1", "/fourth", "/") + httpsHR4 := createRoute("https-hr-4", "foo.example.com", "listener-443-1", "/fourth", "/") - routeHR4 := &route{ - Source: hr4, + httpsRouteHR4 := &route{ + Source: httpsHR4, ValidSectionNameRefs: map[string]struct{}{ "listener-80-1": {}, }, From 64b8222529140e6cbf2c4fc57b194e20e7d8fd13 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Mon, 18 Jul 2022 10:17:22 -0600 Subject: [PATCH 19/42] Don't configure invalid listeners --- internal/state/configuration.go | 6 +++- internal/state/configuration_test.go | 43 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/internal/state/configuration.go b/internal/state/configuration.go index c35ba5f560..136dbca541 100644 --- a/internal/state/configuration.go +++ b/internal/state/configuration.go @@ -71,8 +71,12 @@ func buildConfiguration(graph *graph) Configuration { } configBuilder := newConfigBuilder() + for _, l := range graph.Gateway.Listeners { - configBuilder.upsertListener(l) + // only upsert listeners that are valid + if l.Valid { + configBuilder.upsertListener(l) + } } return configBuilder.build() diff --git a/internal/state/configuration_test.go b/internal/state/configuration_test.go index 6cb4e8c826..961f88dc41 100644 --- a/internal/state/configuration_test.go +++ b/internal/state/configuration_test.go @@ -152,6 +152,14 @@ func TestBuildConfiguration(t *testing.T) { }, } + invalidListener := v1alpha2.Listener{ + Name: "invalid-listener", + Hostname: nil, + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: nil, // missing TLS config + } + // nolint:gosec secretPath := "/etc/nginx/secrets/secret" @@ -210,6 +218,41 @@ func TestBuildConfiguration(t *testing.T) { }, msg: "http and https listeners with no routes", }, + { + graph: &graph{ + GatewayClass: &gatewayClass{ + Source: &v1alpha2.GatewayClass{}, + Valid: true, + }, + Gateway: &gateway{ + Source: &v1alpha2.Gateway{}, + Listeners: map[string]*listener{ + "invalid-listener": { + Source: invalidListener, + Valid: false, + Routes: map[types.NamespacedName]*route{ + {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, + {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, + }, + AcceptedHostnames: map[string]struct{}{ + "foo.example.com": {}, + "bar.example.com": {}, + }, + SecretPath: "", + }, + }, + }, + Routes: map[types.NamespacedName]*route{ + {Namespace: "test", Name: "https-hr-1"}: httpsRouteHR1, + {Namespace: "test", Name: "https-hr-2"}: httpsRouteHR2, + }, + }, + expected: Configuration{ + HTTPServers: []HTTPServer{}, + HTTPSServers: []HTTPServer{}, + }, + msg: "invalid listener", + }, { graph: &graph{ GatewayClass: &gatewayClass{ From 4b6b9ad45ee203c12a7fbea2f85d66015b89ff21 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Mon, 18 Jul 2022 10:18:54 -0600 Subject: [PATCH 20/42] collision -> collisions --- internal/state/graph_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/state/graph_test.go b/internal/state/graph_test.go index f86e4ac84d..326975fc70 100644 --- a/internal/state/graph_test.go +++ b/internal/state/graph_test.go @@ -619,7 +619,7 @@ func TestBuildListeners(t *testing.T) { SecretPath: secretPath, }, }, - msg: "collision", + msg: "collisions", }, { gateway: nil, From bfb6c7b1e366ba39ea5ca0a81bc9fcd4358f802d Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Mon, 18 Jul 2022 10:19:27 -0600 Subject: [PATCH 21/42] validateHTTPSListener --- internal/state/graph_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/state/graph_test.go b/internal/state/graph_test.go index 326975fc70..8dc87569d4 100644 --- a/internal/state/graph_test.go +++ b/internal/state/graph_test.go @@ -1127,7 +1127,7 @@ func TestValidateHTTPSListener(t *testing.T) { for _, test := range tests { result := validateHTTPSListener(test.l, gwNs) if result != test.expected { - t.Errorf("validateListener() returned %v but expected %v for the case of %q", result, test.expected, test.msg) + t.Errorf("validateHTTPSListener() returned %v but expected %v for the case of %q", result, test.expected, test.msg) } } } From 8bfd55120ecc9ad6a5334a6ccf96d5132d8c88b1 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Mon, 18 Jul 2022 10:29:01 -0600 Subject: [PATCH 22/42] Fix up secrets tests and rename WriteAllStoredSecrets to WriteAllRequestedSecrets --- internal/events/loop.go | 4 +- internal/state/secrets.go | 6 +- internal/state/secrets_test.go | 41 +++++++---- .../fake_secret_disk_memory_manager.go | 70 +++++++++---------- 4 files changed, 68 insertions(+), 53 deletions(-) diff --git a/internal/events/loop.go b/internal/events/loop.go index 17ac1acbc6..a5c9711ae5 100644 --- a/internal/events/loop.go +++ b/internal/events/loop.go @@ -92,9 +92,9 @@ func (el *EventLoop) handleEvent(ctx context.Context, event interface{}) { func (el *EventLoop) updateNginx(ctx context.Context, conf state.Configuration) error { // Write all secrets (nuke and pave). - // This will remove all secrets in the secrets directory before writing the stored secrets. + // 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. - err := el.cfg.SecretMemoryManager.WriteAllStoredSecrets() + err := el.cfg.SecretMemoryManager.WriteAllRequestedSecrets() if err != nil { return err } diff --git a/internal/state/secrets.go b/internal/state/secrets.go index adf1ca215b..9c54f80ba5 100644 --- a/internal/state/secrets.go +++ b/internal/state/secrets.go @@ -68,8 +68,8 @@ type SecretDiskMemoryManager interface { // Request marks the secret as requested so that it can be written to disk before reloading NGINX. // Returns the path to the secret and an error if the secret does not exist in the secret store or the secret is invalid. Request(nsname types.NamespacedName) (string, error) - // WriteAllStoredSecrets writes all requested secrets to disk. - WriteAllStoredSecrets() error + // WriteAllRequestedSecrets writes all requested secrets to disk. + WriteAllRequestedSecrets() error } type SecretDiskMemoryManagerImpl struct { @@ -111,7 +111,7 @@ func (s *SecretDiskMemoryManagerImpl) Request(nsname types.NamespacedName) (stri return ss.path, nil } -func (s *SecretDiskMemoryManagerImpl) WriteAllStoredSecrets() error { +func (s *SecretDiskMemoryManagerImpl) WriteAllRequestedSecrets() error { // Remove all existing secrets from secrets directory dir, err := ioutil.ReadDir(s.secretDirectory) if err != nil { diff --git a/internal/state/secrets_test.go b/internal/state/secrets_test.go index 35e062ca78..00ed7bb121 100644 --- a/internal/state/secrets_test.go +++ b/internal/state/secrets_test.go @@ -154,7 +154,7 @@ var _ = Describe("SecretDiskMemoryManager", func() { }) Describe("Manages secrets on disk", Ordered, func() { - testStore := func(s *apiv1.Secret, expPath string, expErr bool) { + testRequest := func(s *apiv1.Secret, expPath string, expErr bool) { nsname := types.NamespacedName{Namespace: s.Namespace, Name: s.Name} actualPath, err := memMgr.Request(nsname) @@ -170,30 +170,30 @@ var _ = Describe("SecretDiskMemoryManager", func() { It("should return an error and empty path when secret does not exist", func() { fakeStore.GetReturns(nil) - testStore(secret1, "", true) + testRequest(secret1, "", true) }) - It("should store a valid secret", func() { + It("request should return the file path for a valid secret", func() { fakeStore.GetReturns(&state.Secret{Secret: secret1, Valid: true}) expectedPath := path.Join(tmpSecretsDir, "test_secret1") - testStore(secret1, expectedPath, false) + testRequest(secret1, expectedPath, false) }) - It("should store another valid secret", func() { + It("request should return the file path for another valid secret", func() { fakeStore.GetReturns(&state.Secret{Secret: secret2, Valid: true}) expectedPath := path.Join(tmpSecretsDir, "test_secret2") - testStore(secret2, expectedPath, false) + testRequest(secret2, expectedPath, false) }) - It("should return an error and empty path when secret is invalid", func() { + It("request should return an error and empty path when secret is invalid", func() { fakeStore.GetReturns(&state.Secret{Secret: invalidSecretType, Valid: false}) - testStore(invalidSecretType, "", true) + testRequest(invalidSecretType, "", true) }) - It("should write all stored secrets", func() { - err := memMgr.WriteAllStoredSecrets() + It("should write all requested secrets", func() { + err := memMgr.WriteAllRequestedSecrets() Expect(err).ToNot(HaveOccurred()) expectedFileNames := []string{"test_secret1", "test_secret2"} @@ -208,15 +208,15 @@ var _ = Describe("SecretDiskMemoryManager", func() { Expect(actualFilenames).To(ConsistOf(expectedFileNames)) }) - It("should store secret after write", func() { + It("request should return the file path for secret after write", func() { fakeStore.GetReturns(&state.Secret{Secret: secret3, Valid: true}) expectedPath := path.Join(tmpSecretsDir, "test_secret3") - testStore(secret3, expectedPath, false) + testRequest(secret3, expectedPath, false) }) It("should write all stored secrets", func() { - err := memMgr.WriteAllStoredSecrets() + err := memMgr.WriteAllRequestedSecrets() Expect(err).ToNot(HaveOccurred()) // read all files from directory @@ -227,6 +227,19 @@ var _ = Describe("SecretDiskMemoryManager", func() { Expect(dir).To(HaveLen(1)) Expect(dir[0].Name()).To(Equal("test_secret3")) }) + When("no secrets are requested", func() { + It("write all secrets should remove all existing secrets and write no additional secrets", func() { + err := memMgr.WriteAllRequestedSecrets() + Expect(err).ToNot(HaveOccurred()) + + // read all files from directory + dir, err := ioutil.ReadDir(tmpSecretsDir) + Expect(err).ToNot(HaveOccurred()) + + // no secrets should exist + Expect(dir).To(BeEmpty()) + }) + }) }) }) @@ -253,6 +266,8 @@ var _ = Describe("SecretStore", func() { actualSecret := store.Get(nsname) if valid { Expect(actualSecret.Valid).To(BeTrue()) + } else { + Expect(actualSecret.Valid).To(BeFalse()) } Expect(actualSecret.Secret).To(Equal(s)) } diff --git a/internal/state/statefakes/fake_secret_disk_memory_manager.go b/internal/state/statefakes/fake_secret_disk_memory_manager.go index d97425ba36..d7e604154d 100644 --- a/internal/state/statefakes/fake_secret_disk_memory_manager.go +++ b/internal/state/statefakes/fake_secret_disk_memory_manager.go @@ -22,14 +22,14 @@ type FakeSecretDiskMemoryManager struct { result1 string result2 error } - WriteAllStoredSecretsStub func() error - writeAllStoredSecretsMutex sync.RWMutex - writeAllStoredSecretsArgsForCall []struct { + WriteAllRequestedSecretsStub func() error + writeAllRequestedSecretsMutex sync.RWMutex + writeAllRequestedSecretsArgsForCall []struct { } - writeAllStoredSecretsReturns struct { + writeAllRequestedSecretsReturns struct { result1 error } - writeAllStoredSecretsReturnsOnCall map[int]struct { + writeAllRequestedSecretsReturnsOnCall map[int]struct { result1 error } invocations map[string][][]interface{} @@ -100,15 +100,15 @@ func (fake *FakeSecretDiskMemoryManager) RequestReturnsOnCall(i int, result1 str }{result1, result2} } -func (fake *FakeSecretDiskMemoryManager) WriteAllStoredSecrets() error { - fake.writeAllStoredSecretsMutex.Lock() - ret, specificReturn := fake.writeAllStoredSecretsReturnsOnCall[len(fake.writeAllStoredSecretsArgsForCall)] - fake.writeAllStoredSecretsArgsForCall = append(fake.writeAllStoredSecretsArgsForCall, struct { +func (fake *FakeSecretDiskMemoryManager) WriteAllRequestedSecrets() error { + fake.writeAllRequestedSecretsMutex.Lock() + ret, specificReturn := fake.writeAllRequestedSecretsReturnsOnCall[len(fake.writeAllRequestedSecretsArgsForCall)] + fake.writeAllRequestedSecretsArgsForCall = append(fake.writeAllRequestedSecretsArgsForCall, struct { }{}) - stub := fake.WriteAllStoredSecretsStub - fakeReturns := fake.writeAllStoredSecretsReturns - fake.recordInvocation("WriteAllStoredSecrets", []interface{}{}) - fake.writeAllStoredSecretsMutex.Unlock() + stub := fake.WriteAllRequestedSecretsStub + fakeReturns := fake.writeAllRequestedSecretsReturns + fake.recordInvocation("WriteAllRequestedSecrets", []interface{}{}) + fake.writeAllRequestedSecretsMutex.Unlock() if stub != nil { return stub() } @@ -118,37 +118,37 @@ func (fake *FakeSecretDiskMemoryManager) WriteAllStoredSecrets() error { return fakeReturns.result1 } -func (fake *FakeSecretDiskMemoryManager) WriteAllStoredSecretsCallCount() int { - fake.writeAllStoredSecretsMutex.RLock() - defer fake.writeAllStoredSecretsMutex.RUnlock() - return len(fake.writeAllStoredSecretsArgsForCall) +func (fake *FakeSecretDiskMemoryManager) WriteAllRequestedSecretsCallCount() int { + fake.writeAllRequestedSecretsMutex.RLock() + defer fake.writeAllRequestedSecretsMutex.RUnlock() + return len(fake.writeAllRequestedSecretsArgsForCall) } -func (fake *FakeSecretDiskMemoryManager) WriteAllStoredSecretsCalls(stub func() error) { - fake.writeAllStoredSecretsMutex.Lock() - defer fake.writeAllStoredSecretsMutex.Unlock() - fake.WriteAllStoredSecretsStub = stub +func (fake *FakeSecretDiskMemoryManager) WriteAllRequestedSecretsCalls(stub func() error) { + fake.writeAllRequestedSecretsMutex.Lock() + defer fake.writeAllRequestedSecretsMutex.Unlock() + fake.WriteAllRequestedSecretsStub = stub } -func (fake *FakeSecretDiskMemoryManager) WriteAllStoredSecretsReturns(result1 error) { - fake.writeAllStoredSecretsMutex.Lock() - defer fake.writeAllStoredSecretsMutex.Unlock() - fake.WriteAllStoredSecretsStub = nil - fake.writeAllStoredSecretsReturns = struct { +func (fake *FakeSecretDiskMemoryManager) WriteAllRequestedSecretsReturns(result1 error) { + fake.writeAllRequestedSecretsMutex.Lock() + defer fake.writeAllRequestedSecretsMutex.Unlock() + fake.WriteAllRequestedSecretsStub = nil + fake.writeAllRequestedSecretsReturns = struct { result1 error }{result1} } -func (fake *FakeSecretDiskMemoryManager) WriteAllStoredSecretsReturnsOnCall(i int, result1 error) { - fake.writeAllStoredSecretsMutex.Lock() - defer fake.writeAllStoredSecretsMutex.Unlock() - fake.WriteAllStoredSecretsStub = nil - if fake.writeAllStoredSecretsReturnsOnCall == nil { - fake.writeAllStoredSecretsReturnsOnCall = make(map[int]struct { +func (fake *FakeSecretDiskMemoryManager) WriteAllRequestedSecretsReturnsOnCall(i int, result1 error) { + fake.writeAllRequestedSecretsMutex.Lock() + defer fake.writeAllRequestedSecretsMutex.Unlock() + fake.WriteAllRequestedSecretsStub = nil + if fake.writeAllRequestedSecretsReturnsOnCall == nil { + fake.writeAllRequestedSecretsReturnsOnCall = make(map[int]struct { result1 error }) } - fake.writeAllStoredSecretsReturnsOnCall[i] = struct { + fake.writeAllRequestedSecretsReturnsOnCall[i] = struct { result1 error }{result1} } @@ -158,8 +158,8 @@ func (fake *FakeSecretDiskMemoryManager) Invocations() map[string][][]interface{ defer fake.invocationsMutex.RUnlock() fake.requestMutex.RLock() defer fake.requestMutex.RUnlock() - fake.writeAllStoredSecretsMutex.RLock() - defer fake.writeAllStoredSecretsMutex.RUnlock() + fake.writeAllRequestedSecretsMutex.RLock() + defer fake.writeAllRequestedSecretsMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value From 7c340245bb2e40e08fac5bf2634e444172b6e72d Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Mon, 18 Jul 2022 12:57:57 -0600 Subject: [PATCH 23/42] Generate default http server --- deploy/manifests/nginx-gateway.yaml | 2 +- internal/nginx/config/generator.go | 18 +++++++-- internal/nginx/config/generator_test.go | 51 +++++++++++++++++-------- internal/nginx/config/http.go | 9 +++-- internal/nginx/config/template.go | 9 ++++- 5 files changed, 63 insertions(+), 26 deletions(-) diff --git a/deploy/manifests/nginx-gateway.yaml b/deploy/manifests/nginx-gateway.yaml index c0017a1b19..97f69cf6e3 100644 --- a/deploy/manifests/nginx-gateway.yaml +++ b/deploy/manifests/nginx-gateway.yaml @@ -81,7 +81,7 @@ spec: initContainers: - image: busybox:1.34 # FIXME(pleshakov): use gateway container to init the Config with proper main config name: nginx-config-initializer - command: [ 'sh', '-c', 'echo "load_module /usr/lib/nginx/modules/ngx_http_js_module.so; events {} pid /etc/nginx/nginx.pid; http { include /etc/nginx/conf.d/*.conf; js_import /usr/lib/nginx/modules/njs/httpmatches.js; server { default_type text/html; return 404; } }" > /etc/nginx/nginx.conf && mkdir /etc/nginx/conf.d /etc/nginx/secrets && chown 1001:0 /etc/nginx/conf.d /etc/nginx/secrets' ] + command: [ 'sh', '-c', 'echo "load_module /usr/lib/nginx/modules/ngx_http_js_module.so; events {} pid /etc/nginx/nginx.pid; http { include /etc/nginx/conf.d/*.conf; js_import /usr/lib/nginx/modules/njs/httpmatches.js; }" > /etc/nginx/nginx.conf && mkdir /etc/nginx/conf.d /etc/nginx/secrets && chown 1001:0 /etc/nginx/conf.d /etc/nginx/secrets' ] volumeMounts: - name: nginx-config mountPath: /etc/nginx diff --git a/internal/nginx/config/generator.go b/internal/nginx/config/generator.go index c01d1a638a..304974287c 100644 --- a/internal/nginx/config/generator.go +++ b/internal/nginx/config/generator.go @@ -47,10 +47,16 @@ func (g *GeneratorImpl) Generate(conf state.Configuration) ([]byte, Warnings) { Servers: make([]server, 0, len(confServers)+1), } + if len(conf.HTTPServers) > 0 { + defaultHTTPServer := generateDefaultHTTPServer() + + servers.Servers = append(servers.Servers, defaultHTTPServer) + } + if len(conf.HTTPSServers) > 0 { - defaultServer := generateDefaultTLSTerminationServer() + defaultTLSTerminationServer := generateDefaultSSLServer() - servers.Servers = append(servers.Servers, defaultServer) + servers.Servers = append(servers.Servers, defaultTLSTerminationServer) } for _, s := range confServers { @@ -63,8 +69,12 @@ func (g *GeneratorImpl) Generate(conf state.Configuration) ([]byte, Warnings) { return g.executor.ExecuteForHTTPServers(servers), warnings } -func generateDefaultTLSTerminationServer() server { - return server{IsDefault: true} +func generateDefaultSSLServer() server { + return server{IsDefaultSSL: true} +} + +func generateDefaultHTTPServer() server { + return server{IsDefaultHTTP: true} } func generate(httpServer state.HTTPServer, serviceStore state.ServiceStore) (server, Warnings) { diff --git a/internal/nginx/config/generator_test.go b/internal/nginx/config/generator_test.go index 757fb19f29..fdaae69dbc 100644 --- a/internal/nginx/config/generator_test.go +++ b/internal/nginx/config/generator_test.go @@ -20,10 +20,17 @@ func TestGenerateForHost(t *testing.T) { generator := NewGeneratorImpl(&statefakes.FakeServiceStore{}) testcases := []struct { - conf state.Configuration - expectDefault bool - msg string + conf state.Configuration + httpDefault bool + sslDefault bool + msg string }{ + { + conf: state.Configuration{}, + httpDefault: false, + sslDefault: false, + msg: "no servers", + }, { conf: state.Configuration{ HTTPServers: []state.HTTPServer{ @@ -32,8 +39,9 @@ func TestGenerateForHost(t *testing.T) { }, }, }, - expectDefault: false, - msg: "only HTTP servers", + httpDefault: true, + sslDefault: false, + msg: "only HTTP servers", }, { conf: state.Configuration{ @@ -43,8 +51,9 @@ func TestGenerateForHost(t *testing.T) { }, }, }, - expectDefault: true, - msg: "only HTTPS servers", + httpDefault: false, + sslDefault: true, + msg: "only HTTPS servers", }, { conf: state.Configuration{ @@ -59,29 +68,39 @@ func TestGenerateForHost(t *testing.T) { }, }, }, - expectDefault: true, - msg: "both HTTP and HTTPS servers", + httpDefault: true, + sslDefault: true, + msg: "both HTTP and HTTPS servers", }, } for _, tc := range testcases { cfg, warnings := generator.Generate(tc.conf) - defaultExists := strings.Contains(string(cfg), "default") + defaultSSLExists := strings.Contains(string(cfg), "listen 443 ssl default_server") + defaultHTTPExists := strings.Contains(string(cfg), "listen 80 default_server") + + if tc.sslDefault && !defaultSSLExists { + t.Errorf("Generate() did not generate a config with a default TLS termination server for test: %q", tc.msg) + } + + if !tc.sslDefault && defaultSSLExists { + t.Errorf("Generate() generated a config with a default TLS termination server for test: %q", tc.msg) + } - if tc.expectDefault && !defaultExists { - t.Errorf("Generate() did not generate a config with a default TLS termination server") + if tc.httpDefault && !defaultHTTPExists { + t.Errorf("Generate() did not generate a config with a default http server for test: %q", tc.msg) } - if !tc.expectDefault && defaultExists { - t.Errorf("Generate() generated a config with a default TLS termination server") + if !tc.httpDefault && defaultHTTPExists { + t.Errorf("Generate() generated a config with a default http server for test: %q", tc.msg) } if len(cfg) == 0 { - t.Errorf("Generate() generated empty config") + t.Errorf("Generate() generated empty config for test: %q", tc.msg) } if len(warnings) > 0 { - t.Errorf("Generate() returned unexpected warnings: %v", warnings) + t.Errorf("Generate() returned unexpected warnings: %v for test: %q", warnings, tc.msg) } } } diff --git a/internal/nginx/config/http.go b/internal/nginx/config/http.go index 072d9891da..50d39b9025 100644 --- a/internal/nginx/config/http.go +++ b/internal/nginx/config/http.go @@ -5,10 +5,11 @@ type httpServers struct { } type server struct { - IsDefault bool - ServerName string - SSL *ssl - Locations []location + IsDefaultHTTP bool + IsDefaultSSL bool + ServerName string + SSL *ssl + Locations []location } type location struct { diff --git a/internal/nginx/config/template.go b/internal/nginx/config/template.go index 612c63d1ff..e66444fb94 100644 --- a/internal/nginx/config/template.go +++ b/internal/nginx/config/template.go @@ -7,11 +7,18 @@ import ( ) var httpServersTemplate = `{{ range $s := .Servers }} - {{ if $s.IsDefault }} + {{ if $s.IsDefaultSSL }} server { listen 443 ssl default_server; ssl_reject_handshake on; +} + {{ else if $s.IsDefaultHTTP }} +server { + listen 80 default_server; + + default_type text/html + return 404; } {{ else }} server { From 445974e11dd143920806223c3ce6fe68f2973207 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Mon, 18 Jul 2022 13:09:12 -0600 Subject: [PATCH 24/42] HTTPServer -> VirtualServer and HTTPSServers -> SSLServers --- internal/nginx/config/generator.go | 18 ++++---- internal/nginx/config/generator_test.go | 12 ++--- internal/state/change_processor_test.go | 32 ++++++------- internal/state/configuration.go | 60 ++++++++++++------------- internal/state/configuration_test.go | 20 ++++----- 5 files changed, 71 insertions(+), 71 deletions(-) diff --git a/internal/nginx/config/generator.go b/internal/nginx/config/generator.go index 304974287c..1d313659f7 100644 --- a/internal/nginx/config/generator.go +++ b/internal/nginx/config/generator.go @@ -40,7 +40,7 @@ func NewGeneratorImpl(serviceStore state.ServiceStore) *GeneratorImpl { func (g *GeneratorImpl) Generate(conf state.Configuration) ([]byte, Warnings) { warnings := newWarnings() - confServers := append(conf.HTTPServers, conf.HTTPSServers...) + confServers := append(conf.HTTPServers, conf.SSLServers...) servers := httpServers{ // capacity is all the conf servers + default tls termination server @@ -53,7 +53,7 @@ func (g *GeneratorImpl) Generate(conf state.Configuration) ([]byte, Warnings) { servers.Servers = append(servers.Servers, defaultHTTPServer) } - if len(conf.HTTPSServers) > 0 { + if len(conf.SSLServers) > 0 { defaultTLSTerminationServer := generateDefaultSSLServer() servers.Servers = append(servers.Servers, defaultTLSTerminationServer) @@ -77,12 +77,12 @@ func generateDefaultHTTPServer() server { return server{IsDefaultHTTP: true} } -func generate(httpServer state.HTTPServer, serviceStore state.ServiceStore) (server, Warnings) { +func generate(virtualServer state.VirtualServer, serviceStore state.ServiceStore) (server, Warnings) { warnings := newWarnings() - locs := make([]location, 0, len(httpServer.PathRules)) // FIXME(pleshakov): expand with rule.Routes + locs := make([]location, 0, len(virtualServer.PathRules)) // FIXME(pleshakov): expand with rule.Routes - for _, rule := range httpServer.PathRules { + for _, rule := range virtualServer.PathRules { matches := make([]httpMatch, 0, len(rule.MatchRules)) for ruleIdx, r := range rule.MatchRules { @@ -126,13 +126,13 @@ func generate(httpServer state.HTTPServer, serviceStore state.ServiceStore) (ser } } s := server{ - ServerName: httpServer.Hostname, + ServerName: virtualServer.Hostname, Locations: locs, } - if httpServer.SSL != nil { + if virtualServer.SSL != nil { s.SSL = &ssl{ - Certificate: httpServer.SSL.CertificatePath, - CertificateKey: httpServer.SSL.CertificatePath, + Certificate: virtualServer.SSL.CertificatePath, + CertificateKey: virtualServer.SSL.CertificatePath, } } return s, warnings diff --git a/internal/nginx/config/generator_test.go b/internal/nginx/config/generator_test.go index fdaae69dbc..ae6543d758 100644 --- a/internal/nginx/config/generator_test.go +++ b/internal/nginx/config/generator_test.go @@ -33,7 +33,7 @@ func TestGenerateForHost(t *testing.T) { }, { conf: state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ { Hostname: "example.com", }, @@ -45,7 +45,7 @@ func TestGenerateForHost(t *testing.T) { }, { conf: state.Configuration{ - HTTPSServers: []state.HTTPServer{ + SSLServers: []state.VirtualServer{ { Hostname: "example.com", }, @@ -57,12 +57,12 @@ func TestGenerateForHost(t *testing.T) { }, { conf: state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ { Hostname: "example.com", }, }, - HTTPSServers: []state.HTTPServer{ + SSLServers: []state.VirtualServer{ { Hostname: "example.com", }, @@ -214,7 +214,7 @@ func TestGenerate(t *testing.T) { certPath := "/etc/nginx/secrets/cert" - httpHost := state.HTTPServer{ + httpHost := state.VirtualServer{ Hostname: "example.com", PathRules: []state.PathRule{ { @@ -336,7 +336,7 @@ func TestGenerate(t *testing.T) { } testcases := []struct { - host state.HTTPServer + host state.VirtualServer expWarnings Warnings expResult server msg string diff --git a/internal/state/change_processor_test.go b/internal/state/change_processor_test.go index baf9f2d75c..b394609cfc 100644 --- a/internal/state/change_processor_test.go +++ b/internal/state/change_processor_test.go @@ -211,7 +211,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(gc) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ { Hostname: "foo.example.com", PathRules: []state.PathRule{ @@ -228,7 +228,7 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - HTTPSServers: []state.HTTPServer{ + SSLServers: []state.VirtualServer{ { Hostname: "foo.example.com", SSL: &state.SSL{CertificatePath: certificatePath}, @@ -298,7 +298,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(hr1Updated) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ { Hostname: "foo.example.com", PathRules: []state.PathRule{ @@ -315,7 +315,7 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - HTTPSServers: []state.HTTPServer{ + SSLServers: []state.VirtualServer{ { Hostname: "foo.example.com", SSL: &state.SSL{CertificatePath: certificatePath}, @@ -384,7 +384,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(gw1Updated) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ { Hostname: "foo.example.com", PathRules: []state.PathRule{ @@ -401,7 +401,7 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - HTTPSServers: []state.HTTPServer{ + SSLServers: []state.VirtualServer{ { Hostname: "foo.example.com", SSL: &state.SSL{CertificatePath: certificatePath}, @@ -470,7 +470,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(gcUpdated) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ { Hostname: "foo.example.com", PathRules: []state.PathRule{ @@ -487,7 +487,7 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - HTTPSServers: []state.HTTPServer{ + SSLServers: []state.VirtualServer{ { Hostname: "foo.example.com", SSL: &state.SSL{CertificatePath: certificatePath}, @@ -553,7 +553,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(gw2) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ { Hostname: "foo.example.com", PathRules: []state.PathRule{ @@ -570,7 +570,7 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - HTTPSServers: []state.HTTPServer{ + SSLServers: []state.VirtualServer{ { Hostname: "foo.example.com", PathRules: []state.PathRule{ @@ -634,7 +634,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(hr2) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ { Hostname: "foo.example.com", PathRules: []state.PathRule{ @@ -651,7 +651,7 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - HTTPSServers: []state.HTTPServer{ + SSLServers: []state.VirtualServer{ { Hostname: "foo.example.com", SSL: &state.SSL{CertificatePath: certificatePath}, @@ -719,7 +719,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureDeleteChange(&v1alpha2.Gateway{}, types.NamespacedName{Namespace: "test", Name: "gateway-1"}) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ { Hostname: "bar.example.com", PathRules: []state.PathRule{ @@ -736,7 +736,7 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, - HTTPSServers: []state.HTTPServer{ + SSLServers: []state.VirtualServer{ { Hostname: "bar.example.com", SSL: &state.SSL{CertificatePath: certificatePath}, @@ -794,8 +794,8 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureDeleteChange(&v1alpha2.HTTPRoute{}, types.NamespacedName{Namespace: "test", Name: "hr-2"}) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{}, - HTTPSServers: []state.HTTPServer{}, + HTTPServers: []state.VirtualServer{}, + SSLServers: []state.VirtualServer{}, } expectedStatuses := state.Statuses{ GatewayClassStatus: &state.GatewayClassStatus{ diff --git a/internal/state/configuration.go b/internal/state/configuration.go index 136dbca541..be37523316 100644 --- a/internal/state/configuration.go +++ b/internal/state/configuration.go @@ -13,14 +13,14 @@ import ( type Configuration struct { // HTTPServers holds all HTTPServers. // FIXME(pleshakov) We assume that all servers are HTTP and listen on port 80. - HTTPServers []HTTPServer - // HTTPSServers holds all HTTPSServers. - // FIXME(kate-osborn) We assume that all HTTPS servers listen on port 443. - HTTPSServers []HTTPServer + HTTPServers []VirtualServer + // SSLServers holds all SSLServers. + // FIXME(kate-osborn) We assume that all SSL servers listen on port 443. + SSLServers []VirtualServer } -// HTTPServer is a virtual server. -type HTTPServer struct { +// VirtualServer is a virtual server. +type VirtualServer struct { // Hostname is the hostname of the server. Hostname string // PathRules is a collection of routing rules. @@ -83,48 +83,48 @@ func buildConfiguration(graph *graph) Configuration { } type configBuilder struct { - http *httpServerBuilder - https *httpServerBuilder + http *virtualServerBuilder + ssl *virtualServerBuilder } func newConfigBuilder() *configBuilder { return &configBuilder{ - http: newHTTPServerBuilder(), - https: newHTTPServerBuilder(), + http: newHTTPServerBuilder(), + ssl: newHTTPServerBuilder(), } } -func (sb *configBuilder) upsertListener(l *listener) { +func (b *configBuilder) upsertListener(l *listener) { switch l.Source.Protocol { case v1alpha2.HTTPProtocolType: - sb.http.upsertListener(l) + b.http.upsertListener(l) case v1alpha2.HTTPSProtocolType: - sb.https.upsertListener(l) + b.ssl.upsertListener(l) default: panic(fmt.Sprintf("listener protocol %s not supported", l.Source.Protocol)) } } -func (sb *configBuilder) build() Configuration { +func (b *configBuilder) build() Configuration { return Configuration{ - HTTPServers: sb.http.build(), - HTTPSServers: sb.https.build(), + HTTPServers: b.http.build(), + SSLServers: b.ssl.build(), } } -type httpServerBuilder struct { +type virtualServerBuilder struct { rulesPerHost map[string]map[string]PathRule listenersForHost map[string]*listener } -func newHTTPServerBuilder() *httpServerBuilder { - return &httpServerBuilder{ +func newHTTPServerBuilder() *virtualServerBuilder { + return &virtualServerBuilder{ rulesPerHost: make(map[string]map[string]PathRule), listenersForHost: make(map[string]*listener), } } -func (p *httpServerBuilder) upsertListener(l *listener) { +func (b *virtualServerBuilder) upsertListener(l *listener) { for _, r := range l.Routes { var hostnames []string @@ -136,9 +136,9 @@ func (p *httpServerBuilder) upsertListener(l *listener) { } for _, h := range hostnames { - p.listenersForHost[h] = l - if _, exist := p.rulesPerHost[h]; !exist { - p.rulesPerHost[h] = make(map[string]PathRule) + b.listenersForHost[h] = l + if _, exist := b.rulesPerHost[h]; !exist { + b.rulesPerHost[h] = make(map[string]PathRule) } } @@ -147,7 +147,7 @@ func (p *httpServerBuilder) upsertListener(l *listener) { for j, m := range rule.Matches { path := getPath(m.Path) - rule, exist := p.rulesPerHost[h][path] + rule, exist := b.rulesPerHost[h][path] if !exist { rule.Path = path } @@ -158,24 +158,24 @@ func (p *httpServerBuilder) upsertListener(l *listener) { Source: r.Source, }) - p.rulesPerHost[h][path] = rule + b.rulesPerHost[h][path] = rule } } } } } -func (p *httpServerBuilder) build() []HTTPServer { +func (b *virtualServerBuilder) build() []VirtualServer { - servers := make([]HTTPServer, 0, len(p.rulesPerHost)) + servers := make([]VirtualServer, 0, len(b.rulesPerHost)) - for h, rules := range p.rulesPerHost { - s := HTTPServer{ + for h, rules := range b.rulesPerHost { + s := VirtualServer{ Hostname: h, PathRules: make([]PathRule, 0, len(rules)), } - l, ok := p.listenersForHost[h] + l, ok := b.listenersForHost[h] if !ok { panic(fmt.Sprintf("no listener found for hostname: %s", h)) } diff --git a/internal/state/configuration_test.go b/internal/state/configuration_test.go index 961f88dc41..dde277d90f 100644 --- a/internal/state/configuration_test.go +++ b/internal/state/configuration_test.go @@ -181,8 +181,8 @@ func TestBuildConfiguration(t *testing.T) { Routes: map[types.NamespacedName]*route{}, }, expected: Configuration{ - HTTPServers: []HTTPServer{}, - HTTPSServers: []HTTPServer{}, + HTTPServers: []VirtualServer{}, + SSLServers: []VirtualServer{}, }, msg: "no listeners and routes", }, @@ -213,8 +213,8 @@ func TestBuildConfiguration(t *testing.T) { Routes: map[types.NamespacedName]*route{}, }, expected: Configuration{ - HTTPServers: []HTTPServer{}, - HTTPSServers: []HTTPServer{}, + HTTPServers: []VirtualServer{}, + SSLServers: []VirtualServer{}, }, msg: "http and https listeners with no routes", }, @@ -248,8 +248,8 @@ func TestBuildConfiguration(t *testing.T) { }, }, expected: Configuration{ - HTTPServers: []HTTPServer{}, - HTTPSServers: []HTTPServer{}, + HTTPServers: []VirtualServer{}, + SSLServers: []VirtualServer{}, }, msg: "invalid listener", }, @@ -297,7 +297,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, expected: Configuration{ - HTTPServers: []HTTPServer{ + HTTPServers: []VirtualServer{ { Hostname: "bar.example.com", PathRules: []PathRule{ @@ -329,7 +329,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, - HTTPSServers: []HTTPServer{ + SSLServers: []VirtualServer{ { Hostname: "bar.example.com", PathRules: []PathRule{ @@ -412,7 +412,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, expected: Configuration{ - HTTPServers: []HTTPServer{ + HTTPServers: []VirtualServer{ { Hostname: "foo.example.com", PathRules: []PathRule{ @@ -454,7 +454,7 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, - HTTPSServers: []HTTPServer{ + SSLServers: []VirtualServer{ { Hostname: "foo.example.com", SSL: &SSL{ From 3d1d47241a72cfb5603969ce66ce9d53bdd503b4 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Tue, 19 Jul 2022 10:04:26 -0600 Subject: [PATCH 25/42] Fix missing semicolon --- internal/nginx/config/template.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/nginx/config/template.go b/internal/nginx/config/template.go index e66444fb94..08cce30d7a 100644 --- a/internal/nginx/config/template.go +++ b/internal/nginx/config/template.go @@ -17,7 +17,7 @@ server { server { listen 80 default_server; - default_type text/html + default_type text/html; return 404; } {{ else }} From 84adfd00371318c44ced68eb65c326c4a3cdcd7d Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Tue, 19 Jul 2022 10:04:50 -0600 Subject: [PATCH 26/42] Change ns of gateway in example --- examples/https-termination/cafe-routes.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/https-termination/cafe-routes.yaml b/examples/https-termination/cafe-routes.yaml index 97db79a937..23e4ed9325 100644 --- a/examples/https-termination/cafe-routes.yaml +++ b/examples/https-termination/cafe-routes.yaml @@ -5,7 +5,7 @@ metadata: spec: parentRefs: - name: gateway - namespace: nginx-gateway + namespace: default sectionName: https hostnames: - "cafe.example.com" @@ -25,7 +25,7 @@ metadata: spec: parentRefs: - name: gateway - namespace: nginx-gateway + namespace: default sectionName: https hostnames: - "cafe.example.com" From 623f125249b31a4d2d19c3e18d6b12497b75936d Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Tue, 19 Jul 2022 10:36:43 -0600 Subject: [PATCH 27/42] Add gateways to each example --- README.md | 5 ----- examples/advanced-routing/README.md | 6 ++++++ examples/advanced-routing/cafe-routes.yaml | 4 ++-- .../advanced-routing}/gateway.yaml | 11 +---------- examples/cafe-example/README.md | 6 ++++++ examples/cafe-example/cafe-routes.yaml | 6 +++--- examples/cafe-example/gateway.yaml | 13 +++++++++++++ 7 files changed, 31 insertions(+), 20 deletions(-) rename {deploy/manifests => examples/advanced-routing}/gateway.yaml (50%) create mode 100644 examples/cafe-example/gateway.yaml diff --git a/README.md b/README.md index c32e2a2c8c..a643f5d643 100644 --- a/README.md +++ b/README.md @@ -93,11 +93,6 @@ You can deploy NGINX Kubernetes Gateway on an existing Kubernetes 1.16+ cluster. NAME READY STATUS RESTARTS AGE nginx-gateway-5d4f4c7db7-xk2kq 2/2 Running 0 112s ``` -1. Create the Gateway resource: - - ``` - kubectl apply -f deploy/manifests/gateway.yaml - ``` ## Expose NGINX Kubernetes Gateway diff --git a/examples/advanced-routing/README.md b/examples/advanced-routing/README.md index 5547236853..00e7d5433d 100644 --- a/examples/advanced-routing/README.md +++ b/examples/advanced-routing/README.md @@ -46,6 +46,12 @@ The cafe application consists of four services: `coffee-v1-svc`, `coffee-v2-svc` ## 3. Configure Routing +1. Create the `Gateway`: + + ``` + kubectl apply -f gateway.yaml + ``` + 1. Create the `HTTPRoute` resources: ``` diff --git a/examples/advanced-routing/cafe-routes.yaml b/examples/advanced-routing/cafe-routes.yaml index 970a6ce153..63bcd30068 100644 --- a/examples/advanced-routing/cafe-routes.yaml +++ b/examples/advanced-routing/cafe-routes.yaml @@ -5,7 +5,7 @@ metadata: spec: parentRefs: - name: gateway - namespace: nginx-gateway + namespace: default sectionName: http hostnames: - "cafe.example.com" @@ -41,7 +41,7 @@ metadata: spec: parentRefs: - name: gateway - namespace: nginx-gateway + namespace: default sectionName: http hostnames: - "cafe.example.com" diff --git a/deploy/manifests/gateway.yaml b/examples/advanced-routing/gateway.yaml similarity index 50% rename from deploy/manifests/gateway.yaml rename to examples/advanced-routing/gateway.yaml index 7be63e319e..660f3d8f48 100644 --- a/deploy/manifests/gateway.yaml +++ b/examples/advanced-routing/gateway.yaml @@ -2,7 +2,7 @@ apiVersion: gateway.networking.k8s.io/v1alpha2 kind: Gateway metadata: name: gateway - namespace: nginx-gateway + namespace: default labels: domain: k8s-gateway.nginx.org spec: @@ -11,12 +11,3 @@ spec: - name: http port: 80 protocol: HTTP - - name: https - port: 443 - protocol: HTTPS - tls: - mode: Terminate - certificateRefs: - - kind: Secret - name: default-server-secret - namespace: nginx-gateway diff --git a/examples/cafe-example/README.md b/examples/cafe-example/README.md index 66dd360cb5..df2d56bc5f 100644 --- a/examples/cafe-example/README.md +++ b/examples/cafe-example/README.md @@ -39,6 +39,12 @@ In this example we deploy NGINX Kubernetes Gateway, a simple web application, an ## 3. Configure Routing +1. Create the `Gateway`: + + ``` + kubectl apply -f gateway.yaml + ``` + 1. Create the `HTTPRoute` resources: ``` diff --git a/examples/cafe-example/cafe-routes.yaml b/examples/cafe-example/cafe-routes.yaml index a3566d7e20..ee1f4d61c0 100644 --- a/examples/cafe-example/cafe-routes.yaml +++ b/examples/cafe-example/cafe-routes.yaml @@ -5,7 +5,7 @@ metadata: spec: parentRefs: - name: gateway - namespace: nginx-gateway + namespace: default sectionName: http hostnames: - "cafe.example.com" @@ -21,7 +21,7 @@ metadata: spec: parentRefs: - name: gateway - namespace: nginx-gateway + namespace: default sectionName: http hostnames: - "cafe.example.com" @@ -41,7 +41,7 @@ metadata: spec: parentRefs: - name: gateway - namespace: nginx-gateway + namespace: default sectionName: http hostnames: - "cafe.example.com" diff --git a/examples/cafe-example/gateway.yaml b/examples/cafe-example/gateway.yaml new file mode 100644 index 0000000000..660f3d8f48 --- /dev/null +++ b/examples/cafe-example/gateway.yaml @@ -0,0 +1,13 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: Gateway +metadata: + name: gateway + namespace: default + labels: + domain: k8s-gateway.nginx.org +spec: + gatewayClassName: nginx + listeners: + - name: http + port: 80 + protocol: HTTP From f2b6cbe83a47ea0b1b31ff90ed5417049b75d37a Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Tue, 19 Jul 2022 10:53:11 -0600 Subject: [PATCH 28/42] Standardize on implementation package and add test suite files --- .../gateway/implementation_suit_test.go | 4 ++-- .../gatewayclass/implementation_suit_test.go | 4 ++-- .../implementations/gatewayconfig/gatewayconfig.go | 2 +- .../secret/implementation_suit_test.go | 13 +++++++++++++ internal/implementations/secret/secret.go | 2 +- internal/implementations/secret/secret_test.go | 2 +- internal/implementations/service/service.go | 2 +- internal/manager/manager.go | 2 +- 8 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 internal/implementations/secret/implementation_suit_test.go diff --git a/internal/implementations/gateway/implementation_suit_test.go b/internal/implementations/gateway/implementation_suit_test.go index 7c9f572a55..dffdfe85a9 100644 --- a/internal/implementations/gateway/implementation_suit_test.go +++ b/internal/implementations/gateway/implementation_suit_test.go @@ -7,7 +7,7 @@ import ( . "github.com/onsi/gomega" ) -func TestImplementation(t *testing.T) { +func TestGatewayImplementation(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Implementation Suite") + RunSpecs(t, "Gateway Implementation Suite") } diff --git a/internal/implementations/gatewayclass/implementation_suit_test.go b/internal/implementations/gatewayclass/implementation_suit_test.go index 5bba5d8086..a6f600b94c 100644 --- a/internal/implementations/gatewayclass/implementation_suit_test.go +++ b/internal/implementations/gatewayclass/implementation_suit_test.go @@ -7,7 +7,7 @@ import ( . "github.com/onsi/gomega" ) -func TestState(t *testing.T) { +func TestGatewayClassImplementation(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Implementation Suite") + RunSpecs(t, "Gateway Class Implementation Suite") } diff --git a/internal/implementations/gatewayconfig/gatewayconfig.go b/internal/implementations/gatewayconfig/gatewayconfig.go index f5d746a6e8..29d5bfaaeb 100644 --- a/internal/implementations/gatewayconfig/gatewayconfig.go +++ b/internal/implementations/gatewayconfig/gatewayconfig.go @@ -1,4 +1,4 @@ -package gatewayconfig +package implementation import ( "github.com/go-logr/logr" diff --git a/internal/implementations/secret/implementation_suit_test.go b/internal/implementations/secret/implementation_suit_test.go new file mode 100644 index 0000000000..bfa87e8dfa --- /dev/null +++ b/internal/implementations/secret/implementation_suit_test.go @@ -0,0 +1,13 @@ +package implementation_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestSecretImplementation(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Secret Implementation Suite") +} diff --git a/internal/implementations/secret/secret.go b/internal/implementations/secret/secret.go index 7126899d13..d3fbc9f285 100644 --- a/internal/implementations/secret/secret.go +++ b/internal/implementations/secret/secret.go @@ -1,4 +1,4 @@ -package secret +package implementation import ( "github.com/go-logr/logr" diff --git a/internal/implementations/secret/secret_test.go b/internal/implementations/secret/secret_test.go index 93860646df..8d0fc8dfd0 100644 --- a/internal/implementations/secret/secret_test.go +++ b/internal/implementations/secret/secret_test.go @@ -1,4 +1,4 @@ -package secret_test +package implementation_test import ( . "github.com/onsi/ginkgo/v2" diff --git a/internal/implementations/service/service.go b/internal/implementations/service/service.go index 37cc4b3a2f..a04cb12761 100644 --- a/internal/implementations/service/service.go +++ b/internal/implementations/service/service.go @@ -1,4 +1,4 @@ -package service +package implementation import ( "github.com/go-logr/logr" diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 140a203a08..22d2603e11 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -15,7 +15,7 @@ import ( gw "github.com/nginxinc/nginx-kubernetes-gateway/internal/implementations/gateway" gc "github.com/nginxinc/nginx-kubernetes-gateway/internal/implementations/gatewayclass" hr "github.com/nginxinc/nginx-kubernetes-gateway/internal/implementations/httproute" - "github.com/nginxinc/nginx-kubernetes-gateway/internal/implementations/secret" + secret "github.com/nginxinc/nginx-kubernetes-gateway/internal/implementations/secret" svc "github.com/nginxinc/nginx-kubernetes-gateway/internal/implementations/service" ngxcfg "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/config" "github.com/nginxinc/nginx-kubernetes-gateway/internal/nginx/file" From 1405fc0092e5d8e407e6d1538dd06672821e50c2 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Tue, 19 Jul 2022 11:07:58 -0600 Subject: [PATCH 29/42] Fix linting error --- internal/status/updater_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/status/updater_test.go b/internal/status/updater_test.go index 7a356859dd..dec47c99a5 100644 --- a/internal/status/updater_test.go +++ b/internal/status/updater_test.go @@ -118,7 +118,7 @@ var _ = Describe("Updater", func() { Status: status, ObservedGeneration: generation, LastTransitionTime: fakeClockTime, - Reason: string(v1alpha2.GatewayClassReasonAccepted), + Reason: reason, Message: msg, }, }, From 97a49212776151ffbabb79684aa4e4118a027e6f Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Tue, 19 Jul 2022 12:18:34 -0600 Subject: [PATCH 30/42] Fix some typos --- examples/https-termination/README.md | 6 +++--- ...ementation_suit_test.go => implementation_suite_test.go} | 0 ...ementation_suit_test.go => implementation_suite_test.go} | 0 ...ementation_suit_test.go => implementation_suite_test.go} | 0 internal/nginx/config/generator.go | 4 ++-- internal/state/configuration.go | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) rename internal/implementations/gateway/{implementation_suit_test.go => implementation_suite_test.go} (100%) rename internal/implementations/gatewayclass/{implementation_suit_test.go => implementation_suite_test.go} (100%) rename internal/implementations/secret/{implementation_suit_test.go => implementation_suite_test.go} (100%) diff --git a/examples/https-termination/README.md b/examples/https-termination/README.md index bc3f6dac0e..fc0270a42c 100644 --- a/examples/https-termination/README.md +++ b/examples/https-termination/README.md @@ -44,7 +44,7 @@ In this example we expand on the simple [cafe-example](../cafe-example) by addin kubectl apply -f cafe-secret.yaml ``` - The TLS certificate and key in this secret which be used to terminate the TLS connections for the cafe application. + The TLS certificate and key in this secret are used to terminate the TLS connections for the cafe application. **Important**: This certificate and key are for demo purposes only. 1. Create the `Gateway` resource: @@ -64,8 +64,8 @@ In this example we expand on the simple [cafe-example](../cafe-example) by addin ```yaml parentRefs: - name: gateway - namespace: nginx-gateway - sectionName: https + namespace: default + sectionName: https ``` ## 4. Test the Application diff --git a/internal/implementations/gateway/implementation_suit_test.go b/internal/implementations/gateway/implementation_suite_test.go similarity index 100% rename from internal/implementations/gateway/implementation_suit_test.go rename to internal/implementations/gateway/implementation_suite_test.go diff --git a/internal/implementations/gatewayclass/implementation_suit_test.go b/internal/implementations/gatewayclass/implementation_suite_test.go similarity index 100% rename from internal/implementations/gatewayclass/implementation_suit_test.go rename to internal/implementations/gatewayclass/implementation_suite_test.go diff --git a/internal/implementations/secret/implementation_suit_test.go b/internal/implementations/secret/implementation_suite_test.go similarity index 100% rename from internal/implementations/secret/implementation_suit_test.go rename to internal/implementations/secret/implementation_suite_test.go diff --git a/internal/nginx/config/generator.go b/internal/nginx/config/generator.go index 1d313659f7..54f887cfe7 100644 --- a/internal/nginx/config/generator.go +++ b/internal/nginx/config/generator.go @@ -54,9 +54,9 @@ func (g *GeneratorImpl) Generate(conf state.Configuration) ([]byte, Warnings) { } if len(conf.SSLServers) > 0 { - defaultTLSTerminationServer := generateDefaultSSLServer() + defaultSSLServer := generateDefaultSSLServer() - servers.Servers = append(servers.Servers, defaultTLSTerminationServer) + servers.Servers = append(servers.Servers, defaultSSLServer) } for _, s := range confServers { diff --git a/internal/state/configuration.go b/internal/state/configuration.go index be37523316..034909a9d5 100644 --- a/internal/state/configuration.go +++ b/internal/state/configuration.go @@ -89,8 +89,8 @@ type configBuilder struct { func newConfigBuilder() *configBuilder { return &configBuilder{ - http: newHTTPServerBuilder(), - ssl: newHTTPServerBuilder(), + http: newVirtualServerBuilder(), + ssl: newVirtualServerBuilder(), } } @@ -117,7 +117,7 @@ type virtualServerBuilder struct { listenersForHost map[string]*listener } -func newHTTPServerBuilder() *virtualServerBuilder { +func newVirtualServerBuilder() *virtualServerBuilder { return &virtualServerBuilder{ rulesPerHost: make(map[string]map[string]PathRule), listenersForHost: make(map[string]*listener), From 02fde36b1066dbee5cdd7548276c1ff06059c46b Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 21 Jul 2022 09:28:55 -0700 Subject: [PATCH 31/42] Add FileManager interface to unit test file i/o --- internal/state/file_manager.go | 35 ++ internal/state/secrets.go | 53 ++- internal/state/secrets_test.go | 70 ++- internal/state/statefakes/fake_file_info.go | 427 +++++++++++++++++ .../state/statefakes/fake_file_manager.go | 428 ++++++++++++++++++ 5 files changed, 1003 insertions(+), 10 deletions(-) create mode 100644 internal/state/file_manager.go create mode 100644 internal/state/statefakes/fake_file_info.go create mode 100644 internal/state/statefakes/fake_file_manager.go diff --git a/internal/state/file_manager.go b/internal/state/file_manager.go new file mode 100644 index 0000000000..c6aaaf0b8e --- /dev/null +++ b/internal/state/file_manager.go @@ -0,0 +1,35 @@ +package state + +import ( + "io/fs" + "io/ioutil" + "os" +) + +type stdLibFileManager struct{} + +func newStdLibFileManager() *stdLibFileManager { + return &stdLibFileManager{} +} + +func (s *stdLibFileManager) ReadDir(dirname string) ([]fs.FileInfo, error) { + return ioutil.ReadDir(dirname) +} + +func (s *stdLibFileManager) Remove(name string) error { + return os.Remove(name) +} + +func (s *stdLibFileManager) Write(file *os.File, contents []byte) error { + _, err := file.Write(contents) + + return err +} + +func (s *stdLibFileManager) Create(name string) (*os.File, error) { + return os.Create(name) +} + +func (s *stdLibFileManager) Chmod(file *os.File, mode os.FileMode) error { + return file.Chmod(mode) +} diff --git a/internal/state/secrets.go b/internal/state/secrets.go index 9c54f80ba5..3bea4cd3b6 100644 --- a/internal/state/secrets.go +++ b/internal/state/secrets.go @@ -4,7 +4,7 @@ import ( "bytes" "crypto/tls" "fmt" - "io/ioutil" + "io/fs" "os" "path" @@ -14,6 +14,8 @@ import ( //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . SecretStore //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . SecretDiskMemoryManager +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . FileManager +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 io/fs.FileInfo // tlsSecretFileMode defines the default file mode for files with TLS Secrets. const tlsSecretFileMode = 0o600 @@ -72,9 +74,25 @@ type SecretDiskMemoryManager interface { WriteAllRequestedSecrets() error } +// FileManager is an interface that exposes File I/O operations. +// Used for unit testing. +type FileManager interface { + // ReadDir returns the file info for the directory. + ReadDir(dirname string) ([]fs.FileInfo, error) + // Remove file with given name. + Remove(name string) error + // Create file at the provided filepath. + Create(name string) (*os.File, error) + // Chmod sets the mode of the file. + Chmod(file *os.File, mode os.FileMode) error + // Write writes contents to the file. + Write(file *os.File, contents []byte) error +} + type SecretDiskMemoryManagerImpl struct { requestedSecrets map[types.NamespacedName]requestedSecret secretStore SecretStore + fileManager FileManager secretDirectory string } @@ -83,12 +101,30 @@ type requestedSecret struct { path string } -func NewSecretDiskMemoryManager(secretDirectory string, secretStore SecretStore) *SecretDiskMemoryManagerImpl { - return &SecretDiskMemoryManagerImpl{ +// SecretDiskMemoryManagerOption is a function that modifies the configuration of the SecretDiskMemoryManager. +type SecretDiskMemoryManagerOption func(*SecretDiskMemoryManagerImpl) + +// WithSecretFileManager sets the file manager of the SecretDiskMemoryManager. +// Used to inject a fake fileManager for unit tests. +func WithSecretFileManager(fileManager FileManager) SecretDiskMemoryManagerOption { + return func(mm *SecretDiskMemoryManagerImpl) { + mm.fileManager = fileManager + } +} + +func NewSecretDiskMemoryManager(secretDirectory string, secretStore SecretStore, options ...SecretDiskMemoryManagerOption) *SecretDiskMemoryManagerImpl { + sm := &SecretDiskMemoryManagerImpl{ requestedSecrets: make(map[types.NamespacedName]requestedSecret), secretStore: secretStore, secretDirectory: secretDirectory, + fileManager: newStdLibFileManager(), + } + + for _, o := range options { + o(sm) } + + return sm } func (s *SecretDiskMemoryManagerImpl) Request(nsname types.NamespacedName) (string, error) { @@ -113,14 +149,14 @@ func (s *SecretDiskMemoryManagerImpl) Request(nsname types.NamespacedName) (stri func (s *SecretDiskMemoryManagerImpl) WriteAllRequestedSecrets() error { // Remove all existing secrets from secrets directory - dir, err := ioutil.ReadDir(s.secretDirectory) + dir, err := s.fileManager.ReadDir(s.secretDirectory) if err != nil { return fmt.Errorf("failed to remove all secrets from %s: %w", s.secretDirectory, err) } for _, d := range dir { filepath := path.Join(s.secretDirectory, d.Name()) - if err := os.Remove(filepath); err != nil { + if err := s.fileManager.Remove(filepath); err != nil { return fmt.Errorf("failed to remove secret %s: %w", filepath, err) } } @@ -128,22 +164,21 @@ func (s *SecretDiskMemoryManagerImpl) WriteAllRequestedSecrets() error { // Write all secrets to secrets directory for nsname, ss := range s.requestedSecrets { - file, err := os.Create(ss.path) + file, err := s.fileManager.Create(ss.path) if err != nil { return fmt.Errorf("failed to create file %s for secret %s: %w", ss.path, nsname, err) } - if err = file.Chmod(tlsSecretFileMode); err != nil { + if err = s.fileManager.Chmod(file, tlsSecretFileMode); err != nil { return fmt.Errorf("failed to change mode of file %s for secret %s: %w", ss.path, nsname, err) } contents := generateCertAndKeyFileContent(ss.secret) - _, err = file.Write(contents) + err = s.fileManager.Write(file, contents) if err != nil { return fmt.Errorf("failed to write secret %s to file %s: %w", nsname, ss.path, err) } - } // reset stored secrets diff --git a/internal/state/secrets_test.go b/internal/state/secrets_test.go index 00ed7bb121..f5c912cd60 100644 --- a/internal/state/secrets_test.go +++ b/internal/state/secrets_test.go @@ -2,6 +2,8 @@ package state_test import ( + "errors" + "io/fs" "io/ioutil" "os" "path" @@ -257,7 +259,6 @@ var _ = Describe("SecretStore", func() { validToInvalidSecret.Data[apiv1.TLSCertKey] = invalidCert }) - Describe("handles CRUD events on secrets", Ordered, func() { testUpsert := func(s *apiv1.Secret, valid bool) { store.Upsert(s) @@ -330,4 +331,71 @@ var _ = Describe("SecretStore", func() { store.Delete(nsname) }) }) + Describe("File Management Error Cases", func() { + var ( + fakeFileManager *statefakes.FakeFileManager + fakeStore *statefakes.FakeSecretStore + fakeFileInfoSlice []fs.FileInfo + memMgr state.SecretDiskMemoryManager + ) + + BeforeEach(OncePerOrdered, func() { + fakeFileManager = &statefakes.FakeFileManager{} + fakeStore = &statefakes.FakeSecretStore{} + fakeFileInfoSlice = []fs.FileInfo{&statefakes.FakeFileInfo{}} + memMgr = state.NewSecretDiskMemoryManager("", fakeStore, state.WithSecretFileManager(fakeFileManager)) + + // populate a requested secret + fakeStore.GetReturns(&state.Secret{Secret: secret1, Valid: true}) + _, err := memMgr.Request(types.NamespacedName{Namespace: secret1.Namespace, Name: secret1.Name}) + Expect(err).ToNot(HaveOccurred()) + }) + + Describe("Write all requested secrets", Ordered, func() { + It("returns an error when secret directory cannot be read from", func() { + errString := "read dir error" + fakeFileManager.ReadDirReturns(nil, errors.New(errString)) + + err := memMgr.WriteAllRequestedSecrets() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errString)) + }) + It("returns an error when a file cannot be removed from the secrets directory", func() { + errString := "remove error" + fakeFileManager.ReadDirReturns(fakeFileInfoSlice, nil) + fakeFileManager.RemoveReturns(errors.New(errString)) + + err := memMgr.WriteAllRequestedSecrets() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errString)) + }) + It("returns an error when file cannot be created", func() { + errString := "create error" + fakeFileManager.RemoveReturns(nil) + fakeFileManager.CreateReturns(nil, errors.New(errString)) + + err := memMgr.WriteAllRequestedSecrets() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errString)) + }) + It("returns an error when it cannot change the mode of the file", func() { + errStr := "chmod error" + fakeFileManager.CreateReturns(&os.File{}, nil) + fakeFileManager.ChmodReturns(errors.New(errStr)) + + err := memMgr.WriteAllRequestedSecrets() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errStr)) + }) + It("returns an error when file cannot be written to", func() { + errString := "write error" + fakeFileManager.ChmodReturns(nil) + fakeFileManager.WriteReturns(errors.New(errString)) + + err := memMgr.WriteAllRequestedSecrets() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errString)) + }) + }) + }) }) diff --git a/internal/state/statefakes/fake_file_info.go b/internal/state/statefakes/fake_file_info.go new file mode 100644 index 0000000000..e7be531dc8 --- /dev/null +++ b/internal/state/statefakes/fake_file_info.go @@ -0,0 +1,427 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package statefakes + +import ( + "io/fs" + "sync" + "time" +) + +type FakeFileInfo struct { + IsDirStub func() bool + isDirMutex sync.RWMutex + isDirArgsForCall []struct { + } + isDirReturns struct { + result1 bool + } + isDirReturnsOnCall map[int]struct { + result1 bool + } + ModTimeStub func() time.Time + modTimeMutex sync.RWMutex + modTimeArgsForCall []struct { + } + modTimeReturns struct { + result1 time.Time + } + modTimeReturnsOnCall map[int]struct { + result1 time.Time + } + ModeStub func() fs.FileMode + modeMutex sync.RWMutex + modeArgsForCall []struct { + } + modeReturns struct { + result1 fs.FileMode + } + modeReturnsOnCall map[int]struct { + result1 fs.FileMode + } + NameStub func() string + nameMutex sync.RWMutex + nameArgsForCall []struct { + } + nameReturns struct { + result1 string + } + nameReturnsOnCall map[int]struct { + result1 string + } + SizeStub func() int64 + sizeMutex sync.RWMutex + sizeArgsForCall []struct { + } + sizeReturns struct { + result1 int64 + } + sizeReturnsOnCall map[int]struct { + result1 int64 + } + SysStub func() any + sysMutex sync.RWMutex + sysArgsForCall []struct { + } + sysReturns struct { + result1 any + } + sysReturnsOnCall map[int]struct { + result1 any + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeFileInfo) IsDir() bool { + fake.isDirMutex.Lock() + ret, specificReturn := fake.isDirReturnsOnCall[len(fake.isDirArgsForCall)] + fake.isDirArgsForCall = append(fake.isDirArgsForCall, struct { + }{}) + stub := fake.IsDirStub + fakeReturns := fake.isDirReturns + fake.recordInvocation("IsDir", []interface{}{}) + fake.isDirMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileInfo) IsDirCallCount() int { + fake.isDirMutex.RLock() + defer fake.isDirMutex.RUnlock() + return len(fake.isDirArgsForCall) +} + +func (fake *FakeFileInfo) IsDirCalls(stub func() bool) { + fake.isDirMutex.Lock() + defer fake.isDirMutex.Unlock() + fake.IsDirStub = stub +} + +func (fake *FakeFileInfo) IsDirReturns(result1 bool) { + fake.isDirMutex.Lock() + defer fake.isDirMutex.Unlock() + fake.IsDirStub = nil + fake.isDirReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeFileInfo) IsDirReturnsOnCall(i int, result1 bool) { + fake.isDirMutex.Lock() + defer fake.isDirMutex.Unlock() + fake.IsDirStub = nil + if fake.isDirReturnsOnCall == nil { + fake.isDirReturnsOnCall = make(map[int]struct { + result1 bool + }) + } + fake.isDirReturnsOnCall[i] = struct { + result1 bool + }{result1} +} + +func (fake *FakeFileInfo) ModTime() time.Time { + fake.modTimeMutex.Lock() + ret, specificReturn := fake.modTimeReturnsOnCall[len(fake.modTimeArgsForCall)] + fake.modTimeArgsForCall = append(fake.modTimeArgsForCall, struct { + }{}) + stub := fake.ModTimeStub + fakeReturns := fake.modTimeReturns + fake.recordInvocation("ModTime", []interface{}{}) + fake.modTimeMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileInfo) ModTimeCallCount() int { + fake.modTimeMutex.RLock() + defer fake.modTimeMutex.RUnlock() + return len(fake.modTimeArgsForCall) +} + +func (fake *FakeFileInfo) ModTimeCalls(stub func() time.Time) { + fake.modTimeMutex.Lock() + defer fake.modTimeMutex.Unlock() + fake.ModTimeStub = stub +} + +func (fake *FakeFileInfo) ModTimeReturns(result1 time.Time) { + fake.modTimeMutex.Lock() + defer fake.modTimeMutex.Unlock() + fake.ModTimeStub = nil + fake.modTimeReturns = struct { + result1 time.Time + }{result1} +} + +func (fake *FakeFileInfo) ModTimeReturnsOnCall(i int, result1 time.Time) { + fake.modTimeMutex.Lock() + defer fake.modTimeMutex.Unlock() + fake.ModTimeStub = nil + if fake.modTimeReturnsOnCall == nil { + fake.modTimeReturnsOnCall = make(map[int]struct { + result1 time.Time + }) + } + fake.modTimeReturnsOnCall[i] = struct { + result1 time.Time + }{result1} +} + +func (fake *FakeFileInfo) Mode() fs.FileMode { + fake.modeMutex.Lock() + ret, specificReturn := fake.modeReturnsOnCall[len(fake.modeArgsForCall)] + fake.modeArgsForCall = append(fake.modeArgsForCall, struct { + }{}) + stub := fake.ModeStub + fakeReturns := fake.modeReturns + fake.recordInvocation("Mode", []interface{}{}) + fake.modeMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileInfo) ModeCallCount() int { + fake.modeMutex.RLock() + defer fake.modeMutex.RUnlock() + return len(fake.modeArgsForCall) +} + +func (fake *FakeFileInfo) ModeCalls(stub func() fs.FileMode) { + fake.modeMutex.Lock() + defer fake.modeMutex.Unlock() + fake.ModeStub = stub +} + +func (fake *FakeFileInfo) ModeReturns(result1 fs.FileMode) { + fake.modeMutex.Lock() + defer fake.modeMutex.Unlock() + fake.ModeStub = nil + fake.modeReturns = struct { + result1 fs.FileMode + }{result1} +} + +func (fake *FakeFileInfo) ModeReturnsOnCall(i int, result1 fs.FileMode) { + fake.modeMutex.Lock() + defer fake.modeMutex.Unlock() + fake.ModeStub = nil + if fake.modeReturnsOnCall == nil { + fake.modeReturnsOnCall = make(map[int]struct { + result1 fs.FileMode + }) + } + fake.modeReturnsOnCall[i] = struct { + result1 fs.FileMode + }{result1} +} + +func (fake *FakeFileInfo) Name() string { + fake.nameMutex.Lock() + ret, specificReturn := fake.nameReturnsOnCall[len(fake.nameArgsForCall)] + fake.nameArgsForCall = append(fake.nameArgsForCall, struct { + }{}) + stub := fake.NameStub + fakeReturns := fake.nameReturns + fake.recordInvocation("Name", []interface{}{}) + fake.nameMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileInfo) NameCallCount() int { + fake.nameMutex.RLock() + defer fake.nameMutex.RUnlock() + return len(fake.nameArgsForCall) +} + +func (fake *FakeFileInfo) NameCalls(stub func() string) { + fake.nameMutex.Lock() + defer fake.nameMutex.Unlock() + fake.NameStub = stub +} + +func (fake *FakeFileInfo) NameReturns(result1 string) { + fake.nameMutex.Lock() + defer fake.nameMutex.Unlock() + fake.NameStub = nil + fake.nameReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeFileInfo) NameReturnsOnCall(i int, result1 string) { + fake.nameMutex.Lock() + defer fake.nameMutex.Unlock() + fake.NameStub = nil + if fake.nameReturnsOnCall == nil { + fake.nameReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.nameReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakeFileInfo) Size() int64 { + fake.sizeMutex.Lock() + ret, specificReturn := fake.sizeReturnsOnCall[len(fake.sizeArgsForCall)] + fake.sizeArgsForCall = append(fake.sizeArgsForCall, struct { + }{}) + stub := fake.SizeStub + fakeReturns := fake.sizeReturns + fake.recordInvocation("Size", []interface{}{}) + fake.sizeMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileInfo) SizeCallCount() int { + fake.sizeMutex.RLock() + defer fake.sizeMutex.RUnlock() + return len(fake.sizeArgsForCall) +} + +func (fake *FakeFileInfo) SizeCalls(stub func() int64) { + fake.sizeMutex.Lock() + defer fake.sizeMutex.Unlock() + fake.SizeStub = stub +} + +func (fake *FakeFileInfo) SizeReturns(result1 int64) { + fake.sizeMutex.Lock() + defer fake.sizeMutex.Unlock() + fake.SizeStub = nil + fake.sizeReturns = struct { + result1 int64 + }{result1} +} + +func (fake *FakeFileInfo) SizeReturnsOnCall(i int, result1 int64) { + fake.sizeMutex.Lock() + defer fake.sizeMutex.Unlock() + fake.SizeStub = nil + if fake.sizeReturnsOnCall == nil { + fake.sizeReturnsOnCall = make(map[int]struct { + result1 int64 + }) + } + fake.sizeReturnsOnCall[i] = struct { + result1 int64 + }{result1} +} + +func (fake *FakeFileInfo) Sys() any { + fake.sysMutex.Lock() + ret, specificReturn := fake.sysReturnsOnCall[len(fake.sysArgsForCall)] + fake.sysArgsForCall = append(fake.sysArgsForCall, struct { + }{}) + stub := fake.SysStub + fakeReturns := fake.sysReturns + fake.recordInvocation("Sys", []interface{}{}) + fake.sysMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileInfo) SysCallCount() int { + fake.sysMutex.RLock() + defer fake.sysMutex.RUnlock() + return len(fake.sysArgsForCall) +} + +func (fake *FakeFileInfo) SysCalls(stub func() any) { + fake.sysMutex.Lock() + defer fake.sysMutex.Unlock() + fake.SysStub = stub +} + +func (fake *FakeFileInfo) SysReturns(result1 any) { + fake.sysMutex.Lock() + defer fake.sysMutex.Unlock() + fake.SysStub = nil + fake.sysReturns = struct { + result1 any + }{result1} +} + +func (fake *FakeFileInfo) SysReturnsOnCall(i int, result1 any) { + fake.sysMutex.Lock() + defer fake.sysMutex.Unlock() + fake.SysStub = nil + if fake.sysReturnsOnCall == nil { + fake.sysReturnsOnCall = make(map[int]struct { + result1 any + }) + } + fake.sysReturnsOnCall[i] = struct { + result1 any + }{result1} +} + +func (fake *FakeFileInfo) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.isDirMutex.RLock() + defer fake.isDirMutex.RUnlock() + fake.modTimeMutex.RLock() + defer fake.modTimeMutex.RUnlock() + fake.modeMutex.RLock() + defer fake.modeMutex.RUnlock() + fake.nameMutex.RLock() + defer fake.nameMutex.RUnlock() + fake.sizeMutex.RLock() + defer fake.sizeMutex.RUnlock() + fake.sysMutex.RLock() + defer fake.sysMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeFileInfo) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ fs.FileInfo = new(FakeFileInfo) diff --git a/internal/state/statefakes/fake_file_manager.go b/internal/state/statefakes/fake_file_manager.go new file mode 100644 index 0000000000..f36e690ae4 --- /dev/null +++ b/internal/state/statefakes/fake_file_manager.go @@ -0,0 +1,428 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package statefakes + +import ( + "io/fs" + "os" + "sync" + + "github.com/nginxinc/nginx-kubernetes-gateway/internal/state" +) + +type FakeFileManager struct { + ChmodStub func(*os.File, fs.FileMode) error + chmodMutex sync.RWMutex + chmodArgsForCall []struct { + arg1 *os.File + arg2 fs.FileMode + } + chmodReturns struct { + result1 error + } + chmodReturnsOnCall map[int]struct { + result1 error + } + CreateStub func(string) (*os.File, error) + createMutex sync.RWMutex + createArgsForCall []struct { + arg1 string + } + createReturns struct { + result1 *os.File + result2 error + } + createReturnsOnCall map[int]struct { + result1 *os.File + result2 error + } + ReadDirStub func(string) ([]fs.FileInfo, error) + readDirMutex sync.RWMutex + readDirArgsForCall []struct { + arg1 string + } + readDirReturns struct { + result1 []fs.FileInfo + result2 error + } + readDirReturnsOnCall map[int]struct { + result1 []fs.FileInfo + result2 error + } + RemoveStub func(string) error + removeMutex sync.RWMutex + removeArgsForCall []struct { + arg1 string + } + removeReturns struct { + result1 error + } + removeReturnsOnCall map[int]struct { + result1 error + } + WriteStub func(*os.File, []byte) error + writeMutex sync.RWMutex + writeArgsForCall []struct { + arg1 *os.File + arg2 []byte + } + writeReturns struct { + result1 error + } + writeReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeFileManager) Chmod(arg1 *os.File, arg2 fs.FileMode) error { + fake.chmodMutex.Lock() + ret, specificReturn := fake.chmodReturnsOnCall[len(fake.chmodArgsForCall)] + fake.chmodArgsForCall = append(fake.chmodArgsForCall, struct { + arg1 *os.File + arg2 fs.FileMode + }{arg1, arg2}) + stub := fake.ChmodStub + fakeReturns := fake.chmodReturns + fake.recordInvocation("Chmod", []interface{}{arg1, arg2}) + fake.chmodMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileManager) ChmodCallCount() int { + fake.chmodMutex.RLock() + defer fake.chmodMutex.RUnlock() + return len(fake.chmodArgsForCall) +} + +func (fake *FakeFileManager) ChmodCalls(stub func(*os.File, fs.FileMode) error) { + fake.chmodMutex.Lock() + defer fake.chmodMutex.Unlock() + fake.ChmodStub = stub +} + +func (fake *FakeFileManager) ChmodArgsForCall(i int) (*os.File, fs.FileMode) { + fake.chmodMutex.RLock() + defer fake.chmodMutex.RUnlock() + argsForCall := fake.chmodArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeFileManager) ChmodReturns(result1 error) { + fake.chmodMutex.Lock() + defer fake.chmodMutex.Unlock() + fake.ChmodStub = nil + fake.chmodReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeFileManager) ChmodReturnsOnCall(i int, result1 error) { + fake.chmodMutex.Lock() + defer fake.chmodMutex.Unlock() + fake.ChmodStub = nil + if fake.chmodReturnsOnCall == nil { + fake.chmodReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.chmodReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeFileManager) Create(arg1 string) (*os.File, error) { + fake.createMutex.Lock() + ret, specificReturn := fake.createReturnsOnCall[len(fake.createArgsForCall)] + fake.createArgsForCall = append(fake.createArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.CreateStub + fakeReturns := fake.createReturns + fake.recordInvocation("Create", []interface{}{arg1}) + fake.createMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeFileManager) CreateCallCount() int { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + return len(fake.createArgsForCall) +} + +func (fake *FakeFileManager) CreateCalls(stub func(string) (*os.File, error)) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = stub +} + +func (fake *FakeFileManager) CreateArgsForCall(i int) string { + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + argsForCall := fake.createArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeFileManager) CreateReturns(result1 *os.File, result2 error) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = nil + fake.createReturns = struct { + result1 *os.File + result2 error + }{result1, result2} +} + +func (fake *FakeFileManager) CreateReturnsOnCall(i int, result1 *os.File, result2 error) { + fake.createMutex.Lock() + defer fake.createMutex.Unlock() + fake.CreateStub = nil + if fake.createReturnsOnCall == nil { + fake.createReturnsOnCall = make(map[int]struct { + result1 *os.File + result2 error + }) + } + fake.createReturnsOnCall[i] = struct { + result1 *os.File + result2 error + }{result1, result2} +} + +func (fake *FakeFileManager) ReadDir(arg1 string) ([]fs.FileInfo, error) { + fake.readDirMutex.Lock() + ret, specificReturn := fake.readDirReturnsOnCall[len(fake.readDirArgsForCall)] + fake.readDirArgsForCall = append(fake.readDirArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.ReadDirStub + fakeReturns := fake.readDirReturns + fake.recordInvocation("ReadDir", []interface{}{arg1}) + fake.readDirMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeFileManager) ReadDirCallCount() int { + fake.readDirMutex.RLock() + defer fake.readDirMutex.RUnlock() + return len(fake.readDirArgsForCall) +} + +func (fake *FakeFileManager) ReadDirCalls(stub func(string) ([]fs.FileInfo, error)) { + fake.readDirMutex.Lock() + defer fake.readDirMutex.Unlock() + fake.ReadDirStub = stub +} + +func (fake *FakeFileManager) ReadDirArgsForCall(i int) string { + fake.readDirMutex.RLock() + defer fake.readDirMutex.RUnlock() + argsForCall := fake.readDirArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeFileManager) ReadDirReturns(result1 []fs.FileInfo, result2 error) { + fake.readDirMutex.Lock() + defer fake.readDirMutex.Unlock() + fake.ReadDirStub = nil + fake.readDirReturns = struct { + result1 []fs.FileInfo + result2 error + }{result1, result2} +} + +func (fake *FakeFileManager) ReadDirReturnsOnCall(i int, result1 []fs.FileInfo, result2 error) { + fake.readDirMutex.Lock() + defer fake.readDirMutex.Unlock() + fake.ReadDirStub = nil + if fake.readDirReturnsOnCall == nil { + fake.readDirReturnsOnCall = make(map[int]struct { + result1 []fs.FileInfo + result2 error + }) + } + fake.readDirReturnsOnCall[i] = struct { + result1 []fs.FileInfo + result2 error + }{result1, result2} +} + +func (fake *FakeFileManager) Remove(arg1 string) error { + fake.removeMutex.Lock() + ret, specificReturn := fake.removeReturnsOnCall[len(fake.removeArgsForCall)] + fake.removeArgsForCall = append(fake.removeArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.RemoveStub + fakeReturns := fake.removeReturns + fake.recordInvocation("Remove", []interface{}{arg1}) + fake.removeMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileManager) RemoveCallCount() int { + fake.removeMutex.RLock() + defer fake.removeMutex.RUnlock() + return len(fake.removeArgsForCall) +} + +func (fake *FakeFileManager) RemoveCalls(stub func(string) error) { + fake.removeMutex.Lock() + defer fake.removeMutex.Unlock() + fake.RemoveStub = stub +} + +func (fake *FakeFileManager) RemoveArgsForCall(i int) string { + fake.removeMutex.RLock() + defer fake.removeMutex.RUnlock() + argsForCall := fake.removeArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeFileManager) RemoveReturns(result1 error) { + fake.removeMutex.Lock() + defer fake.removeMutex.Unlock() + fake.RemoveStub = nil + fake.removeReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeFileManager) RemoveReturnsOnCall(i int, result1 error) { + fake.removeMutex.Lock() + defer fake.removeMutex.Unlock() + fake.RemoveStub = nil + if fake.removeReturnsOnCall == nil { + fake.removeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.removeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeFileManager) Write(arg1 *os.File, arg2 []byte) error { + var arg2Copy []byte + if arg2 != nil { + arg2Copy = make([]byte, len(arg2)) + copy(arg2Copy, arg2) + } + fake.writeMutex.Lock() + ret, specificReturn := fake.writeReturnsOnCall[len(fake.writeArgsForCall)] + fake.writeArgsForCall = append(fake.writeArgsForCall, struct { + arg1 *os.File + arg2 []byte + }{arg1, arg2Copy}) + stub := fake.WriteStub + fakeReturns := fake.writeReturns + fake.recordInvocation("Write", []interface{}{arg1, arg2Copy}) + fake.writeMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeFileManager) WriteCallCount() int { + fake.writeMutex.RLock() + defer fake.writeMutex.RUnlock() + return len(fake.writeArgsForCall) +} + +func (fake *FakeFileManager) WriteCalls(stub func(*os.File, []byte) error) { + fake.writeMutex.Lock() + defer fake.writeMutex.Unlock() + fake.WriteStub = stub +} + +func (fake *FakeFileManager) WriteArgsForCall(i int) (*os.File, []byte) { + fake.writeMutex.RLock() + defer fake.writeMutex.RUnlock() + argsForCall := fake.writeArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeFileManager) WriteReturns(result1 error) { + fake.writeMutex.Lock() + defer fake.writeMutex.Unlock() + fake.WriteStub = nil + fake.writeReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeFileManager) WriteReturnsOnCall(i int, result1 error) { + fake.writeMutex.Lock() + defer fake.writeMutex.Unlock() + fake.WriteStub = nil + if fake.writeReturnsOnCall == nil { + fake.writeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.writeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeFileManager) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.chmodMutex.RLock() + defer fake.chmodMutex.RUnlock() + fake.createMutex.RLock() + defer fake.createMutex.RUnlock() + fake.readDirMutex.RLock() + defer fake.readDirMutex.RUnlock() + fake.removeMutex.RLock() + defer fake.removeMutex.RUnlock() + fake.writeMutex.RLock() + defer fake.writeMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeFileManager) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ state.FileManager = new(FakeFileManager) From 6f8a3dc94cc72c7a1e5954f6f8e29dc9d4e58e83 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 21 Jul 2022 09:30:14 -0700 Subject: [PATCH 32/42] Move listener tests into listener_test.go --- internal/state/graph_test.go | 147 ------------------------------ internal/state/listener_test.go | 156 ++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 147 deletions(-) create mode 100644 internal/state/listener_test.go diff --git a/internal/state/graph_test.go b/internal/state/graph_test.go index 8dc87569d4..fe4922ea45 100644 --- a/internal/state/graph_test.go +++ b/internal/state/graph_test.go @@ -985,153 +985,6 @@ func TestFindAcceptedHostnames(t *testing.T) { } -func TestValidateHTTPListener(t *testing.T) { - tests := []struct { - l v1alpha2.Listener - expected bool - msg string - }{ - { - l: v1alpha2.Listener{ - Port: 80, - Protocol: v1alpha2.HTTPProtocolType, - }, - expected: true, - msg: "valid", - }, - { - l: v1alpha2.Listener{ - Port: 81, - Protocol: v1alpha2.HTTPProtocolType, - }, - expected: false, - msg: "invalid port", - }, - } - - for _, test := range tests { - result := validateHTTPListener(test.l) - if result != test.expected { - t.Errorf("validateListener() returned %v but expected %v for the case of %q", result, test.expected, test.msg) - } - } -} - -func TestValidateHTTPSListener(t *testing.T) { - gwNs := "gateway-ns" - - validSecretRef := &v1alpha2.SecretObjectReference{ - Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("Secret")), - Name: "secret", - Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer(gwNs)), - } - - invalidSecretRefType := &v1alpha2.SecretObjectReference{ - Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("ConfigMap")), - Name: "secret", - Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer(gwNs)), - } - - invalidSecretRefTNamespace := &v1alpha2.SecretObjectReference{ - Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("Secret")), - Name: "secret", - Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer("diff-ns")), - } - - tests := []struct { - l v1alpha2.Listener - expected bool - msg string - }{ - { - l: v1alpha2.Listener{ - Port: 443, - Protocol: v1alpha2.HTTPSProtocolType, - TLS: &v1alpha2.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), - CertificateRefs: []*v1alpha2.SecretObjectReference{validSecretRef}, - }, - }, - expected: true, - msg: "valid", - }, - { - l: v1alpha2.Listener{ - Port: 80, - Protocol: v1alpha2.HTTPSProtocolType, - TLS: &v1alpha2.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), - CertificateRefs: []*v1alpha2.SecretObjectReference{validSecretRef}, - }, - }, - expected: false, - msg: "invalid port", - }, - { - l: v1alpha2.Listener{ - Port: 443, - Protocol: v1alpha2.HTTPSProtocolType, - TLS: &v1alpha2.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), - }, - }, - expected: false, - msg: "invalid - no cert ref", - }, - { - l: v1alpha2.Listener{ - Port: 443, - Protocol: v1alpha2.HTTPSProtocolType, - TLS: &v1alpha2.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1alpha2.TLSModePassthrough), - CertificateRefs: []*v1alpha2.SecretObjectReference{validSecretRef}, - }, - }, - expected: false, - msg: "invalid tls mode", - }, - { - l: v1alpha2.Listener{ - Port: 443, - Protocol: v1alpha2.HTTPSProtocolType, - TLS: &v1alpha2.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), - CertificateRefs: []*v1alpha2.SecretObjectReference{invalidSecretRefType}, - }, - }, - expected: false, - msg: "invalid cert ref kind", - }, - { - l: v1alpha2.Listener{ - Port: 443, - Protocol: v1alpha2.HTTPSProtocolType, - TLS: &v1alpha2.GatewayTLSConfig{ - Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), - CertificateRefs: []*v1alpha2.SecretObjectReference{invalidSecretRefTNamespace}, - }, - }, - expected: false, - msg: "invalid cert ref namespace", - }, - { - l: v1alpha2.Listener{ - Port: 443, - Protocol: v1alpha2.HTTPSProtocolType, - }, - expected: false, - msg: "invalid - no tls config", - }, - } - - for _, test := range tests { - result := validateHTTPSListener(test.l, gwNs) - if result != test.expected { - t.Errorf("validateHTTPSListener() returned %v but expected %v for the case of %q", result, test.expected, test.msg) - } - } -} - func TestGetHostname(t *testing.T) { var emptyHostname v1alpha2.Hostname var hostname v1alpha2.Hostname = "example.com" diff --git a/internal/state/listener_test.go b/internal/state/listener_test.go new file mode 100644 index 0000000000..f2f57a9b5c --- /dev/null +++ b/internal/state/listener_test.go @@ -0,0 +1,156 @@ +package state + +import ( + "testing" + + "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers" +) + +func TestValidateHTTPListener(t *testing.T) { + tests := []struct { + l v1alpha2.Listener + expected bool + msg string + }{ + { + l: v1alpha2.Listener{ + Port: 80, + Protocol: v1alpha2.HTTPProtocolType, + }, + expected: true, + msg: "valid", + }, + { + l: v1alpha2.Listener{ + Port: 81, + Protocol: v1alpha2.HTTPProtocolType, + }, + expected: false, + msg: "invalid port", + }, + } + + for _, test := range tests { + result := validateHTTPListener(test.l) + if result != test.expected { + t.Errorf("validateListener() returned %v but expected %v for the case of %q", result, test.expected, test.msg) + } + } +} + +func TestValidateHTTPSListener(t *testing.T) { + gwNs := "gateway-ns" + + validSecretRef := &v1alpha2.SecretObjectReference{ + Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer(gwNs)), + } + + invalidSecretRefType := &v1alpha2.SecretObjectReference{ + Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("ConfigMap")), + Name: "secret", + Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer(gwNs)), + } + + invalidSecretRefTNamespace := &v1alpha2.SecretObjectReference{ + Kind: (*v1alpha2.Kind)(helpers.GetStringPointer("Secret")), + Name: "secret", + Namespace: (*v1alpha2.Namespace)(helpers.GetStringPointer("diff-ns")), + } + + tests := []struct { + l v1alpha2.Listener + expected bool + msg string + }{ + { + l: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{validSecretRef}, + }, + }, + expected: true, + msg: "valid", + }, + { + l: v1alpha2.Listener{ + Port: 80, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{validSecretRef}, + }, + }, + expected: false, + msg: "invalid port", + }, + { + l: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + }, + }, + expected: false, + msg: "invalid - no cert ref", + }, + { + l: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModePassthrough), + CertificateRefs: []*v1alpha2.SecretObjectReference{validSecretRef}, + }, + }, + expected: false, + msg: "invalid tls mode", + }, + { + l: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{invalidSecretRefType}, + }, + }, + expected: false, + msg: "invalid cert ref kind", + }, + { + l: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + TLS: &v1alpha2.GatewayTLSConfig{ + Mode: helpers.GetTLSModePointer(v1alpha2.TLSModeTerminate), + CertificateRefs: []*v1alpha2.SecretObjectReference{invalidSecretRefTNamespace}, + }, + }, + expected: false, + msg: "invalid cert ref namespace", + }, + { + l: v1alpha2.Listener{ + Port: 443, + Protocol: v1alpha2.HTTPSProtocolType, + }, + expected: false, + msg: "invalid - no tls config", + }, + } + + for _, test := range tests { + result := validateHTTPSListener(test.l, gwNs) + if result != test.expected { + t.Errorf("validateHTTPSListener() returned %v but expected %v for the case of %q", result, test.expected, test.msg) + } + } +} From a8e4a541a9d63ccf5bec9a21d49ebec926dc187e Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 21 Jul 2022 09:45:49 -0700 Subject: [PATCH 33/42] Remove namespace from example resources --- examples/advanced-routing/cafe-routes.yaml | 2 -- examples/advanced-routing/gateway.yaml | 1 - examples/cafe-example/cafe-routes.yaml | 3 --- examples/cafe-example/gateway.yaml | 1 - examples/https-termination/cafe-routes.yaml | 2 -- examples/https-termination/cafe-secret.yaml | 1 - examples/https-termination/gateway.yaml | 1 - 7 files changed, 11 deletions(-) diff --git a/examples/advanced-routing/cafe-routes.yaml b/examples/advanced-routing/cafe-routes.yaml index 63bcd30068..614f5fe1c4 100644 --- a/examples/advanced-routing/cafe-routes.yaml +++ b/examples/advanced-routing/cafe-routes.yaml @@ -5,7 +5,6 @@ metadata: spec: parentRefs: - name: gateway - namespace: default sectionName: http hostnames: - "cafe.example.com" @@ -41,7 +40,6 @@ metadata: spec: parentRefs: - name: gateway - namespace: default sectionName: http hostnames: - "cafe.example.com" diff --git a/examples/advanced-routing/gateway.yaml b/examples/advanced-routing/gateway.yaml index 660f3d8f48..5ce1a34b21 100644 --- a/examples/advanced-routing/gateway.yaml +++ b/examples/advanced-routing/gateway.yaml @@ -2,7 +2,6 @@ apiVersion: gateway.networking.k8s.io/v1alpha2 kind: Gateway metadata: name: gateway - namespace: default labels: domain: k8s-gateway.nginx.org spec: diff --git a/examples/cafe-example/cafe-routes.yaml b/examples/cafe-example/cafe-routes.yaml index ee1f4d61c0..b84c4ce6ab 100644 --- a/examples/cafe-example/cafe-routes.yaml +++ b/examples/cafe-example/cafe-routes.yaml @@ -5,7 +5,6 @@ metadata: spec: parentRefs: - name: gateway - namespace: default sectionName: http hostnames: - "cafe.example.com" @@ -21,7 +20,6 @@ metadata: spec: parentRefs: - name: gateway - namespace: default sectionName: http hostnames: - "cafe.example.com" @@ -41,7 +39,6 @@ metadata: spec: parentRefs: - name: gateway - namespace: default sectionName: http hostnames: - "cafe.example.com" diff --git a/examples/cafe-example/gateway.yaml b/examples/cafe-example/gateway.yaml index 660f3d8f48..5ce1a34b21 100644 --- a/examples/cafe-example/gateway.yaml +++ b/examples/cafe-example/gateway.yaml @@ -2,7 +2,6 @@ apiVersion: gateway.networking.k8s.io/v1alpha2 kind: Gateway metadata: name: gateway - namespace: default labels: domain: k8s-gateway.nginx.org spec: diff --git a/examples/https-termination/cafe-routes.yaml b/examples/https-termination/cafe-routes.yaml index 23e4ed9325..33a87d375e 100644 --- a/examples/https-termination/cafe-routes.yaml +++ b/examples/https-termination/cafe-routes.yaml @@ -5,7 +5,6 @@ metadata: spec: parentRefs: - name: gateway - namespace: default sectionName: https hostnames: - "cafe.example.com" @@ -25,7 +24,6 @@ metadata: spec: parentRefs: - name: gateway - namespace: default sectionName: https hostnames: - "cafe.example.com" diff --git a/examples/https-termination/cafe-secret.yaml b/examples/https-termination/cafe-secret.yaml index a81954b8db..4510460bba 100644 --- a/examples/https-termination/cafe-secret.yaml +++ b/examples/https-termination/cafe-secret.yaml @@ -2,7 +2,6 @@ apiVersion: v1 kind: Secret metadata: name: cafe-secret - namespace: default type: kubernetes.io/tls data: tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNzakNDQVpvQ0NRQzdCdVdXdWRtRkNEQU5CZ2txaGtpRzl3MEJBUXNGQURBYk1Sa3dGd1lEVlFRRERCQmoKWVdabExtVjRZVzF3YkdVdVkyOXRNQjRYRFRJeU1EY3hOREl4TlRJek9Wb1hEVEl6TURjeE5ESXhOVEl6T1ZvdwpHekVaTUJjR0ExVUVBd3dRWTJGbVpTNWxlR0Z0Y0d4bExtTnZiVENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFECmdnRVBBRENDQVFvQ2dnRUJBTHFZMnRHNFc5aStFYzJhdnV4Q2prb2tnUUx1ek10U1Rnc1RNaEhuK3ZRUmxIam8KVzFLRnMvQVdlS25UUStyTWVKVWNseis4M3QwRGtyRThwUisxR2NKSE50WlNMb0NEYUlRN0Nhck5nY1daS0o4Qgo1WDNnVS9YeVJHZjI2c1REd2xzU3NkSEQ1U2U3K2Vab3NPcTdHTVF3K25HR2NVZ0VtL1Q1UEMvY05PWE0zZWxGClRPL051MStoMzROVG9BbDNQdTF2QlpMcDNQVERtQ0thaEROV0NWbUJQUWpNNFI4VERsbFhhMHQ5Z1o1MTRSRzUKWHlZWTNtdzZpUzIrR1dYVXllMjFuWVV4UEhZbDV4RHY0c0FXaGRXbElweHlZQlNCRURjczN6QlI2bFF1OWkxZAp0R1k4dGJ3blVmcUVUR3NZdWxzc05qcU95V1VEcFdJelhibHhJZVVDQXdFQUFUQU5CZ2txaGtpRzl3MEJBUXNGCkFBT0NBUUVBcjkrZWJ0U1dzSnhLTGtLZlRkek1ISFhOd2Y5ZXFVbHNtTXZmMGdBdWVKTUpUR215dG1iWjlpbXQKL2RnWlpYVE9hTElHUG9oZ3BpS0l5eVVRZVdGQ2F0NHRxWkNPVWRhbUloOGk0Q1h6QVJYVHNvcUNOenNNLzZMRQphM25XbFZyS2lmZHYrWkxyRi8vblc0VVNvOEoxaCtQeDljY0tpRDZZU0RVUERDRGh1RUtFWXcvbHpoUDJVOXNmCnl6cEJKVGQ4enFyM3paTjNGWWlITmgzYlRhQS82di9jU2lyamNTK1EwQXg4RWpzQzYxRjRVMTc4QzdWNWRCKzQKcmtPTy9QNlA0UFlWNTRZZHMvRjE2WkZJTHFBNENCYnExRExuYWRxamxyN3NPbzl2ZzNnWFNMYXBVVkdtZ2todAp6VlZPWG1mU0Z4OS90MDBHUi95bUdPbERJbWlXMGc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== diff --git a/examples/https-termination/gateway.yaml b/examples/https-termination/gateway.yaml index d67aa65cf2..13a00f8d4c 100644 --- a/examples/https-termination/gateway.yaml +++ b/examples/https-termination/gateway.yaml @@ -2,7 +2,6 @@ apiVersion: gateway.networking.k8s.io/v1alpha2 kind: Gateway metadata: name: gateway - namespace: default labels: domain: k8s-gateway.nginx.org spec: From 08a84dbb4634bbcc5478f152cc26ef3bb0a3f186 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 21 Jul 2022 09:46:52 -0700 Subject: [PATCH 34/42] Update capacity --- internal/nginx/config/generator.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/nginx/config/generator.go b/internal/nginx/config/generator.go index 54f887cfe7..a3b405baa9 100644 --- a/internal/nginx/config/generator.go +++ b/internal/nginx/config/generator.go @@ -43,8 +43,8 @@ func (g *GeneratorImpl) Generate(conf state.Configuration) ([]byte, Warnings) { confServers := append(conf.HTTPServers, conf.SSLServers...) servers := httpServers{ - // capacity is all the conf servers + default tls termination server - Servers: make([]server, 0, len(confServers)+1), + // capacity is all the conf servers + default ssl & http servers + Servers: make([]server, 0, len(confServers)+2), } if len(conf.HTTPServers) > 0 { From d05d2c1cb4bde6d0935a8747c467a4fba38ac7ac Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 21 Jul 2022 09:48:01 -0700 Subject: [PATCH 35/42] Add fixme --- internal/state/change_processor_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/state/change_processor_test.go b/internal/state/change_processor_test.go index b394609cfc..6de1be50b6 100644 --- a/internal/state/change_processor_test.go +++ b/internal/state/change_processor_test.go @@ -13,6 +13,7 @@ import ( "github.com/nginxinc/nginx-kubernetes-gateway/internal/state/statefakes" ) +// FIXME(kate-osborn): Consider refactoring these tests to reduce code duplication. var _ = Describe("ChangeProcessor", func() { Describe("Normal cases of processing changes", func() { const ( From 67958f6fab72859f313c9499972e6828d808db0a Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 21 Jul 2022 09:49:27 -0700 Subject: [PATCH 36/42] Add doc string --- internal/state/secrets.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/state/secrets.go b/internal/state/secrets.go index 3bea4cd3b6..7ffb950d4d 100644 --- a/internal/state/secrets.go +++ b/internal/state/secrets.go @@ -66,6 +66,7 @@ func (s SecretStoreImpl) Get(nsname types.NamespacedName) *Secret { return s.secrets[nsname] } +// SecretDiskMemoryManager manages secrets that are requested by Gateway resources. type SecretDiskMemoryManager interface { // Request marks the secret as requested so that it can be written to disk before reloading NGINX. // Returns the path to the secret and an error if the secret does not exist in the secret store or the secret is invalid. From 2909f0eb718c7dc3a923af795d7ef4cfed52e441 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 21 Jul 2022 09:50:18 -0700 Subject: [PATCH 37/42] Fix typo --- internal/state/secrets_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/state/secrets_test.go b/internal/state/secrets_test.go index f5c912cd60..61b1dadacc 100644 --- a/internal/state/secrets_test.go +++ b/internal/state/secrets_test.go @@ -217,7 +217,7 @@ var _ = Describe("SecretDiskMemoryManager", func() { testRequest(secret3, expectedPath, false) }) - It("should write all stored secrets", func() { + It("should write all requested secrets", func() { err := memMgr.WriteAllRequestedSecrets() Expect(err).ToNot(HaveOccurred()) From 9eb07ec629af09cc745545211333b2e3b9307f96 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Thu, 21 Jul 2022 10:45:37 -0700 Subject: [PATCH 38/42] Add fixme for concurrency question --- internal/state/secrets.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/state/secrets.go b/internal/state/secrets.go index 7ffb950d4d..6563e64895 100644 --- a/internal/state/secrets.go +++ b/internal/state/secrets.go @@ -90,6 +90,7 @@ type FileManager interface { Write(file *os.File, contents []byte) error } +// FIXME(kate-osborn): Is it necessary to make this concurrent-safe? type SecretDiskMemoryManagerImpl struct { requestedSecrets map[types.NamespacedName]requestedSecret secretStore SecretStore From 37571b6a97533d31b86454f0780dd96ffafabbc3 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Mon, 25 Jul 2022 08:59:21 -0600 Subject: [PATCH 39/42] Move file i/o tests under secret disk memory manager describe --- internal/state/secrets_test.go | 134 ++++++++++++++++----------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/internal/state/secrets_test.go b/internal/state/secrets_test.go index 61b1dadacc..6a6f953001 100644 --- a/internal/state/secrets_test.go +++ b/internal/state/secrets_test.go @@ -243,6 +243,73 @@ var _ = Describe("SecretDiskMemoryManager", func() { }) }) }) + Describe("File Management Error Cases", func() { + var ( + fakeFileManager *statefakes.FakeFileManager + fakeStore *statefakes.FakeSecretStore + fakeFileInfoSlice []fs.FileInfo + memMgr state.SecretDiskMemoryManager + ) + + BeforeEach(OncePerOrdered, func() { + fakeFileManager = &statefakes.FakeFileManager{} + fakeStore = &statefakes.FakeSecretStore{} + fakeFileInfoSlice = []fs.FileInfo{&statefakes.FakeFileInfo{}} + memMgr = state.NewSecretDiskMemoryManager("", fakeStore, state.WithSecretFileManager(fakeFileManager)) + + // populate a requested secret + fakeStore.GetReturns(&state.Secret{Secret: secret1, Valid: true}) + _, err := memMgr.Request(types.NamespacedName{Namespace: secret1.Namespace, Name: secret1.Name}) + Expect(err).ToNot(HaveOccurred()) + }) + + Describe("Write all requested secrets", Ordered, func() { + It("returns an error when secret directory cannot be read from", func() { + errString := "read dir error" + fakeFileManager.ReadDirReturns(nil, errors.New(errString)) + + err := memMgr.WriteAllRequestedSecrets() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errString)) + }) + It("returns an error when a file cannot be removed from the secrets directory", func() { + errString := "remove error" + fakeFileManager.ReadDirReturns(fakeFileInfoSlice, nil) + fakeFileManager.RemoveReturns(errors.New(errString)) + + err := memMgr.WriteAllRequestedSecrets() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errString)) + }) + It("returns an error when file cannot be created", func() { + errString := "create error" + fakeFileManager.RemoveReturns(nil) + fakeFileManager.CreateReturns(nil, errors.New(errString)) + + err := memMgr.WriteAllRequestedSecrets() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errString)) + }) + It("returns an error when it cannot change the mode of the file", func() { + errStr := "chmod error" + fakeFileManager.CreateReturns(&os.File{}, nil) + fakeFileManager.ChmodReturns(errors.New(errStr)) + + err := memMgr.WriteAllRequestedSecrets() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errStr)) + }) + It("returns an error when file cannot be written to", func() { + errString := "write error" + fakeFileManager.ChmodReturns(nil) + fakeFileManager.WriteReturns(errors.New(errString)) + + err := memMgr.WriteAllRequestedSecrets() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errString)) + }) + }) + }) }) var _ = Describe("SecretStore", func() { @@ -331,71 +398,4 @@ var _ = Describe("SecretStore", func() { store.Delete(nsname) }) }) - Describe("File Management Error Cases", func() { - var ( - fakeFileManager *statefakes.FakeFileManager - fakeStore *statefakes.FakeSecretStore - fakeFileInfoSlice []fs.FileInfo - memMgr state.SecretDiskMemoryManager - ) - - BeforeEach(OncePerOrdered, func() { - fakeFileManager = &statefakes.FakeFileManager{} - fakeStore = &statefakes.FakeSecretStore{} - fakeFileInfoSlice = []fs.FileInfo{&statefakes.FakeFileInfo{}} - memMgr = state.NewSecretDiskMemoryManager("", fakeStore, state.WithSecretFileManager(fakeFileManager)) - - // populate a requested secret - fakeStore.GetReturns(&state.Secret{Secret: secret1, Valid: true}) - _, err := memMgr.Request(types.NamespacedName{Namespace: secret1.Namespace, Name: secret1.Name}) - Expect(err).ToNot(HaveOccurred()) - }) - - Describe("Write all requested secrets", Ordered, func() { - It("returns an error when secret directory cannot be read from", func() { - errString := "read dir error" - fakeFileManager.ReadDirReturns(nil, errors.New(errString)) - - err := memMgr.WriteAllRequestedSecrets() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(errString)) - }) - It("returns an error when a file cannot be removed from the secrets directory", func() { - errString := "remove error" - fakeFileManager.ReadDirReturns(fakeFileInfoSlice, nil) - fakeFileManager.RemoveReturns(errors.New(errString)) - - err := memMgr.WriteAllRequestedSecrets() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(errString)) - }) - It("returns an error when file cannot be created", func() { - errString := "create error" - fakeFileManager.RemoveReturns(nil) - fakeFileManager.CreateReturns(nil, errors.New(errString)) - - err := memMgr.WriteAllRequestedSecrets() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(errString)) - }) - It("returns an error when it cannot change the mode of the file", func() { - errStr := "chmod error" - fakeFileManager.CreateReturns(&os.File{}, nil) - fakeFileManager.ChmodReturns(errors.New(errStr)) - - err := memMgr.WriteAllRequestedSecrets() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(errStr)) - }) - It("returns an error when file cannot be written to", func() { - errString := "write error" - fakeFileManager.ChmodReturns(nil) - fakeFileManager.WriteReturns(errors.New(errString)) - - err := memMgr.WriteAllRequestedSecrets() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(errString)) - }) - }) - }) }) From 5070b28960002b398ef4ed2ba31a1603e9a28e19 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Mon, 25 Jul 2022 09:00:50 -0600 Subject: [PATCH 40/42] Use impl in tests --- internal/state/secrets_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/state/secrets_test.go b/internal/state/secrets_test.go index 6a6f953001..4cd43dbefe 100644 --- a/internal/state/secrets_test.go +++ b/internal/state/secrets_test.go @@ -248,7 +248,7 @@ var _ = Describe("SecretDiskMemoryManager", func() { fakeFileManager *statefakes.FakeFileManager fakeStore *statefakes.FakeSecretStore fakeFileInfoSlice []fs.FileInfo - memMgr state.SecretDiskMemoryManager + memMgr *state.SecretDiskMemoryManagerImpl ) BeforeEach(OncePerOrdered, func() { From 57472ba3fc74823bcb03f75db6e9eacb8f67c44b Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Mon, 25 Jul 2022 09:41:16 -0600 Subject: [PATCH 41/42] Rewrite error tests as table --- internal/state/secrets_test.go | 76 ++++++++++++++-------------------- 1 file changed, 31 insertions(+), 45 deletions(-) diff --git a/internal/state/secrets_test.go b/internal/state/secrets_test.go index 4cd43dbefe..997be60ecf 100644 --- a/internal/state/secrets_test.go +++ b/internal/state/secrets_test.go @@ -243,7 +243,7 @@ var _ = Describe("SecretDiskMemoryManager", func() { }) }) }) - Describe("File Management Error Cases", func() { + Describe("Write all requested secrets", func() { var ( fakeFileManager *statefakes.FakeFileManager fakeStore *statefakes.FakeSecretStore @@ -263,52 +263,38 @@ var _ = Describe("SecretDiskMemoryManager", func() { Expect(err).ToNot(HaveOccurred()) }) - Describe("Write all requested secrets", Ordered, func() { - It("returns an error when secret directory cannot be read from", func() { - errString := "read dir error" - fakeFileManager.ReadDirReturns(nil, errors.New(errString)) + DescribeTable("error cases", Ordered, + func(e error, preparer func(e error)) { + preparer(e) err := memMgr.WriteAllRequestedSecrets() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(errString)) - }) - It("returns an error when a file cannot be removed from the secrets directory", func() { - errString := "remove error" - fakeFileManager.ReadDirReturns(fakeFileInfoSlice, nil) - fakeFileManager.RemoveReturns(errors.New(errString)) - - err := memMgr.WriteAllRequestedSecrets() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(errString)) - }) - It("returns an error when file cannot be created", func() { - errString := "create error" - fakeFileManager.RemoveReturns(nil) - fakeFileManager.CreateReturns(nil, errors.New(errString)) - - err := memMgr.WriteAllRequestedSecrets() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(errString)) - }) - It("returns an error when it cannot change the mode of the file", func() { - errStr := "chmod error" - fakeFileManager.CreateReturns(&os.File{}, nil) - fakeFileManager.ChmodReturns(errors.New(errStr)) - - err := memMgr.WriteAllRequestedSecrets() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(errStr)) - }) - It("returns an error when file cannot be written to", func() { - errString := "write error" - fakeFileManager.ChmodReturns(nil) - fakeFileManager.WriteReturns(errors.New(errString)) - - err := memMgr.WriteAllRequestedSecrets() - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(errString)) - }) - }) + Expect(err).To(MatchError(e)) + }, + Entry("read directory error", errors.New("read dir"), + func(e error) { + fakeFileManager.ReadDirReturns(nil, e) + }), + Entry("remove file error", errors.New("remove file"), + func(e error) { + fakeFileManager.ReadDirReturns(fakeFileInfoSlice, nil) + fakeFileManager.RemoveReturns(e) + }), + Entry("create file error", errors.New("create error"), + func(e error) { + fakeFileManager.RemoveReturns(nil) + fakeFileManager.CreateReturns(nil, e) + }), + Entry("chmod error", errors.New("chmod"), + func(e error) { + fakeFileManager.CreateReturns(&os.File{}, nil) + fakeFileManager.ChmodReturns(e) + }), + Entry("write error", errors.New("write"), + func(e error) { + fakeFileManager.ChmodReturns(nil) + fakeFileManager.WriteReturns(e) + }), + ) }) }) From 530249e211863b9b065bdbe0d44782d61a79ce44 Mon Sep 17 00:00:00 2001 From: Kate Osborn Date: Wed, 27 Jul 2022 10:19:47 -0600 Subject: [PATCH 42/42] Fix template spacing --- internal/nginx/config/template.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/nginx/config/template.go b/internal/nginx/config/template.go index 08cce30d7a..5bfcdd293a 100644 --- a/internal/nginx/config/template.go +++ b/internal/nginx/config/template.go @@ -22,15 +22,15 @@ server { } {{ else }} server { - {{ if $s.SSL }} + {{ if $s.SSL }} listen 443 ssl; ssl_certificate {{ $s.SSL.Certificate }}; ssl_certificate_key {{ $s.SSL.CertificateKey }}; - {{ end }} + {{ end }} server_name {{ $s.ServerName }}; - {{ range $l := $s.Locations }} + {{ range $l := $s.Locations }} location {{ $l.Path }} { {{ if $l.Internal }} internal; @@ -47,7 +47,7 @@ server { proxy_pass {{ $l.ProxyPass }}$request_uri; {{ end }} } - {{ end }} + {{ end }} } {{ end }} {{ end }}