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/deploy/manifests/nginx-gateway.yaml b/deploy/manifests/nginx-gateway.yaml index d7bedf4c3b..97f69cf6e3 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; }" > /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/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..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: nginx-gateway sectionName: http hostnames: - "cafe.example.com" @@ -41,7 +40,6 @@ metadata: spec: parentRefs: - name: gateway - namespace: nginx-gateway sectionName: http hostnames: - "cafe.example.com" diff --git a/deploy/manifests/gateway.yaml b/examples/advanced-routing/gateway.yaml similarity index 89% rename from deploy/manifests/gateway.yaml rename to examples/advanced-routing/gateway.yaml index 68294a3524..5ce1a34b21 100644 --- a/deploy/manifests/gateway.yaml +++ b/examples/advanced-routing/gateway.yaml @@ -2,7 +2,6 @@ apiVersion: gateway.networking.k8s.io/v1alpha2 kind: Gateway metadata: name: gateway - namespace: nginx-gateway labels: domain: k8s-gateway.nginx.org spec: 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..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: nginx-gateway sectionName: http hostnames: - "cafe.example.com" @@ -21,7 +20,6 @@ metadata: spec: parentRefs: - name: gateway - namespace: nginx-gateway sectionName: http hostnames: - "cafe.example.com" @@ -41,7 +39,6 @@ metadata: spec: parentRefs: - name: gateway - namespace: nginx-gateway 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..5ce1a34b21 --- /dev/null +++ b/examples/cafe-example/gateway.yaml @@ -0,0 +1,12 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: Gateway +metadata: + name: gateway + labels: + domain: k8s-gateway.nginx.org +spec: + gatewayClassName: nginx + listeners: + - name: http + port: 80 + protocol: HTTP diff --git a/examples/https-termination/README.md b/examples/https-termination/README.md new file mode 100644 index 0000000000..fc0270a42c --- /dev/null +++ b/examples/https-termination/README.md @@ -0,0 +1,90 @@ +# 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 + +1. Create a secret with a TLS certificate and key: + ``` + kubectl apply -f cafe-secret.yaml + ``` + + 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: + ``` + 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: default + sectionName: https + ``` + +## 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..33a87d375e --- /dev/null +++ b/examples/https-termination/cafe-routes.yaml @@ -0,0 +1,37 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: HTTPRoute +metadata: + name: coffee +spec: + parentRefs: + - name: 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 + 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-secret.yaml b/examples/https-termination/cafe-secret.yaml new file mode 100644 index 0000000000..4510460bba --- /dev/null +++ b/examples/https-termination/cafe-secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: cafe-secret +type: kubernetes.io/tls +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNzakNDQVpvQ0NRQzdCdVdXdWRtRkNEQU5CZ2txaGtpRzl3MEJBUXNGQURBYk1Sa3dGd1lEVlFRRERCQmoKWVdabExtVjRZVzF3YkdVdVkyOXRNQjRYRFRJeU1EY3hOREl4TlRJek9Wb1hEVEl6TURjeE5ESXhOVEl6T1ZvdwpHekVaTUJjR0ExVUVBd3dRWTJGbVpTNWxlR0Z0Y0d4bExtTnZiVENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFECmdnRVBBRENDQVFvQ2dnRUJBTHFZMnRHNFc5aStFYzJhdnV4Q2prb2tnUUx1ek10U1Rnc1RNaEhuK3ZRUmxIam8KVzFLRnMvQVdlS25UUStyTWVKVWNseis4M3QwRGtyRThwUisxR2NKSE50WlNMb0NEYUlRN0Nhck5nY1daS0o4Qgo1WDNnVS9YeVJHZjI2c1REd2xzU3NkSEQ1U2U3K2Vab3NPcTdHTVF3K25HR2NVZ0VtL1Q1UEMvY05PWE0zZWxGClRPL051MStoMzROVG9BbDNQdTF2QlpMcDNQVERtQ0thaEROV0NWbUJQUWpNNFI4VERsbFhhMHQ5Z1o1MTRSRzUKWHlZWTNtdzZpUzIrR1dYVXllMjFuWVV4UEhZbDV4RHY0c0FXaGRXbElweHlZQlNCRURjczN6QlI2bFF1OWkxZAp0R1k4dGJ3blVmcUVUR3NZdWxzc05qcU95V1VEcFdJelhibHhJZVVDQXdFQUFUQU5CZ2txaGtpRzl3MEJBUXNGCkFBT0NBUUVBcjkrZWJ0U1dzSnhLTGtLZlRkek1ISFhOd2Y5ZXFVbHNtTXZmMGdBdWVKTUpUR215dG1iWjlpbXQKL2RnWlpYVE9hTElHUG9oZ3BpS0l5eVVRZVdGQ2F0NHRxWkNPVWRhbUloOGk0Q1h6QVJYVHNvcUNOenNNLzZMRQphM25XbFZyS2lmZHYrWkxyRi8vblc0VVNvOEoxaCtQeDljY0tpRDZZU0RVUERDRGh1RUtFWXcvbHpoUDJVOXNmCnl6cEJKVGQ4enFyM3paTjNGWWlITmgzYlRhQS82di9jU2lyamNTK1EwQXg4RWpzQzYxRjRVMTc4QzdWNWRCKzQKcmtPTy9QNlA0UFlWNTRZZHMvRjE2WkZJTHFBNENCYnExRExuYWRxamxyN3NPbzl2ZzNnWFNMYXBVVkdtZ2todAp6VlZPWG1mU0Z4OS90MDBHUi95bUdPbERJbWlXMGc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzZtTnJSdUZ2WXZoSE4KbXI3c1FvNUtKSUVDN3N6TFVrNExFeklSNS9yMEVaUjQ2RnRTaGJQd0ZuaXAwMFBxekhpVkhKYy92TjdkQTVLeApQS1VmdFJuQ1J6YldVaTZBZzJpRU93bXF6WUhGbVNpZkFlVjk0RlAxOGtSbjl1ckV3OEpiRXJIUncrVW51L25tCmFMRHF1eGpFTVBweGhuRklCSnYwK1R3djNEVGx6TjNwUlV6dnpidGZvZCtEVTZBSmR6N3Rid1dTNmR6MHc1Z2kKbW9RelZnbFpnVDBJek9FZkV3NVpWMnRMZllHZWRlRVJ1VjhtR041c09va3R2aGxsMU1udHRaMkZNVHgySmVjUQo3K0xBRm9YVnBTS2NjbUFVZ1JBM0xOOHdVZXBVTHZZdFhiUm1QTFc4SjFINmhFeHJHTHBiTERZNmpzbGxBNlZpCk0xMjVjU0hsQWdNQkFBRUNnZ0VBQnpaRE50bmVTdWxGdk9HZlFYaHRFWGFKdWZoSzJBenRVVVpEcUNlRUxvekQKWlV6dHdxbkNRNlJLczUyandWNTN4cU9kUU94bTNMbjNvSHdNa2NZcEliWW82MjJ2dUczYnkwaVEzaFlsVHVMVgpqQmZCcS9UUXFlL2NMdngvSkczQWhFNmJxdFRjZFlXeGFmTmY2eUtpR1dzZk11WVVXTWs4MGVJVUxuRmZaZ1pOCklYNTlSOHlqdE9CVm9Sa3hjYTVoMW1ZTDFsSlJNM3ZqVHNHTHFybmpOTjNBdWZ3ZGRpK1VDbGZVL2l0K1EvZkUKV216aFFoTlRpNVFkRWJLVStOTnYvNnYvb2JvandNb25HVVBCdEFTUE05cmxFemIralQ1WHdWQjgvLzRGY3VoSwoyVzNpcjhtNHVlQ1JHSVlrbGxlLzhuQmZ0eVhiVkNocVRyZFBlaGlPM1FLQmdRRGlrR3JTOTc3cjg3Y1JPOCtQClpoeXltNXo4NVIzTHVVbFNTazJiOTI1QlhvakpZL2RRZDVTdFVsSWE4OUZKZnNWc1JRcEhHaTFCYzBMaTY1YjIKazR0cE5xcVFoUmZ1UVh0UG9GYXRuQzlPRnJVTXJXbDVJN0ZFejZnNkNQMVBXMEg5d2hPemFKZUdpZVpNYjlYTQoybDdSSFZOcC9jTDlYbmhNMnN0Q1lua2Iwd0tCZ1FEUzF4K0crakEyUVNtRVFWNXA1RnRONGcyamsyZEFjMEhNClRIQ2tTazFDRjhkR0Z2UWtsWm5ZbUt0dXFYeXNtekJGcnZKdmt2eUhqbUNYYTducXlpajBEdDZtODViN3BGcVAKQWxtajdtbXI3Z1pUeG1ZMXBhRWFLMXY4SDNINGtRNVl3MWdrTWRybVJHcVAvaTBGaDVpaGtSZS9DOUtGTFVkSQpDcnJjTzhkUVp3S0JnSHA1MzRXVWNCMVZibzFlYStIMUxXWlFRUmxsTWlwRFM2TzBqeWZWSmtFb1BZSEJESnp2ClIrdzZLREJ4eFoyWmJsZ05LblV0YlhHSVFZd3lGelhNcFB5SGxNVHpiZkJhYmJLcDFyR2JVT2RCMXpXM09PRkgKcmppb21TUm1YNmxhaDk0SjRHU0lFZ0drNGw1SHhxZ3JGRDZ2UDd4NGRjUktJWFpLZ0w2dVJSSUpBb0dCQU1CVApaL2p5WStRNTBLdEtEZHUrYU9ORW4zaGxUN3hrNXRKN3NBek5rbWdGMU10RXlQUk9Xd1pQVGFJbWpRbk9qbHdpCldCZ2JGcXg0M2ZlQ1Z4ZXJ6V3ZEM0txaWJVbWpCTkNMTGtYeGh3ZEVteFQwVit2NzZGYzgwaTNNYVdSNnZZR08KditwVVovL0F6UXdJcWZ6dlVmV2ZxdStrMHlhVXhQOGNlcFBIRyt0bEFvR0FmQUtVVWhqeFU0Ym5vVzVwVUhKegpwWWZXZXZ5TW54NWZyT2VsSmRmNzlvNGMvMHhVSjh1eFBFWDFkRmNrZW96dHNpaVFTNkN6MENRY09XVWxtSkRwCnVrdERvVzM3VmNSQU1BVjY3NlgxQVZlM0UwNm5aL2g2Tkd4Z28rT042Q3pwL0lkMkJPUm9IMFAxa2RjY1NLT3kKMUtFZlNnb1B0c1N1eEpBZXdUZmxDMXc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K 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/examples/https-termination/gateway.yaml b/examples/https-termination/gateway.yaml new file mode 100644 index 0000000000..13a00f8d4c --- /dev/null +++ b/examples/https-termination/gateway.yaml @@ -0,0 +1,18 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: Gateway +metadata: + name: gateway + 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 diff --git a/internal/events/loop.go b/internal/events/loop.go index 95f2c4cbdd..a5c9711ae5 100644 --- a/internal/events/loop.go +++ b/internal/events/loop.go @@ -15,39 +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.SecretDiskMemoryManager + // 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 - 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, - 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, - 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. @@ -61,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) } } @@ -78,26 +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 { - cfg, warnings := el.generator.Generate(conf) + // Write all secrets (nuke and pave). + // This will remove all secrets in the secrets directory before writing the requested secrets. + // FIXME(kate-osborn): We may want to rethink this approach in the future and write and remove secrets individually. + err := el.cfg.SecretMemoryManager.WriteAllRequestedSecrets() + if err != nil { + return err + } + + 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 } @@ -105,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(), @@ -113,20 +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.cfg.SecretStore.Upsert(r) default: panic(fmt.Errorf("unknown resource type %T", e.Resource)) } @@ -135,14 +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.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 f364d0ce9f..120c07b20f 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.FakeSecretDiskMemoryManager + 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.FakeSecretDiskMemoryManager{} + 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(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()) @@ -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/gatewayclass/implementation_suit_test.go b/internal/implementations/gateway/implementation_suite_test.go similarity index 60% rename from internal/implementations/gatewayclass/implementation_suit_test.go rename to internal/implementations/gateway/implementation_suite_test.go index 5bba5d8086..dffdfe85a9 100644 --- a/internal/implementations/gatewayclass/implementation_suit_test.go +++ b/internal/implementations/gateway/implementation_suite_test.go @@ -7,7 +7,7 @@ import ( . "github.com/onsi/gomega" ) -func TestState(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_suite_test.go b/internal/implementations/gatewayclass/implementation_suite_test.go new file mode 100644 index 0000000000..a6f600b94c --- /dev/null +++ b/internal/implementations/gatewayclass/implementation_suite_test.go @@ -0,0 +1,13 @@ +package implementation_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestGatewayClassImplementation(t *testing.T) { + RegisterFailHandler(Fail) + 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/gateway/implementation_suit_test.go b/internal/implementations/secret/implementation_suite_test.go similarity index 61% rename from internal/implementations/gateway/implementation_suit_test.go rename to internal/implementations/secret/implementation_suite_test.go index 7c9f572a55..bfa87e8dfa 100644 --- a/internal/implementations/gateway/implementation_suit_test.go +++ b/internal/implementations/secret/implementation_suite_test.go @@ -7,7 +7,7 @@ import ( . "github.com/onsi/gomega" ) -func TestImplementation(t *testing.T) { +func TestSecretImplementation(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Implementation Suite") + RunSpecs(t, "Secret Implementation Suite") } diff --git a/internal/implementations/secret/secret.go b/internal/implementations/secret/secret.go new file mode 100644 index 0000000000..d3fbc9f285 --- /dev/null +++ b/internal/implementations/secret/secret.go @@ -0,0 +1,53 @@ +package implementation + +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..8d0fc8dfd0 --- /dev/null +++ b/internal/implementations/secret/secret_test.go @@ -0,0 +1,64 @@ +package implementation_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/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 89061faa3f..22d2603e11 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" + 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" @@ -24,8 +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 +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() @@ -68,11 +74,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 +102,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(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 { diff --git a/internal/nginx/config/generator.go b/internal/nginx/config/generator.go index 77d5236eef..a3b405baa9 100644 --- a/internal/nginx/config/generator.go +++ b/internal/nginx/config/generator.go @@ -40,11 +40,26 @@ func NewGeneratorImpl(serviceStore state.ServiceStore) *GeneratorImpl { func (g *GeneratorImpl) Generate(conf state.Configuration) ([]byte, Warnings) { warnings := newWarnings() + confServers := append(conf.HTTPServers, conf.SSLServers...) + servers := httpServers{ - Servers: make([]server, 0, len(conf.HTTPServers)), + // capacity is all the conf servers + default ssl & http servers + Servers: make([]server, 0, len(confServers)+2), + } + + if len(conf.HTTPServers) > 0 { + defaultHTTPServer := generateDefaultHTTPServer() + + servers.Servers = append(servers.Servers, defaultHTTPServer) } - for _, s := range conf.HTTPServers { + if len(conf.SSLServers) > 0 { + defaultSSLServer := generateDefaultSSLServer() + + servers.Servers = append(servers.Servers, defaultSSLServer) + } + + for _, s := range confServers { cfg, warns := generate(s, g.serviceStore) servers.Servers = append(servers.Servers, cfg) @@ -54,12 +69,20 @@ func (g *GeneratorImpl) Generate(conf state.Configuration) ([]byte, Warnings) { return g.executor.ExecuteForHTTPServers(servers), warnings } -func generate(httpServer state.HTTPServer, serviceStore state.ServiceStore) (server, Warnings) { +func generateDefaultSSLServer() server { + return server{IsDefaultSSL: true} +} + +func generateDefaultHTTPServer() server { + return server{IsDefaultHTTP: true} +} + +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 { @@ -102,11 +125,17 @@ func generate(httpServer state.HTTPServer, serviceStore state.ServiceStore) (ser locs = append(locs, pathLoc) } } - - return server{ - ServerName: httpServer.Hostname, + s := server{ + ServerName: virtualServer.Hostname, Locations: locs, - }, warnings + } + if virtualServer.SSL != nil { + s.SSL = &ssl{ + Certificate: virtualServer.SSL.CertificatePath, + CertificateKey: virtualServer.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..ae6543d758 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,89 @@ import ( func TestGenerateForHost(t *testing.T) { generator := NewGeneratorImpl(&statefakes.FakeServiceStore{}) - conf := state.Configuration{ - HTTPServers: []state.HTTPServer{ - { - Hostname: "example.com", + testcases := []struct { + conf state.Configuration + httpDefault bool + sslDefault bool + msg string + }{ + { + conf: state.Configuration{}, + httpDefault: false, + sslDefault: false, + msg: "no servers", + }, + { + conf: state.Configuration{ + HTTPServers: []state.VirtualServer{ + { + Hostname: "example.com", + }, + }, }, + httpDefault: true, + sslDefault: false, + msg: "only HTTP servers", + }, + { + conf: state.Configuration{ + SSLServers: []state.VirtualServer{ + { + Hostname: "example.com", + }, + }, + }, + httpDefault: false, + sslDefault: true, + msg: "only HTTPS servers", + }, + { + conf: state.Configuration{ + HTTPServers: []state.VirtualServer{ + { + Hostname: "example.com", + }, + }, + SSLServers: []state.VirtualServer{ + { + Hostname: "example.com", + }, + }, + }, + httpDefault: true, + sslDefault: 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) + 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.httpDefault && !defaultHTTPExists { + t.Errorf("Generate() did not generate a config with a default http server for test: %q", tc.msg) + } + + 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 for test: %q", tc.msg) + } + if len(warnings) > 0 { + t.Errorf("Generate() returned unexpected warnings: %v for test: %q", warnings, tc.msg) + } } } @@ -143,7 +212,9 @@ func TestGenerate(t *testing.T) { }, } - host := state.HTTPServer{ + certPath := "/etc/nginx/secrets/cert" + + httpHost := state.VirtualServer{ Hostname: "example.com", PathRules: []state.PathRule{ { @@ -189,6 +260,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 +290,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 +327,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.VirtualServer + 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..50d39b9025 100644 --- a/internal/nginx/config/http.go +++ b/internal/nginx/config/http.go @@ -5,8 +5,11 @@ type httpServers struct { } type server struct { - ServerName string - Locations []location + IsDefaultHTTP bool + IsDefaultSSL bool + ServerName string + SSL *ssl + Locations []location } type location struct { @@ -15,3 +18,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..5bfcdd293a 100644 --- a/internal/nginx/config/template.go +++ b/internal/nginx/config/template.go @@ -7,10 +7,30 @@ import ( ) var httpServersTemplate = `{{ range $s := .Servers }} + {{ 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 { + {{ 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 }} + {{ range $l := $s.Locations }} location {{ $l.Path }} { {{ if $l.Internal }} internal; @@ -27,8 +47,9 @@ server { proxy_pass {{ $l.ProxyPass }}$request_uri; {{ end }} } - {{ end }} + {{ end }} } + {{ end }} {{ end }} ` diff --git a/internal/state/change_processor.go b/internal/state/change_processor.go index 71625e29b7..150647ece6 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 SecretDiskMemoryManager } 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..6de1be50b6 100644 --- a/internal/state/change_processor_test.go +++ b/internal/state/change_processor_test.go @@ -10,13 +10,16 @@ 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" ) +// FIXME(kate-osborn): Consider refactoring these tests to reduce code duplication. 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 +27,7 @@ var _ = Describe("ChangeProcessor", func() { hr1, hr1Updated, hr2 *v1alpha2.HTTPRoute gw1, gw1Updated, gw2 *v1alpha2.Gateway processor state.ChangeProcessor + fakeSecretMemoryMgr *statefakes.FakeSecretDiskMemoryManager ) BeforeEach(OncePerOrdered, func() { @@ -54,6 +58,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 +106,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 +134,15 @@ var _ = Describe("ChangeProcessor", func() { gw2 = createGateway("gateway-2") + fakeSecretMemoryMgr = &statefakes.FakeSecretDiskMemoryManager{} + processor = state.NewChangeProcessorImpl(state.ChangeProcessorConfig{ - GatewayCtlrName: controllerName, - GatewayClassName: gcName, + GatewayCtlrName: controllerName, + GatewayClassName: gcName, + SecretMemoryManager: fakeSecretMemoryMgr, }) + + fakeSecretMemoryMgr.RequestReturns(certificatePath, nil) }) Describe("Process resources", Ordered, func() { @@ -124,7 +154,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 +184,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}, }, }, }, @@ -178,7 +212,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(gc) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ { Hostname: "foo.example.com", PathRules: []state.PathRule{ @@ -195,7 +229,26 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + SSLServers: []state.VirtualServer{ + { + 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 +261,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}, }, }, }, @@ -241,7 +299,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(hr1Updated) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ { Hostname: "foo.example.com", PathRules: []state.PathRule{ @@ -258,6 +316,24 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + SSLServers: []state.VirtualServer{ + { + 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 +347,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}, }, }, }, @@ -304,9 +385,27 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(gw1Updated) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ + { + Hostname: "foo.example.com", + PathRules: []state.PathRule{ + { + Path: "/", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: hr1Updated, + }, + }, + }, + }, + }, + }, + SSLServers: []state.VirtualServer{ { Hostname: "foo.example.com", + SSL: &state.SSL{CertificatePath: certificatePath}, PathRules: []state.PathRule{ { Path: "/", @@ -334,13 +433,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}, }, }, }, @@ -367,9 +471,27 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(gcUpdated) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ + { + Hostname: "foo.example.com", + PathRules: []state.PathRule{ + { + Path: "/", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: hr1Updated, + }, + }, + }, + }, + }, + }, + SSLServers: []state.VirtualServer{ { Hostname: "foo.example.com", + SSL: &state.SSL{CertificatePath: certificatePath}, PathRules: []state.PathRule{ { Path: "/", @@ -397,13 +519,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}, }, }, }, @@ -427,7 +554,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(gw2) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ { Hostname: "foo.example.com", PathRules: []state.PathRule{ @@ -444,6 +571,26 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + SSLServers: []state.VirtualServer{ + { + 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 +604,10 @@ var _ = Describe("ChangeProcessor", func() { Valid: true, AttachedRoutes: 1, }, + "listener-443-1": { + Valid: true, + AttachedRoutes: 1, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{ @@ -467,7 +618,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}, }, }, }, @@ -483,7 +635,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(hr2) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{ + HTTPServers: []state.VirtualServer{ { Hostname: "foo.example.com", PathRules: []state.PathRule{ @@ -500,6 +652,24 @@ var _ = Describe("ChangeProcessor", func() { }, }, }, + SSLServers: []state.VirtualServer{ + { + 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 +683,10 @@ var _ = Describe("ChangeProcessor", func() { Valid: true, AttachedRoutes: 1, }, + "listener-443-1": { + Valid: true, + AttachedRoutes: 1, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{ @@ -523,12 +697,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}, }, }, }, @@ -544,9 +720,27 @@ 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{ + { + Path: "/", + MatchRules: []state.MatchRule{ + { + MatchIdx: 0, + RuleIdx: 0, + Source: hr2, + }, + }, + }, + }, + }, + }, + SSLServers: []state.VirtualServer{ { Hostname: "bar.example.com", + SSL: &state.SSL{CertificatePath: certificatePath}, PathRules: []state.PathRule{ { Path: "/", @@ -574,13 +768,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 +795,8 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureDeleteChange(&v1alpha2.HTTPRoute{}, types.NamespacedName{Namespace: "test", Name: "hr-2"}) expectedConf := state.Configuration{ - HTTPServers: []state.HTTPServer{}, + HTTPServers: []state.VirtualServer{}, + SSLServers: []state.VirtualServer{}, } expectedStatuses := state.Statuses{ GatewayClassStatus: &state.GatewayClassStatus{ @@ -610,6 +810,10 @@ var _ = Describe("ChangeProcessor", func() { Valid: true, AttachedRoutes: 0, }, + "listener-443-1": { + Valid: true, + AttachedRoutes: 0, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{}, @@ -634,6 +838,10 @@ var _ = Describe("ChangeProcessor", func() { Valid: false, AttachedRoutes: 0, }, + "listener-443-1": { + Valid: false, + AttachedRoutes: 0, + }, }, }, IgnoredGatewayStatuses: map[types.NamespacedName]state.IgnoredGatewayStatus{}, @@ -680,13 +888,16 @@ var _ = Describe("ChangeProcessor", func() { Describe("Edge cases with panic", func() { var processor state.ChangeProcessor + var fakeSecretMemoryMgr *statefakes.FakeSecretDiskMemoryManager BeforeEach(func() { - cfg := state.ChangeProcessorConfig{ - GatewayCtlrName: "test.controller", - GatewayClassName: "my-class", - } - processor = state.NewChangeProcessorImpl(cfg) + fakeSecretMemoryMgr = &statefakes.FakeSecretDiskMemoryManager{} + + 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..034909a9d5 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" @@ -12,15 +13,25 @@ import ( type Configuration struct { // HTTPServers holds all HTTPServers. // FIXME(pleshakov) We assume that all servers are HTTP and listen on port 80. - HTTPServers []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. 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,120 @@ 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 + // only upsert listeners that are valid + if l.Valid { + 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 *virtualServerBuilder + ssl *virtualServerBuilder +} + +func newConfigBuilder() *configBuilder { + return &configBuilder{ + http: newVirtualServerBuilder(), + ssl: newVirtualServerBuilder(), + } +} + +func (b *configBuilder) upsertListener(l *listener) { + switch l.Source.Protocol { + case v1alpha2.HTTPProtocolType: + b.http.upsertListener(l) + case v1alpha2.HTTPSProtocolType: + b.ssl.upsertListener(l) + default: + panic(fmt.Sprintf("listener protocol %s not supported", l.Source.Protocol)) + } +} + +func (b *configBuilder) build() Configuration { + return Configuration{ + HTTPServers: b.http.build(), + SSLServers: b.ssl.build(), + } +} + +type virtualServerBuilder struct { + rulesPerHost map[string]map[string]PathRule + listenersForHost map[string]*listener +} + +func newVirtualServerBuilder() *virtualServerBuilder { + return &virtualServerBuilder{ + rulesPerHost: make(map[string]map[string]PathRule), + listenersForHost: make(map[string]*listener), + } +} + +func (b *virtualServerBuilder) 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 { + b.listenersForHost[h] = l + if _, exist := b.rulesPerHost[h]; !exist { + b.rulesPerHost[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 := b.rulesPerHost[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 - } + b.rulesPerHost[h][path] = rule } } } } +} - httpServers := make([]HTTPServer, 0, len(pathRulesForHosts)) +func (b *virtualServerBuilder) build() []VirtualServer { - for h, rules := range pathRulesForHosts { - s := HTTPServer{ + servers := make([]VirtualServer, 0, len(b.rulesPerHost)) + + for h, rules := range b.rulesPerHost { + s := VirtualServer{ Hostname: h, PathRules: make([]PathRule, 0, len(rules)), } + l, ok := b.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 +195,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..dde277d90f 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,17 @@ func TestBuildConfiguration(t *testing.T) { InvalidSectionNameRefs: map[string]struct{}{}, } - hr4 := createRoute("hr-4", "foo.example.com", "/fourth", "/") + httpsHR3 := createRoute("https-hr-3", "foo.example.com", "listener-443-1", "/", "/third") + + httpsRouteHR3 := &route{ + Source: httpsHR3, + ValidSectionNameRefs: map[string]struct{}{ + "listener-443-1": {}, + }, + InvalidSectionNameRefs: map[string]struct{}{}, + } + + hr4 := createRoute("hr-4", "foo.example.com", "listener-80-1", "/fourth", "/") routeHR4 := &route{ Source: hr4, @@ -88,6 +118,51 @@ func TestBuildConfiguration(t *testing.T) { InvalidSectionNameRefs: map[string]struct{}{}, } + httpsHR4 := createRoute("https-hr-4", "foo.example.com", "listener-443-1", "/fourth", "/") + + httpsRouteHR4 := &route{ + Source: httpsHR4, + ValidSectionNameRefs: map[string]struct{}{ + "listener-80-1": {}, + }, + 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")), + }, + }, + }, + } + + 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" + tests := []struct { graph *graph expected Configuration @@ -106,7 +181,8 @@ func TestBuildConfiguration(t *testing.T) { Routes: map[types.NamespacedName]*route{}, }, expected: Configuration{ - HTTPServers: []HTTPServer{}, + HTTPServers: []VirtualServer{}, + SSLServers: []VirtualServer{}, }, msg: "no listeners and routes", }, @@ -120,18 +196,62 @@ 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: []VirtualServer{}, + SSLServers: []VirtualServer{}, }, - msg: "listener with no routes", + 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: []VirtualServer{}, + SSLServers: []VirtualServer{}, + }, + msg: "invalid listener", }, { graph: &graph{ @@ -143,7 +263,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,15 +274,30 @@ 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{ - HTTPServers: []HTTPServer{ + HTTPServers: []VirtualServer{ { Hostname: "bar.example.com", PathRules: []PathRule{ @@ -193,8 +329,46 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, + SSLServers: []VirtualServer{ + { + 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 +380,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,15 +390,29 @@ 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{ - HTTPServers: []HTTPServer{ + HTTPServers: []VirtualServer{ { Hostname: "foo.example.com", PathRules: []PathRule{ @@ -265,8 +454,53 @@ func TestBuildConfiguration(t *testing.T) { }, }, }, + SSLServers: []VirtualServer{ + { + 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 +513,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 +538,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/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/graph.go b/internal/state/graph.go index 407990e783..80525cc53f 100644 --- a/internal/state/graph.go +++ b/internal/state/graph.go @@ -16,21 +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 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 - // 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. @@ -70,12 +55,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 SecretDiskMemoryManager, +) *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 { @@ -151,6 +141,23 @@ func buildGatewayClass(gc *v1alpha2.GatewayClass, controllerName string) *gatewa } } +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 { + 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. @@ -277,46 +284,6 @@ 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 - listeners := make(map[string]*listener) - - if gw == nil || string(gw.Spec.GatewayClassName) != gcName { - return listeners - } - - usedListenerHostnames := make(map[string]*listener) - - for _, gl := range gw.Spec.Listeners { - valid := validateListener(gl) - - h := getHostname(gl.Hostname) - - // 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 - } - - l := &listener{ - Source: gl, - Valid: valid, - Routes: make(map[types.NamespacedName]*route), - AcceptedHostnames: make(map[string]struct{}), - } - - listeners[string(gl.Name)] = l - usedListenerHostnames[h] = l - } - - return listeners -} - -func validateListener(listener v1alpha2.Listener) bool { - // FIXME(pleshakov) For now, only support HTTP on port 80. - return listener.Protocol == v1alpha2.HTTPProtocolType && listener.Port == 80 -} - func getHostname(h *v1alpha2.Hostname) string { if h == nil { return "" diff --git a/internal/state/graph_test.go b/internal/state/graph_test.go index e8b0e8315b..fe4922ea45 100644 --- a/internal/state/graph_test.go +++ b/internal/state/graph_test.go @@ -1,9 +1,11 @@ +// nolint:gosec package state 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 +13,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 +93,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 +114,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 +134,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 +172,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 +183,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 +210,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 +228,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 +401,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 +447,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 +466,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 +511,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 +561,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,8 +604,22 @@ 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", + msg: "collisions", }, { gateway: nil, @@ -389,6 +628,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 +643,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,46 +985,6 @@ func TestFindAcceptedHostnames(t *testing.T) { } -func TestValidateListener(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", - }, - { - l: v1alpha2.Listener{ - Port: 80, - Protocol: v1alpha2.TCPProtocolType, - }, - expected: false, - msg: "invalid protocol", - }, - } - - for _, test := range tests { - result := validateListener(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 TestGetHostname(t *testing.T) { var emptyHostname v1alpha2.Hostname var hostname v1alpha2.Hostname = "example.com" diff --git a/internal/state/listener.go b/internal/state/listener.go new file mode 100644 index 0000000000..38a6501921 --- /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 SecretDiskMemoryManager) *listenerConfiguratorFactory { + return &listenerConfiguratorFactory{ + https: newHTTPSListenerConfigurator(gw, secretMemoryMgr), + http: newHTTPListenerConfigurator(), + } +} + +type httpsListenerConfigurator struct { + gateway *v1alpha2.Gateway + secretMemoryMgr SecretDiskMemoryManager + usedHostnames map[string]*listener +} + +func newHTTPSListenerConfigurator(gateway *v1alpha2.Gateway, secretMemoryMgr SecretDiskMemoryManager) *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.Request(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 +} 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) + } + } +} diff --git a/internal/state/secrets.go b/internal/state/secrets.go new file mode 100644 index 0000000000..6563e64895 --- /dev/null +++ b/internal/state/secrets.go @@ -0,0 +1,215 @@ +package state + +import ( + "bytes" + "crypto/tls" + "fmt" + "io/fs" + "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 . 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 + +// SecretStore stores secrets. +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 SecretStoreImpl 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() *SecretStoreImpl { + return &SecretStoreImpl{ + secrets: make(map[types.NamespacedName]*Secret), + } +} + +func (s SecretStoreImpl) 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 SecretStoreImpl) Delete(nsname types.NamespacedName) { + delete(s.secrets, nsname) +} + +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. + Request(nsname types.NamespacedName) (string, error) + // WriteAllRequestedSecrets writes all requested secrets to disk. + 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 +} + +// FIXME(kate-osborn): Is it necessary to make this concurrent-safe? +type SecretDiskMemoryManagerImpl struct { + requestedSecrets map[types.NamespacedName]requestedSecret + secretStore SecretStore + fileManager FileManager + secretDirectory string +} + +type requestedSecret struct { + secret *apiv1.Secret + path string +} + +// 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) { + secret := s.secretStore.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 := requestedSecret{ + secret: secret.Secret, + path: path.Join(s.secretDirectory, generateFilepathForSecret(nsname)), + } + + s.requestedSecrets[nsname] = ss + + return ss.path, nil +} + +func (s *SecretDiskMemoryManagerImpl) WriteAllRequestedSecrets() error { + // Remove all existing secrets from secrets directory + 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 := s.fileManager.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.requestedSecrets { + + 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 = 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 = 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 + s.requestedSecrets = make(map[types.NamespacedName]requestedSecret) + + 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..997be60ecf --- /dev/null +++ b/internal/state/secrets_test.go @@ -0,0 +1,387 @@ +// nolint:gosec +package state_test + +import ( + "errors" + "io/fs" + "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("SecretDiskMemoryManager", func() { + var ( + fakeStore *statefakes.FakeSecretStore + memMgr state.SecretDiskMemoryManager + 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() { + testRequest := func(s *apiv1.Secret, expPath string, expErr bool) { + nsname := types.NamespacedName{Namespace: s.Namespace, Name: s.Name} + actualPath, err := memMgr.Request(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) + + testRequest(secret1, "", true) + }) + 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") + + testRequest(secret1, expectedPath, false) + }) + + 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") + + testRequest(secret2, expectedPath, false) + }) + + It("request should return an error and empty path when secret is invalid", func() { + fakeStore.GetReturns(&state.Secret{Secret: invalidSecretType, Valid: false}) + + testRequest(invalidSecretType, "", true) + }) + + It("should write all requested secrets", func() { + err := memMgr.WriteAllRequestedSecrets() + 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("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") + + testRequest(secret3, expectedPath, false) + }) + + It("should write all requested secrets", func() { + err := memMgr.WriteAllRequestedSecrets() + 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")) + }) + 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()) + }) + }) + }) + Describe("Write all requested secrets", func() { + var ( + fakeFileManager *statefakes.FakeFileManager + fakeStore *statefakes.FakeSecretStore + fakeFileInfoSlice []fs.FileInfo + memMgr *state.SecretDiskMemoryManagerImpl + ) + + 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()) + }) + + DescribeTable("error cases", Ordered, + func(e error, preparer func(e error)) { + preparer(e) + + err := memMgr.WriteAllRequestedSecrets() + 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) + }), + ) + }) +}) + +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()) + } else { + Expect(actualSecret.Valid).To(BeFalse()) + } + 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_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) diff --git a/internal/state/statefakes/fake_secret_disk_memory_manager.go b/internal/state/statefakes/fake_secret_disk_memory_manager.go new file mode 100644 index 0000000000..d7e604154d --- /dev/null +++ b/internal/state/statefakes/fake_secret_disk_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 FakeSecretDiskMemoryManager struct { + RequestStub func(types.NamespacedName) (string, error) + requestMutex sync.RWMutex + requestArgsForCall []struct { + arg1 types.NamespacedName + } + requestReturns struct { + result1 string + result2 error + } + requestReturnsOnCall map[int]struct { + result1 string + result2 error + } + WriteAllRequestedSecretsStub func() error + writeAllRequestedSecretsMutex sync.RWMutex + writeAllRequestedSecretsArgsForCall []struct { + } + writeAllRequestedSecretsReturns struct { + result1 error + } + writeAllRequestedSecretsReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +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.RequestStub + fakeReturns := fake.requestReturns + fake.recordInvocation("Request", []interface{}{arg1}) + fake.requestMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSecretDiskMemoryManager) RequestCallCount() int { + fake.requestMutex.RLock() + defer fake.requestMutex.RUnlock() + return len(fake.requestArgsForCall) +} + +func (fake *FakeSecretDiskMemoryManager) RequestCalls(stub func(types.NamespacedName) (string, error)) { + fake.requestMutex.Lock() + defer fake.requestMutex.Unlock() + fake.RequestStub = stub +} + +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) 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) 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.requestReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeSecretDiskMemoryManager) WriteAllRequestedSecrets() error { + fake.writeAllRequestedSecretsMutex.Lock() + ret, specificReturn := fake.writeAllRequestedSecretsReturnsOnCall[len(fake.writeAllRequestedSecretsArgsForCall)] + fake.writeAllRequestedSecretsArgsForCall = append(fake.writeAllRequestedSecretsArgsForCall, struct { + }{}) + stub := fake.WriteAllRequestedSecretsStub + fakeReturns := fake.writeAllRequestedSecretsReturns + fake.recordInvocation("WriteAllRequestedSecrets", []interface{}{}) + fake.writeAllRequestedSecretsMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeSecretDiskMemoryManager) WriteAllRequestedSecretsCallCount() int { + fake.writeAllRequestedSecretsMutex.RLock() + defer fake.writeAllRequestedSecretsMutex.RUnlock() + return len(fake.writeAllRequestedSecretsArgsForCall) +} + +func (fake *FakeSecretDiskMemoryManager) WriteAllRequestedSecretsCalls(stub func() error) { + fake.writeAllRequestedSecretsMutex.Lock() + defer fake.writeAllRequestedSecretsMutex.Unlock() + fake.WriteAllRequestedSecretsStub = stub +} + +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) 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.writeAllRequestedSecretsReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeSecretDiskMemoryManager) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.requestMutex.RLock() + defer fake.requestMutex.RUnlock() + fake.writeAllRequestedSecretsMutex.RLock() + defer fake.writeAllRequestedSecretsMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeSecretDiskMemoryManager) 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.SecretDiskMemoryManager = new(FakeSecretDiskMemoryManager) 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/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, }, }, 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..9f682d0893 --- /dev/null +++ b/pkg/sdk/secret_controller.go @@ -0,0 +1,62 @@ +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/reconcile" +) + +type secretReconciler struct { + client.Client + scheme *runtime.Scheme + impl SecretImpl +} + +// 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{}). + 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 +}