From cccba35005962f9eee96d2b592cc5b7b1485d965 Mon Sep 17 00:00:00 2001 From: Jintao Zhang Date: Wed, 14 Jun 2023 03:55:59 +0800 Subject: [PATCH] Revert "Remove fastcgi feature" (#10081) * Revert "Remove fastcgi feature (#9864)" This reverts commit 90ed0ccdbe4edd55ad4ab16ffec40be64f3308d9. * revert fastcgi* annotations warning Signed-off-by: Jintao Zhang --------- Signed-off-by: Jintao Zhang --- .github/workflows/ci.yaml | 6 + MANUAL_RELEASE.md | 2 + docs/e2e-tests.md | 131 +------- docs/kubectl-plugin.md | 1 + docs/user-guide/fcgi-services.md | 118 +++++++- .../nginx-configuration/annotations.md | 2 +- images/fastcgi-helloserver/Makefile | 59 ++++ images/fastcgi-helloserver/cloudbuild.yaml | 22 ++ images/fastcgi-helloserver/rootfs/Dockerfile | 32 ++ images/fastcgi-helloserver/rootfs/main.go | 30 ++ internal/ingress/annotations/annotations.go | 62 ++-- .../annotations/backendprotocol/main.go | 2 +- internal/ingress/annotations/fastcgi/main.go | 106 +++++++ .../ingress/annotations/fastcgi/main_test.go | 285 ++++++++++++++++++ internal/ingress/annotations/parser/main.go | 1 + internal/ingress/controller/controller.go | 5 +- .../ingress/controller/controller_test.go | 2 +- .../ingress/controller/template/template.go | 3 + .../controller/template/template_test.go | 2 + pkg/apis/ingress/types.go | 4 + pkg/apis/ingress/types_equals.go | 4 + rootfs/etc/nginx/template/nginx.tmpl | 11 + test/e2e/annotations/backendprotocol.go | 15 + test/e2e/annotations/fastcgi.go | 125 ++++++++ test/e2e/framework/fastcgi_helloserver.go | 104 +++++++ 25 files changed, 975 insertions(+), 159 deletions(-) create mode 100644 images/fastcgi-helloserver/Makefile create mode 100644 images/fastcgi-helloserver/cloudbuild.yaml create mode 100755 images/fastcgi-helloserver/rootfs/Dockerfile create mode 100644 images/fastcgi-helloserver/rootfs/main.go create mode 100644 internal/ingress/annotations/fastcgi/main.go create mode 100644 internal/ingress/annotations/fastcgi/main_test.go create mode 100644 test/e2e/annotations/fastcgi.go create mode 100644 test/e2e/framework/fastcgi_helloserver.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index abb2af5326..9764a5e747 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -391,6 +391,8 @@ jobs: - 'images/custom-error-pages/**' cfssl: - 'images/cfssl/**' + fastcgi-helloserver: + - 'images/fastcgi-helloserver/**' echo: - 'images/echo/**' go-grpc-greeter-server: @@ -410,6 +412,10 @@ jobs: if: ${{ steps.filter-images.outputs.cfssl == 'true' }} run: | cd images/cfssl && make build + - name: fastcgi-helloserver + if: ${{ steps.filter-images.outputs.fastcgi-helloserver == 'true' }} + run: | + cd images/fastcgi-helloserver && make build - name: echo image build if: ${{ steps.filter-images.outputs.echo == 'true' }} run: | diff --git a/MANUAL_RELEASE.md b/MANUAL_RELEASE.md index 4b4df0ec6c..d7144b85d4 100644 --- a/MANUAL_RELEASE.md +++ b/MANUAL_RELEASE.md @@ -56,6 +56,8 @@ - [cfssl](https://github.com/kubernetes/ingress-nginx/tree/main/images/cfssl) + - [fastcgi-helloserver](https://github.com/kubernetes/ingress-nginx/tree/main/images/fastcgi-helloserver) + - [httpbin](https://github.com/kubernetes/ingress-nginx/tree/main/images/httpbin) - [kube-webhook-certgen](https://github.com/kubernetes/ingress-nginx/tree/main/images/kube-webhook-certgen) diff --git a/docs/e2e-tests.md b/docs/e2e-tests.md index 64d35c79a2..025ff686de 100644 --- a/docs/e2e-tests.md +++ b/docs/e2e-tests.md @@ -108,6 +108,7 @@ Do not try to edit it manually. - [should set backend protocol to $scheme:// and use proxy_pass](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/backendprotocol.go#L49) - [should set backend protocol to grpc:// and use grpc_pass](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/backendprotocol.go#L64) - [should set backend protocol to grpcs:// and use grpc_pass](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/backendprotocol.go#L79) +- [should set backend protocol to '' and use fastcgi_pass](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/backendprotocol.go#L94) - [should set backend protocol to '' and use ajp_pass](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/backendprotocol.go#L109) ### [canary-*](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L36) @@ -191,6 +192,13 @@ Do not try to edit it manually. - [disable-http-access-log set access_log off](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/disableaccesslog.go#L53) - [disable-stream-access-log set access_log off](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/disableaccesslog.go#L71) +### [backend-protocol - FastCGI](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/fastcgi.go#L30) + +- [should use fastcgi_pass in the configuration file](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/fastcgi.go#L37) +- [should add fastcgi_index in the configuration file](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/fastcgi.go#L54) +- [should add fastcgi_param in the configuration file](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/fastcgi.go#L71) +- [should return OK for service with backend protocol FastCGI](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/fastcgi.go#L102) + ### [force-ssl-redirect](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/forcesslredirect.go#L27) - [should redirect to https](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/forcesslredirect.go#L34) @@ -342,101 +350,6 @@ Do not try to edit it manually. - [should set the X-Forwarded-Prefix to the annotation value](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/xforwardedprefix.go#L35) - [should not add X-Forwarded-Prefix if the annotation value is empty](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/xforwardedprefix.go#L57) -### [denylist-source-range](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/ipdenylist.go#L28) - -- [only deny explicitly denied IPs, allow all others](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/ipdenylist.go#L35) -- [only allow explicitly allowed IPs, deny all others](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/ipdenylist.go#L86) - -### [affinity session-cookie-name](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L35) - -- [should set sticky cookie SERVERID](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L42) -- [should change cookie name on ingress definition change](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L64) -- [should set the path to /something on the generated cookie](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L99) -- [does not set the path to / on the generated cookie if there's more than one rule referring to the same backend](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L121) -- [should set cookie with expires](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L194) -- [should set cookie with domain](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L225) -- [should not set cookie without domain annotation](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L248) -- [should work with use-regex annotation and session-cookie-path](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L270) -- [should warn user when use-regex is true and session-cookie-path is not set](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L294) -- [should not set affinity across all server locations when using separate ingresses](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L320) -- [should set sticky cookie without host](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L352) -- [should work with server-alias annotation](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L372) -- [should set secure in cookie with provided true annotation on http](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L412) -- [should not set secure in cookie with provided false annotation on http](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L435) -- [should set secure in cookie with provided false annotation on https](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/affinity.go#L458) - -### [rewrite-target use-regex enable-rewrite-log](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/rewrite.go#L30) - -- [should write rewrite logs](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/rewrite.go#L37) -- [should use correct longest path match](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/rewrite.go#L66) -- [should use ~* location modifier if regex annotation is present](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/rewrite.go#L111) -- [should fail to use longest match for documented warning](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/rewrite.go#L158) -- [should allow for custom rewrite parameters](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/rewrite.go#L190) - -### [auth-*](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L39) - -- [should return status code 200 when no authentication is configured](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L46) -- [should return status code 503 when authentication is configured with an invalid secret](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L65) -- [should return status code 401 when authentication is configured but Authorization header is not configured](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L89) -- [should return status code 401 when authentication is configured and Authorization header is sent with invalid credentials](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L116) -- [should return status code 401 and cors headers when authentication and cors is configured but Authorization header is not configured](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L144) -- [should return status code 200 when authentication is configured and Authorization header is sent](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L172) -- [should return status code 200 when authentication is configured with a map and Authorization header is sent](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L199) -- [should return status code 401 when authentication is configured with invalid content and Authorization header is sent](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L227) -- [ when external auth is configured](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L266) -- [ when external auth is not configured](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L284) -- [ when auth-headers are set](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L301) -- [should set cache_key when external auth cache is configured](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L322) -- [user retains cookie by default](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L411) -- [user does not retain cookie if upstream returns error status code](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L422) -- [user with annotated ingress retains cookie if upstream returns error status code](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L433) -- [should return status code 200 when signed in](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L485) -- [should redirect to signin url when not signed in](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L494) -- [keeps processing new ingresses even if one of the existing ingresses is misconfigured](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L505) -- [should overwrite Foo header with auth response](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L529) -- [should not create additional upstream block when auth-keepalive is not set](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L552) -- [should not create additional upstream block when host part of auth-url contains a variable](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L570) -- [should not create additional upstream block when auth-keepalive is negative](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L590) -- [should not create additional upstream block when auth-keepalive is set with HTTP/2](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L609) -- [should create additional upstream block when auth-keepalive is set with HTTP/1.x](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L623) -- [should return status code 200 when signed in](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L678) -- [should redirect to signin url when not signed in](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L687) -- [keeps processing new ingresses even if one of the existing ingresses is misconfigured](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L698) -- [should return status code 200 when signed in after auth backend is deleted ](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L772) -- [should deny login for different location on same server](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L792) -- [should deny login for different servers](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L820) -- [should redirect to signin url when not signed in](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L849) -- [should return 503 (location was denied)](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L879) -- [should add error to the config](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/auth.go#L887) - -### [canary-*](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L36) - -- [should response with a 200 status from the mainline upstream when requests are made to the mainline ingress](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L48) -- [should return 404 status for requests to the canary if no matching ingress is found](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L80) -- [should return the correct status codes when endpoints are unavailable](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L107) -- [should route requests to the correct upstream if mainline ingress is created before the canary ingress](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L161) -- [should route requests to the correct upstream if mainline ingress is created after the canary ingress](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L206) -- [should route requests to the correct upstream if the mainline ingress is modified](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L250) -- [should route requests to the correct upstream if the canary ingress is modified](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L307) -- [should route requests to the correct upstream](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L372) -- [should route requests to the correct upstream](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L426) -- [should route requests to the correct upstream](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L490) -- [should route requests to the correct upstream](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L532) -- [should routes to mainline upstream when the given Regex causes error](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L566) -- [should route requests to the correct upstream](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L604) -- [respects always and never values](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L643) -- [should route requests only to mainline if canary weight is 0](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L705) -- [should route requests only to canary if canary weight is 100](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L743) -- [should route requests only to canary if canary weight is equal to canary weight total](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L775) -- [should route requests split between mainline and canary if canary weight is 50](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L808) -- [should route requests split between mainline and canary if canary weight is 100 and weight total is 200](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L834) -- [should not use canary as a catch-all server](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L863) -- [should not use canary with domain as a server](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L891) -- [does not crash when canary ingress has multiple paths to the same non-matching backend](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L915) -- [always routes traffic to canary if first request was affinitized to canary (default behavior)](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L943) -- [always routes traffic to canary if first request was affinitized to canary (explicit sticky behavior)](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L1000) -- [routes traffic to either mainline or canary backend (legacy behavior)](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/annotations/canary.go#L1058) - ### [Debug CLI](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/dbg/main.go#L29) - [should list the backend servers](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/dbg/main.go#L37) @@ -485,6 +398,10 @@ Do not try to edit it manually. - [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/exec.go#L) +### [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/fastcgi_helloserver.go#L) + +- [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/fastcgi_helloserver.go#L) + ### [[Setting] ](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/framework.go#L194) - [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/framework.go#L) @@ -561,30 +478,6 @@ Do not try to edit it manually. - [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/util.go#L) -### [[Setting] ](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/framework.go#L194) - -- [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/framework.go#L) - -### [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/healthz.go#L) - -- [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/healthz.go#L) - -### [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/k8s.go#L) - -- [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/k8s.go#L) - -### [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/metrics.go#L) - -- [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/metrics.go#L) - -### [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/deployment.go#L) - -- [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/deployment.go#L) - -### [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/exec.go#L) - -- [](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/framework/exec.go#L) - ### [[Shutdown] Grace period shutdown](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/gracefulshutdown/grace_period.go#L32) - [/healthz should return status code 500 during shutdown grace period](https://github.com/kubernetes/ingress-nginx/tree/main/test/e2e/gracefulshutdown/grace_period.go#L35) diff --git a/docs/kubectl-plugin.md b/docs/kubectl-plugin.md index 9dc808642a..9e5a5dcc6d 100644 --- a/docs/kubectl-plugin.md +++ b/docs/kubectl-plugin.md @@ -200,6 +200,7 @@ kubectl ingress-nginx conf -n ingress-nginx --host testaddr.local ```console $ kubectl ingress-nginx exec -i -n ingress-nginx -- ls /etc/nginx +fastcgi_params geoip lua mime.types diff --git a/docs/user-guide/fcgi-services.md b/docs/user-guide/fcgi-services.md index d4f1d1f1ea..db4d9428bc 100644 --- a/docs/user-guide/fcgi-services.md +++ b/docs/user-guide/fcgi-services.md @@ -2,10 +2,118 @@ # Exposing FastCGI Servers -**This feature has been removed from Ingress NGINX** +> **FastCGI** is a [binary protocol](https://en.wikipedia.org/wiki/Binary_protocol "Binary protocol") for interfacing interactive programs with a [web server](https://en.wikipedia.org/wiki/Web_server "Web server"). [...] (It's) aim is to reduce the overhead related to interfacing between web server and CGI programs, allowing a server to handle more web page requests per unit of time. +> +> — Wikipedia -People willing to use fastcgi servers, should create an NGINX + FastCGI service and expose -this service via Ingress NGINX. +The _ingress-nginx_ ingress controller can be used to directly expose [FastCGI](https://en.wikipedia.org/wiki/FastCGI) servers. Enabling FastCGI in your Ingress only requires setting the _backend-protocol_ annotation to `FCGI`, and with a couple more annotations you can customize the way _ingress-nginx_ handles the communication with your FastCGI _server_. -We recommend using images like `cgr.dev/chainguard/nginx:latest` and expose your fast_cgi application -as another container on this Pod. + +## Example Objects to Expose a FastCGI Pod + +The _Pod_ example object below exposes port `9000`, which is the conventional FastCGI port. + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: example-app +labels: + app: example-app +spec: + containers: + - name: example-app + image: example-app:1.0 + ports: + - containerPort: 9000 + name: fastcgi +``` + +The _Service_ object example below matches port `9000` from the _Pod_ object above. + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: example-service +spec: + selector: + app: example-app + ports: + - port: 9000 + targetPort: 9000 + name: fastcgi +``` + +And the _Ingress_ and _ConfigMap_ objects below demonstrates the supported _FastCGI_ specific annotations (NGINX actually has 50 FastCGI directives, all of which have not been exposed in the ingress yet), and matches the service `example-service`, and the port named `fastcgi` from above. The _ConfigMap_ **must** be created first for the _Ingress Controller_ to be able to find it when the _Ingress_ object is created, otherwise you will need to restart the _Ingress Controller_ pods. + +```yaml +# The ConfigMap MUST be created first for the ingress controller to be able to +# find it when the Ingress object is created. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: example-cm +data: + SCRIPT_FILENAME: "/example/index.php" + +--- + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + nginx.ingress.kubernetes.io/backend-protocol: "FCGI" + nginx.ingress.kubernetes.io/fastcgi-index: "index.php" + nginx.ingress.kubernetes.io/fastcgi-params-configmap: "example-cm" + name: example-app +spec: + ingressClassName: nginx + rules: + - host: app.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: example-service + port: + name: fastcgi +``` + +## FastCGI Ingress Annotations + +To enable FastCGI, the `nginx.ingress.kubernetes.io/backend-protocol` annotation needs to be set to `FCGI`, which overrides the default `HTTP` value. + +> `nginx.ingress.kubernetes.io/backend-protocol: "FCGI"` + +**This enables the _FastCGI_ mode for all paths defined in the _Ingress_ object** + +### The `nginx.ingress.kubernetes.io/fastcgi-index` Annotation + +To specify an index file, the `fastcgi-index` annotation value can optionally be set. In the example below, the value is set to `index.php`. This annotation corresponds to [the _NGINX_ `fastcgi_index` directive](https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_index). + +> `nginx.ingress.kubernetes.io/fastcgi-index: "index.php"` + +### The `nginx.ingress.kubernetes.io/fastcgi-params-configmap` Annotation + +To specify [_NGINX_ `fastcgi_param` directives](https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_param), the `fastcgi-params-configmap` annotation is used, which in turn must lead to a _ConfigMap_ object containing the _NGINX_ `fastcgi_param` directives as key/values. + +> `nginx.ingress.kubernetes.io/fastcgi-params-configmap: "example-configmap"` + +And the _ConfigMap_ object to specify the `SCRIPT_FILENAME` and `HTTP_PROXY` _NGINX's_ `fastcgi_param` directives will look like the following: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: example-configmap +data: + SCRIPT_FILENAME: "/example/index.php" + HTTP_PROXY: "" +``` +Using the _namespace/_ prefix is also supported, for example: + +> `nginx.ingress.kubernetes.io/fastcgi-params-configmap: "example-namespace/example-configmap"` diff --git a/docs/user-guide/nginx-configuration/annotations.md b/docs/user-guide/nginx-configuration/annotations.md index fc43a47b21..b515a9f3b5 100755 --- a/docs/user-guide/nginx-configuration/annotations.md +++ b/docs/user-guide/nginx-configuration/annotations.md @@ -894,7 +894,7 @@ Include /etc/nginx/owasp-modsecurity-crs/nginx-modsecurity.conf ### Backend Protocol Using `backend-protocol` annotations is possible to indicate how NGINX should communicate with the backend service. (Replaces `secure-backends` in older versions) -Valid Values: HTTP, HTTPS, GRPC, GRPCS, AJP +Valid Values: HTTP, HTTPS, GRPC, GRPCS, AJP and FCGI By default NGINX uses `HTTP`. diff --git a/images/fastcgi-helloserver/Makefile b/images/fastcgi-helloserver/Makefile new file mode 100644 index 0000000000..4d277a19d9 --- /dev/null +++ b/images/fastcgi-helloserver/Makefile @@ -0,0 +1,59 @@ +# Copyright 2020 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Docker image for e2e testing. + +# set default shell +SHELL=/bin/bash -o pipefail -o errexit + +DIR:=$(strip $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))) +INIT_BUILDX=$(DIR)/../../hack/init-buildx.sh + +SHORT_SHA ?=$(shell git rev-parse --short HEAD) +TAG ?=v$(shell date +%Y%m%d)-$(SHORT_SHA) + +REGISTRY ?= local + +IMAGE = $(REGISTRY)/e2e-test-fastcgi-helloserver + +# required to enable buildx +export DOCKER_CLI_EXPERIMENTAL=enabled + +# build with buildx +PLATFORMS?=linux/amd64,linux/arm,linux/arm64 +OUTPUT= +PROGRESS=plain + +build: ensure-buildx + docker buildx build \ + --platform=${PLATFORMS} $(OUTPUT) \ + --progress=$(PROGRESS) \ + --pull \ + -t $(IMAGE):$(TAG) rootfs + +# push the cross built image +push: OUTPUT=--push +push: build + +# enable buildx +ensure-buildx: +# this is required for cloudbuild +ifeq ("$(wildcard $(INIT_BUILDX))","") + @curl -sSL https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/hack/init-buildx.sh | bash +else + @exec $(INIT_BUILDX) +endif + @echo "done" + +.PHONY: build push ensure-buildx diff --git a/images/fastcgi-helloserver/cloudbuild.yaml b/images/fastcgi-helloserver/cloudbuild.yaml new file mode 100644 index 0000000000..a4c9d5effc --- /dev/null +++ b/images/fastcgi-helloserver/cloudbuild.yaml @@ -0,0 +1,22 @@ +timeout: 1800s +options: + substitution_option: ALLOW_LOOSE +steps: + - name: gcr.io/k8s-staging-test-infra/gcb-docker-gcloud:v20211118-2f2d816b90 + entrypoint: bash + env: + - DOCKER_CLI_EXPERIMENTAL=enabled + - SHORT_SHA=$SHORT_SHA + - BASE_REF=$_PULL_BASE_REF + - REGISTRY=gcr.io/k8s-staging-ingress-nginx + # default cloudbuild has HOME=/builder/home and docker buildx is in /root/.docker/cli-plugins/docker-buildx + # set the home to /root explicitly to if using docker buildx + - HOME=/root + args: + - -c + - | + gcloud auth configure-docker \ + && cd images/fastcgi-helloserver && make push +substitutions: + _GIT_TAG: "12345" + _PULL_BASE_REF: "master" diff --git a/images/fastcgi-helloserver/rootfs/Dockerfile b/images/fastcgi-helloserver/rootfs/Dockerfile new file mode 100755 index 0000000000..a118343733 --- /dev/null +++ b/images/fastcgi-helloserver/rootfs/Dockerfile @@ -0,0 +1,32 @@ +# Copyright 2017 The Kubernetes Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM golang:1.20.4-alpine3.18 as builder + +WORKDIR /go/src/k8s.io/ingress-nginx/images/fastcgi + +COPY . . + +RUN CGO_ENABLED=0 go build -a -installsuffix cgo \ + -ldflags "-s -w" \ + -o fastcgi-helloserver main.go + +# Use distroless as minimal base image to package the binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot + +COPY --from=builder /go/src/k8s.io/ingress-nginx/images/fastcgi/fastcgi-helloserver / +USER nonroot:nonroot + +CMD ["/fastcgi-helloserver"] diff --git a/images/fastcgi-helloserver/rootfs/main.go b/images/fastcgi-helloserver/rootfs/main.go new file mode 100644 index 0000000000..91db60c269 --- /dev/null +++ b/images/fastcgi-helloserver/rootfs/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "net" + "net/http" + "net/http/fcgi" +) + +func hello(w http.ResponseWriter, r *http.Request) { + keys, ok := r.URL.Query()["name"] + + if !ok || len(keys[0]) < 1 { + fmt.Fprintf(w, "Hello world!") + return + } + + key := keys[0] + fmt.Fprintf(w, "Hello "+string(key)+"!") +} + +func main() { + http.HandleFunc("/hello", hello) + + l, err := net.Listen("tcp", "0.0.0.0:9000") + if err != nil { + panic(err) + } + fcgi.Serve(l, nil) +} diff --git a/internal/ingress/annotations/annotations.go b/internal/ingress/annotations/annotations.go index ad7dad37d7..5bb2bf5e60 100644 --- a/internal/ingress/annotations/annotations.go +++ b/internal/ingress/annotations/annotations.go @@ -41,6 +41,7 @@ import ( "k8s.io/ingress-nginx/internal/ingress/annotations/cors" "k8s.io/ingress-nginx/internal/ingress/annotations/customhttperrors" "k8s.io/ingress-nginx/internal/ingress/annotations/defaultbackend" + "k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi" "k8s.io/ingress-nginx/internal/ingress/annotations/globalratelimit" "k8s.io/ingress-nginx/internal/ingress/annotations/http2pushpreload" "k8s.io/ingress-nginx/internal/ingress/annotations/ipdenylist" @@ -85,35 +86,37 @@ type Ingress struct { CorsConfig cors.Config CustomHTTPErrors []int DefaultBackend *apiv1.Service - Denied *string - ExternalAuth authreq.Config - EnableGlobalAuth bool - HTTP2PushPreload bool - Opentracing opentracing.Config - Opentelemetry opentelemetry.Config - Proxy proxy.Config - ProxySSL proxyssl.Config - RateLimit ratelimit.Config - GlobalRateLimit globalratelimit.Config - Redirect redirect.Config - Rewrite rewrite.Config - Satisfy string - ServerSnippet string - ServiceUpstream bool - SessionAffinity sessionaffinity.Config - SSLPassthrough bool - UsePortInRedirects bool - UpstreamHashBy upstreamhashby.Config - LoadBalancing string - UpstreamVhost string - Whitelist ipwhitelist.SourceRange - Denylist ipdenylist.SourceRange - XForwardedPrefix string - SSLCipher sslcipher.Config - Logs log.Config - ModSecurity modsecurity.Config - Mirror mirror.Config - StreamSnippet string + //TODO: Change this back into an error when https://github.com/imdario/mergo/issues/100 is resolved + FastCGI fastcgi.Config + Denied *string + ExternalAuth authreq.Config + EnableGlobalAuth bool + HTTP2PushPreload bool + Opentracing opentracing.Config + Opentelemetry opentelemetry.Config + Proxy proxy.Config + ProxySSL proxyssl.Config + RateLimit ratelimit.Config + GlobalRateLimit globalratelimit.Config + Redirect redirect.Config + Rewrite rewrite.Config + Satisfy string + ServerSnippet string + ServiceUpstream bool + SessionAffinity sessionaffinity.Config + SSLPassthrough bool + UsePortInRedirects bool + UpstreamHashBy upstreamhashby.Config + LoadBalancing string + UpstreamVhost string + Whitelist ipwhitelist.SourceRange + Denylist ipdenylist.SourceRange + XForwardedPrefix string + SSLCipher sslcipher.Config + Logs log.Config + ModSecurity modsecurity.Config + Mirror mirror.Config + StreamSnippet string } // Extractor defines the annotation parsers to be used in the extraction of annotations @@ -135,6 +138,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor { "CorsConfig": cors.NewParser(cfg), "CustomHTTPErrors": customhttperrors.NewParser(cfg), "DefaultBackend": defaultbackend.NewParser(cfg), + "FastCGI": fastcgi.NewParser(cfg), "ExternalAuth": authreq.NewParser(cfg), "EnableGlobalAuth": authreqglobal.NewParser(cfg), "HTTP2PushPreload": http2pushpreload.NewParser(cfg), diff --git a/internal/ingress/annotations/backendprotocol/main.go b/internal/ingress/annotations/backendprotocol/main.go index 0140c30a30..d8ea72386f 100644 --- a/internal/ingress/annotations/backendprotocol/main.go +++ b/internal/ingress/annotations/backendprotocol/main.go @@ -31,7 +31,7 @@ import ( const HTTP = "HTTP" var ( - validProtocols = regexp.MustCompile(`^(AUTO_HTTP|HTTP|HTTPS|AJP|GRPC|GRPCS)$`) + validProtocols = regexp.MustCompile(`^(AUTO_HTTP|HTTP|HTTPS|AJP|GRPC|GRPCS|FCGI)$`) ) type backendProtocol struct { diff --git a/internal/ingress/annotations/fastcgi/main.go b/internal/ingress/annotations/fastcgi/main.go new file mode 100644 index 0000000000..84bac4109b --- /dev/null +++ b/internal/ingress/annotations/fastcgi/main.go @@ -0,0 +1,106 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fastcgi + +import ( + "fmt" + "reflect" + + networking "k8s.io/api/networking/v1" + "k8s.io/client-go/tools/cache" + + "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + ing_errors "k8s.io/ingress-nginx/internal/ingress/errors" + "k8s.io/ingress-nginx/internal/ingress/resolver" +) + +type fastcgi struct { + r resolver.Resolver +} + +// Config describes the per location fastcgi config +type Config struct { + Index string `json:"index"` + Params map[string]string `json:"params"` +} + +// Equal tests for equality between two Configuration types +func (l1 *Config) Equal(l2 *Config) bool { + if l1 == l2 { + return true + } + + if l1 == nil || l2 == nil { + return false + } + + if l1.Index != l2.Index { + return false + } + + return reflect.DeepEqual(l1.Params, l2.Params) +} + +// NewParser creates a new fastcgiConfig protocol annotation parser +func NewParser(r resolver.Resolver) parser.IngressAnnotation { + return fastcgi{r} +} + +// ParseAnnotations parses the annotations contained in the ingress +// rule used to indicate the fastcgiConfig. +func (a fastcgi) Parse(ing *networking.Ingress) (interface{}, error) { + + fcgiConfig := Config{} + + if ing.GetAnnotations() == nil { + return fcgiConfig, nil + } + + index, err := parser.GetStringAnnotation("fastcgi-index", ing) + if err != nil { + index = "" + } + fcgiConfig.Index = index + + cm, err := parser.GetStringAnnotation("fastcgi-params-configmap", ing) + if err != nil { + return fcgiConfig, nil + } + + cmns, cmn, err := cache.SplitMetaNamespaceKey(cm) + if err != nil { + return fcgiConfig, ing_errors.LocationDenied{ + Reason: fmt.Errorf("error reading configmap name from annotation: %w", err), + } + } + + if cmns != "" && cmns != ing.Namespace { + return fcgiConfig, fmt.Errorf("different namespace is not supported on fast_cgi param configmap") + } + + cm = fmt.Sprintf("%v/%v", ing.Namespace, cmn) + cmap, err := a.r.GetConfigMap(cm) + if err != nil { + return fcgiConfig, ing_errors.LocationDenied{ + Reason: fmt.Errorf("unexpected error reading configmap %s: %w", cm, err), + } + } + + fcgiConfig.Params = cmap.Data + + return fcgiConfig, nil +} diff --git a/internal/ingress/annotations/fastcgi/main_test.go b/internal/ingress/annotations/fastcgi/main_test.go new file mode 100644 index 0000000000..35c5bbc122 --- /dev/null +++ b/internal/ingress/annotations/fastcgi/main_test.go @@ -0,0 +1,285 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fastcgi + +import ( + "fmt" + "testing" + + api "k8s.io/api/core/v1" + networking "k8s.io/api/networking/v1" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/ingress-nginx/internal/ingress/annotations/parser" + "k8s.io/ingress-nginx/internal/ingress/resolver" +) + +func buildIngress() *networking.Ingress { + return &networking.Ingress{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "foo", + Namespace: api.NamespaceDefault, + }, + Spec: networking.IngressSpec{ + DefaultBackend: &networking.IngressBackend{ + Service: &networking.IngressServiceBackend{ + Name: "default-backend", + Port: networking.ServiceBackendPort{ + Number: 80, + }, + }, + }, + }, + } +} + +type mockConfigMap struct { + resolver.Mock +} + +func (m mockConfigMap) GetConfigMap(name string) (*api.ConfigMap, error) { + if name != "default/demo-configmap" && name != "otherns/demo-configmap" { + return nil, fmt.Errorf("there is no configmap with name %v", name) + } + + cmns, cmn, err := cache.SplitMetaNamespaceKey(name) + if err != nil { + return nil, fmt.Errorf("invalid configmap name") + } + + return &api.ConfigMap{ + ObjectMeta: meta_v1.ObjectMeta{ + Namespace: cmns, + Name: cmn, + }, + Data: map[string]string{"REDIRECT_STATUS": "200", "SERVER_NAME": "$server_name"}, + }, nil +} + +func TestParseEmptyFastCGIAnnotations(t *testing.T) { + ing := buildIngress() + + i, err := NewParser(&mockConfigMap{}).Parse(ing) + if err != nil { + t.Errorf("unexpected error parsing ingress without fastcgi") + } + + config, ok := i.(Config) + if !ok { + t.Errorf("Parse do not return a Config object") + } + + if config.Index != "" { + t.Errorf("Index should be an empty string") + } + + if len(config.Params) != 0 { + t.Errorf("Params should be an empty slice") + } +} + +func TestParseFastCGIIndexAnnotation(t *testing.T) { + ing := buildIngress() + + const expectedAnnotation = "index.php" + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix("fastcgi-index")] = expectedAnnotation + ing.SetAnnotations(data) + + i, err := NewParser(&mockConfigMap{}).Parse(ing) + if err != nil { + t.Errorf("unexpected error parsing ingress without fastcgi") + } + + config, ok := i.(Config) + if !ok { + t.Errorf("Parse do not return a Config object") + } + + if config.Index != "index.php" { + t.Errorf("expected %s but %v returned", expectedAnnotation, config.Index) + } +} + +func TestParseEmptyFastCGIParamsConfigMapAnnotation(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix("fastcgi-params-configmap")] = "" + ing.SetAnnotations(data) + + i, err := NewParser(&mockConfigMap{}).Parse(ing) + if err != nil { + t.Errorf("unexpected error parsing ingress without fastcgi") + } + + config, ok := i.(Config) + if !ok { + t.Errorf("Parse do not return a Config object") + } + + if len(config.Params) != 0 { + t.Errorf("Params should be an empty slice") + } +} + +func TestParseFastCGIInvalidParamsConfigMapAnnotation(t *testing.T) { + ing := buildIngress() + + invalidConfigMapList := []string{"unknown/configMap", "unknown/config/map"} + for _, configmap := range invalidConfigMapList { + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix("fastcgi-params-configmap")] = configmap + ing.SetAnnotations(data) + + i, err := NewParser(&mockConfigMap{}).Parse(ing) + if err == nil { + t.Errorf("Reading an unexisting configmap should return an error") + } + + config, ok := i.(Config) + if !ok { + t.Errorf("Parse do not return a Config object") + } + + if len(config.Params) != 0 { + t.Errorf("Params should be an empty slice") + } + } +} + +func TestParseFastCGIParamsConfigMapAnnotationWithoutNS(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix("fastcgi-params-configmap")] = "demo-configmap" + ing.SetAnnotations(data) + + i, err := NewParser(&mockConfigMap{}).Parse(ing) + if err != nil { + t.Errorf("unexpected error parsing ingress without fastcgi") + } + + config, ok := i.(Config) + if !ok { + t.Errorf("Parse do not return a Config object") + } + + if len(config.Params) != 2 { + t.Errorf("Params should have a length of 2") + } + + if config.Params["REDIRECT_STATUS"] != "200" || config.Params["SERVER_NAME"] != "$server_name" { + t.Errorf("Params value is not the one expected") + } +} + +func TestParseFastCGIParamsConfigMapAnnotationWithNS(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix("fastcgi-params-configmap")] = "default/demo-configmap" + ing.SetAnnotations(data) + + i, err := NewParser(&mockConfigMap{}).Parse(ing) + if err != nil { + t.Errorf("unexpected error parsing ingress without fastcgi") + } + + config, ok := i.(Config) + if !ok { + t.Errorf("Parse do not return a Config object") + } + + if len(config.Params) != 2 { + t.Errorf("Params should have a length of 2") + } + + if config.Params["REDIRECT_STATUS"] != "200" || config.Params["SERVER_NAME"] != "$server_name" { + t.Errorf("Params value is not the one expected") + } +} + +func TestParseFastCGIParamsConfigMapAnnotationWithDifferentNS(t *testing.T) { + ing := buildIngress() + + data := map[string]string{} + data[parser.GetAnnotationWithPrefix("fastcgi-params-configmap")] = "otherns/demo-configmap" + ing.SetAnnotations(data) + + _, err := NewParser(&mockConfigMap{}).Parse(ing) + if err == nil { + t.Errorf("Different namespace configmap should return an error") + } + +} + +func TestConfigEquality(t *testing.T) { + + var nilConfig *Config + + config := Config{ + Index: "index.php", + Params: map[string]string{"REDIRECT_STATUS": "200", "SERVER_NAME": "$server_name"}, + } + + configCopy := Config{ + Index: "index.php", + Params: map[string]string{"REDIRECT_STATUS": "200", "SERVER_NAME": "$server_name"}, + } + + config2 := Config{ + Index: "index.php", + Params: map[string]string{"REDIRECT_STATUS": "200"}, + } + + config3 := Config{ + Index: "index.py", + Params: map[string]string{"SERVER_NAME": "$server_name", "REDIRECT_STATUS": "200"}, + } + + config4 := Config{ + Index: "index.php", + Params: map[string]string{"SERVER_NAME": "$server_name", "REDIRECT_STATUS": "200"}, + } + + if !config.Equal(&config) { + t.Errorf("config should be equal to itself") + } + + if nilConfig.Equal(&config) { + t.Errorf("Foo") + } + + if !config.Equal(&configCopy) { + t.Errorf("config should be equal to configCopy") + } + + if config.Equal(&config2) { + t.Errorf("config2 should not be equal to config") + } + + if config.Equal(&config3) { + t.Errorf("config3 should not be equal to config") + } + + if !config.Equal(&config4) { + t.Errorf("config4 should be equal to config") + } +} diff --git a/internal/ingress/annotations/parser/main.go b/internal/ingress/annotations/parser/main.go index 8a0ea4b87e..107a278b0b 100644 --- a/internal/ingress/annotations/parser/main.go +++ b/internal/ingress/annotations/parser/main.go @@ -160,6 +160,7 @@ func normalizeString(input string) string { var configmapAnnotations = sets.NewString( "auth-proxy-set-header", + "fastcgi-params-configmap", ) // AnnotationsReferencesConfigmap checks if at least one annotation in the Ingress rule diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go index e93d3b5de4..4a44171301 100644 --- a/internal/ingress/controller/controller.go +++ b/internal/ingress/controller/controller.go @@ -271,8 +271,6 @@ func (n *NGINXController) CheckWarning(ing *networking.Ingress) ([]string, error "influxdb-host", "influxdb-server-name", "secure-verify-ca-secret", - "fastcgi-params-configmap", - "fastcgi-index", ) // Skip checks if the ingress is marked as deleted @@ -608,7 +606,7 @@ func (n *NGINXController) getConfiguration(ingresses []*ingress.Ingress) (sets.S for _, server := range servers { // If a location is defined by a prefix string that ends with the slash character, and requests are processed by one of - // proxy_pass, uwsgi_pass, scgi_pass, memcached_pass, or grpc_pass, then the special processing is performed. + // proxy_pass, fastcgi_pass, uwsgi_pass, scgi_pass, memcached_pass, or grpc_pass, then the special processing is performed. // In response to a request with URI equal to // this string, but without the trailing slash, a permanent redirect with the // code 301 will be returned to the requested URI with the slash appended. If this is not desired, an exact match of the // URIand location could be defined like this: @@ -1519,6 +1517,7 @@ func locationApplyAnnotations(loc *ingress.Location, anns *annotations.Ingress) loc.Logs = anns.Logs loc.DefaultBackend = anns.DefaultBackend loc.BackendProtocol = anns.BackendProtocol + loc.FastCGI = anns.FastCGI loc.CustomHTTPErrors = anns.CustomHTTPErrors loc.ModSecurity = anns.ModSecurity loc.Satisfy = anns.Satisfy diff --git a/internal/ingress/controller/controller_test.go b/internal/ingress/controller/controller_test.go index 355f5da950..c631461c01 100644 --- a/internal/ingress/controller/controller_test.go +++ b/internal/ingress/controller/controller_test.go @@ -431,7 +431,7 @@ func TestCheckWarning(t *testing.T) { t.Run("adding invalid annotations increases the warning count", func(t *testing.T) { ing.ObjectMeta.Annotations[parser.GetAnnotationWithPrefix("enable-influxdb")] = "true" ing.ObjectMeta.Annotations[parser.GetAnnotationWithPrefix("secure-verify-ca-secret")] = "true" - ing.ObjectMeta.Annotations[parser.GetAnnotationWithPrefix("fastcgi-index")] = "blabla" + ing.ObjectMeta.Annotations[parser.GetAnnotationWithPrefix("influxdb-host")] = "blabla" defer func() { ing.ObjectMeta.Annotations = map[string]string{} }() diff --git a/internal/ingress/controller/template/template.go b/internal/ingress/controller/template/template.go index 15ae438abb..2d941f95dd 100644 --- a/internal/ingress/controller/template/template.go +++ b/internal/ingress/controller/template/template.go @@ -739,6 +739,9 @@ func buildProxyPass(host string, b interface{}, loc interface{}) string { case "AJP": proto = "" proxyPass = "ajp_pass" + case "FCGI": + proto = "" + proxyPass = "fastcgi_pass" } upstreamName := "upstream_balancer" diff --git a/internal/ingress/controller/template/template_test.go b/internal/ingress/controller/template/template_test.go index 1b88c3fe7a..1980d7e525 100644 --- a/internal/ingress/controller/template/template_test.go +++ b/internal/ingress/controller/template/template_test.go @@ -1138,6 +1138,7 @@ func TestOpentracingPropagateContext(t *testing.T) { {BackendProtocol: "GRPC"}: "opentracing_grpc_propagate_context;", {BackendProtocol: "GRPCS"}: "opentracing_grpc_propagate_context;", {BackendProtocol: "AJP"}: "opentracing_propagate_context;", + {BackendProtocol: "FCGI"}: "opentracing_propagate_context;", nil: "", } @@ -1157,6 +1158,7 @@ func TestOpentelemetryPropagateContext(t *testing.T) { {BackendProtocol: "GRPC"}: "opentelemetry_propagate;", {BackendProtocol: "GRPCS"}: "opentelemetry_propagate;", {BackendProtocol: "AJP"}: "opentelemetry_propagate;", + {BackendProtocol: "FCGI"}: "opentelemetry_propagate;", nil: "", } diff --git a/pkg/apis/ingress/types.go b/pkg/apis/ingress/types.go index 68cacc25b9..e50666c18c 100644 --- a/pkg/apis/ingress/types.go +++ b/pkg/apis/ingress/types.go @@ -27,6 +27,7 @@ import ( "k8s.io/ingress-nginx/internal/ingress/annotations/authtls" "k8s.io/ingress-nginx/internal/ingress/annotations/connection" "k8s.io/ingress-nginx/internal/ingress/annotations/cors" + "k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi" "k8s.io/ingress-nginx/internal/ingress/annotations/globalratelimit" "k8s.io/ingress-nginx/internal/ingress/annotations/ipdenylist" "k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist" @@ -339,6 +340,9 @@ type Location struct { // BackendProtocol indicates which protocol should be used to communicate with the service // By default this is HTTP BackendProtocol string `json:"backend-protocol"` + // FastCGI allows the ingress to act as a FastCGI client for a given location. + // +optional + FastCGI fastcgi.Config `json:"fastcgi,omitempty"` // CustomHTTPErrors specifies the error codes that should be intercepted. // +optional CustomHTTPErrors []int `json:"custom-http-errors"` diff --git a/pkg/apis/ingress/types_equals.go b/pkg/apis/ingress/types_equals.go index fee8c31d56..84b1a186a4 100644 --- a/pkg/apis/ingress/types_equals.go +++ b/pkg/apis/ingress/types_equals.go @@ -435,6 +435,10 @@ func (l1 *Location) Equal(l2 *Location) bool { return false } + if !(&l1.FastCGI).Equal(&l2.FastCGI) { + return false + } + match := compareInts(l1.CustomHTTPErrors, l2.CustomHTTPErrors) if !match { return false diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl index 6383334072..9b3a47de3d 100644 --- a/rootfs/etc/nginx/template/nginx.tmpl +++ b/rootfs/etc/nginx/template/nginx.tmpl @@ -305,6 +305,7 @@ http { keepalive_requests {{ $cfg.KeepAliveRequests }}; client_body_temp_path /tmp/nginx/client-body; + fastcgi_temp_path /tmp/nginx/fastcgi-temp; proxy_temp_path /tmp/nginx/proxy-temp; ajp_temp_path /tmp/nginx/ajp-temp; @@ -1488,6 +1489,16 @@ stream { {{ range $errCode := $location.CustomHTTPErrors }} error_page {{ $errCode }} = @custom_{{ $location.DefaultBackendUpstreamName }}_{{ $errCode }};{{ end }} + {{ if (eq $location.BackendProtocol "FCGI") }} + include /etc/nginx/fastcgi_params; + {{ end }} + {{- if $location.FastCGI.Index -}} + fastcgi_index {{ $location.FastCGI.Index | quote }}; + {{- end -}} + {{ range $k, $v := $location.FastCGI.Params }} + fastcgi_param {{ $k }} {{ $v | quote }}; + {{ end }} + {{ if not (empty $location.Redirect.URL) }} return {{ $location.Redirect.Code }} {{ $location.Redirect.URL }}; {{ end }} diff --git a/test/e2e/annotations/backendprotocol.go b/test/e2e/annotations/backendprotocol.go index 8256befd23..bccb03afb4 100644 --- a/test/e2e/annotations/backendprotocol.go +++ b/test/e2e/annotations/backendprotocol.go @@ -91,6 +91,21 @@ var _ = framework.DescribeAnnotation("backend-protocol", func() { }) }) + ginkgo.It("should set backend protocol to '' and use fastcgi_pass", func() { + host := "backendprotocol.foo.com" + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/backend-protocol": "FCGI", + } + + ing := framework.NewSingleIngress(host, "/", host, f.Namespace, framework.EchoService, 80, annotations) + f.EnsureIngress(ing) + + f.WaitForNginxServer(host, + func(server string) bool { + return strings.Contains(server, "fastcgi_pass upstream_balancer;") + }) + }) + ginkgo.It("should set backend protocol to '' and use ajp_pass", func() { host := "backendprotocol.foo.com" annotations := map[string]string{ diff --git a/test/e2e/annotations/fastcgi.go b/test/e2e/annotations/fastcgi.go new file mode 100644 index 0000000000..572eca5481 --- /dev/null +++ b/test/e2e/annotations/fastcgi.go @@ -0,0 +1,125 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package annotations + +import ( + "net/http" + "strings" + + "github.com/onsi/ginkgo/v2" + corev1 "k8s.io/api/core/v1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/ingress-nginx/test/e2e/framework" +) + +var _ = framework.DescribeAnnotation("backend-protocol - FastCGI", func() { + f := framework.NewDefaultFramework("fastcgi") + + ginkgo.BeforeEach(func() { + f.NewFastCGIHelloServerDeployment() + }) + + ginkgo.It("should use fastcgi_pass in the configuration file", func() { + host := "fastcgi" + + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/backend-protocol": "FCGI", + } + + ing := framework.NewSingleIngress(host, "/hello", host, f.Namespace, "fastcgi-helloserver", 9000, annotations) + f.EnsureIngress(ing) + + f.WaitForNginxServer(host, + func(server string) bool { + return strings.Contains(server, "include /etc/nginx/fastcgi_params;") && + strings.Contains(server, "fastcgi_pass") + }) + }) + + ginkgo.It("should add fastcgi_index in the configuration file", func() { + host := "fastcgi-index" + + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/backend-protocol": "FCGI", + "nginx.ingress.kubernetes.io/fastcgi-index": "index.php", + } + + ing := framework.NewSingleIngress(host, "/hello", host, f.Namespace, "fastcgi-helloserver", 9000, annotations) + f.EnsureIngress(ing) + + f.WaitForNginxServer(host, + func(server string) bool { + return strings.Contains(server, "fastcgi_index \"index.php\";") + }) + }) + + ginkgo.It("should add fastcgi_param in the configuration file", func() { + configuration := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fastcgi-configmap", + Namespace: f.Namespace, + }, + Data: map[string]string{ + "SCRIPT_FILENAME": "/home/www/scripts/php$fastcgi_script_name", + "REDIRECT_STATUS": "200", + }, + } + + f.EnsureConfigMap(configuration) + + host := "fastcgi-params-configmap" + + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/backend-protocol": "FCGI", + "nginx.ingress.kubernetes.io/fastcgi-params-configmap": "fastcgi-configmap", + } + + ing := framework.NewSingleIngress(host, "/hello", host, f.Namespace, "fastcgi-helloserver", 9000, annotations) + f.EnsureIngress(ing) + + f.WaitForNginxServer(host, + func(server string) bool { + return strings.Contains(server, "fastcgi_param SCRIPT_FILENAME \"/home/www/scripts/php$fastcgi_script_name\";") && + strings.Contains(server, "fastcgi_param REDIRECT_STATUS \"200\";") + }) + }) + + ginkgo.It("should return OK for service with backend protocol FastCGI", func() { + host := "fastcgi-helloserver" + path := "/hello" + + annotations := map[string]string{ + "nginx.ingress.kubernetes.io/backend-protocol": "FCGI", + } + + ing := framework.NewSingleIngress(host, path, host, f.Namespace, "fastcgi-helloserver", 9000, annotations) + f.EnsureIngress(ing) + + f.WaitForNginxServer(host, + func(server string) bool { + return strings.Contains(server, "fastcgi_pass") + }) + + f.HTTPTestClient(). + GET(path). + WithHeader("Host", host). + Expect(). + Status(http.StatusOK). + Body().Contains("Hello world!") + }) +}) diff --git a/test/e2e/framework/fastcgi_helloserver.go b/test/e2e/framework/fastcgi_helloserver.go new file mode 100644 index 0000000000..719048c069 --- /dev/null +++ b/test/e2e/framework/fastcgi_helloserver.go @@ -0,0 +1,104 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package framework + +import ( + "github.com/onsi/ginkgo/v2" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// NewFastCGIHelloServerDeployment creates a new single replica +// deployment of the fortune teller image in a particular namespace +func (f *Framework) NewFastCGIHelloServerDeployment() { + f.NewNewFastCGIHelloServerDeploymentWithReplicas(1) +} + +// NewNewFastCGIHelloServerDeploymentWithReplicas creates a new deployment of the +// fortune teller image in a particular namespace. Number of replicas is configurable +func (f *Framework) NewNewFastCGIHelloServerDeploymentWithReplicas(replicas int32) { + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fastcgi-helloserver", + Namespace: f.Namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: NewInt32(replicas), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "fastcgi-helloserver", + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": "fastcgi-helloserver", + }, + }, + Spec: corev1.PodSpec{ + TerminationGracePeriodSeconds: NewInt64(0), + Containers: []corev1.Container{ + { + Name: "fastcgi-helloserver", + Image: "registry.k8s.io/ingress-nginx/e2e-test-fastcgi-helloserver@sha256:0e08c836cc58f1ea862578de99b13bc4264fe071e816f96dc1d79857bfba7473", + Env: []corev1.EnvVar{}, + Ports: []corev1.ContainerPort{ + { + Name: "fastcgi", + ContainerPort: 9000, + }, + }, + }, + }, + }, + }, + }, + } + + d := f.EnsureDeployment(deployment) + + err := waitForPodsReady(f.KubeClientSet, DefaultTimeout, int(replicas), f.Namespace, metav1.ListOptions{ + LabelSelector: fields.SelectorFromSet(fields.Set(d.Spec.Template.ObjectMeta.Labels)).String(), + }) + assert.Nil(ginkgo.GinkgoT(), err, "failed to wait for to become ready") + + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fastcgi-helloserver", + Namespace: f.Namespace, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "fastcgi", + Port: 9000, + TargetPort: intstr.FromInt(9000), + Protocol: "TCP", + }, + }, + Selector: map[string]string{ + "app": "fastcgi-helloserver", + }, + }, + } + + f.EnsureService(service) +}