diff --git a/.github/workflows/helm-tests.yml b/.github/workflows/helm-tests.yml index ca919fa0..44270a22 100644 --- a/.github/workflows/helm-tests.yml +++ b/.github/workflows/helm-tests.yml @@ -196,10 +196,16 @@ jobs: kubectl get ingress --all-namespaces -o jsonpath='{range .items[0]}kubectl describe ingress {.metadata.name} -n {.metadata.namespace}{end}' | sh kubectl get middleware.traefik.io --all-namespaces -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name' --no-headers | while read -r namespace name; do kubectl describe middleware.traefik.io "$name" -n "$namespace"; done - PUBLICIP='http://'$(kubectl -n kube-system get svc traefik -o jsonpath='{.status.loadBalancer.ingress[0].ip}') - export VECTOR_ENDPOINT=$PUBLICIP/vector$RELEASE_NAME - export STAC_ENDPOINT=$PUBLICIP/stac$RELEASE_NAME - export RASTER_ENDPOINT=$PUBLICIP/raster$RELEASE_NAME + # Get the IP address of the Traefik service + PUBLICIP_VALUE=$(kubectl -n kube-system get svc traefik -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + PUBLICIP=http://eoapi.local + export VECTOR_ENDPOINT=$PUBLICIP/vector + export STAC_ENDPOINT=$PUBLICIP/stac + export RASTER_ENDPOINT=$PUBLICIP/raster + + # Add entry to /etc/hosts for eoapi.local + echo "Adding eoapi.local to /etc/hosts with IP: $PUBLICIP_VALUE" + echo "$PUBLICIP_VALUE eoapi.local" | sudo tee -a /etc/hosts echo '#################################' echo $VECTOR_ENDPOINT @@ -207,10 +213,12 @@ jobs: echo $RASTER_ENDPOINT echo '#################################' - pytest .github/workflows/tests/test_vector.py || kubectl logs svc/vector - pytest .github/workflows/tests/test_stac.py || kubectl logs svc/stac + # Run tests with proper failure propagation + set -e # Make sure any command failure causes the script to exit with error + pytest .github/workflows/tests/test_vector.py || { kubectl logs svc/vector; exit 1; } + pytest .github/workflows/tests/test_stac.py || { kubectl logs svc/stac; exit 1; } # TODO: fix raster tests - #pytest .github/workflows/tests/test_raster.py || kubectl logs svc/raster + #pytest .github/workflows/tests/test_raster.py || { kubectl logs svc/raster; exit 1; } - name: error if tests failed if: steps.testrunner.outcome == 'failure' diff --git a/docs/unified-ingress.md b/docs/unified-ingress.md new file mode 100644 index 00000000..f287e704 --- /dev/null +++ b/docs/unified-ingress.md @@ -0,0 +1,107 @@ +# Unified Ingress Configuration + +This document describes the unified ingress approach implemented in the eoAPI Helm chart. + +## Overview + +As of version 0.7.0, eoAPI uses a consolidated, controller-agnostic ingress configuration. This approach: + +- Eliminates code duplication between different ingress controller implementations +- Provides consistent behavior across controllers +- Simplifies testing and maintainability +- Removes artificial restrictions on using certain ingress controllers in specific environments +- Makes it easier to add support for additional ingress controllers in the future + +## Configuration + +The ingress configuration has been streamlined and generalized in the `values.yaml` file: + +```yaml +ingress: + # Unified ingress configuration for both nginx and traefik + enabled: true + # ingressClassName: "nginx" or "traefik" + className: "nginx" + # Path configuration + pathType: "Prefix" # Can be "Prefix" or "ImplementationSpecific" based on controller + pathSuffix: "" # Add a suffix to service paths (e.g. "(/|$)(.*)" for nginx regex) + rootPath: "" # Root path for doc server + # Host configuration + host: "" + # Custom annotations to add to the ingress + annotations: {} + # TLS configuration + tls: + enabled: false + secretName: eoapi-tls + certManager: false + certManagerIssuer: letsencrypt-prod + certManagerEmail: "" +``` + +## Controller-Specific Configurations + +### NGINX Ingress Controller + +For NGINX, use the following configuration: + +```yaml +ingress: + enabled: true + className: "nginx" + pathType: "Prefix" + annotations: + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/enable-access-log: "true" +``` + +### Traefik Ingress Controller + +When using Traefik, the system automatically includes the Traefik middleware to strip prefixes (e.g., `/stac`, `/raster`) from requests before forwarding them to services. This is handled by the `traefik-middleware.yaml` template. + +For basic Traefik configuration: + +```yaml +ingress: + enabled: true + className: "traefik" + pathType: "Prefix" + # When using TLS, setting host is required to avoid "No domain found" warnings + host: "example.domain.com" # Required to work properly with TLS + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web +``` + +For Traefik with TLS: + +```yaml +ingress: + enabled: true + className: "traefik" + pathType: "Prefix" + # Host is required when using TLS with Traefik + host: "example.domain.com" + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + tls: + enabled: true + secretName: eoapi-tls +``` + +## Migration + +If you're migrating from a version 0.6.0 or earlier, follow these guidelines: + +1. Update your values to use the new unified configuration +2. Ensure your ingress controller-specific annotations are set correctly +3. Set the appropriate `pathType` for your controller +4. Test the configuration before deploying to production + +## Note for Traefik Users + +Traefik is now fully supported in all environments, including production. The previous restriction limiting Traefik to testing environments has been removed. + +## Document Server + +The document server implementation has also been unified. It now works with both NGINX and Traefik controllers using the same configuration. diff --git a/helm-chart/eoapi/ingress.bkup b/helm-chart/eoapi/ingress.bkup deleted file mode 100644 index da5f0349..00000000 --- a/helm-chart/eoapi/ingress.bkup +++ /dev/null @@ -1,62 +0,0 @@ -{{/* ORIGINAL INGRESS TEMPLATE FROM `helm create` */}} -{{- if .Values.ingress.enabled -}} -{{- $fullName := include "eoapi.fullname" . -}} -{{- $svcPort := .Values.service.port -}} -{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} - {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} - {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} - {{- end }} -{{- end }} -{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1 -{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1beta1 -{{- else -}} -apiVersion: extensions/v1beta1 -{{- end }} -kind: Ingress -metadata: - name: {{ $fullName }} - labels: - {{- include "eoapi.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} - pathType: {{ .pathType }} - {{- end }} - backend: - {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} - service: - name: {{ $fullName }} - port: - number: {{ $svcPort }} - {{- else }} - serviceName: {{ $fullName }} - servicePort: {{ $svcPort }} - {{- end }} - {{- end }} - {{- end }} -{{- end }} diff --git a/helm-chart/eoapi/templates/_helpers.tpl b/helm-chart/eoapi/templates/_helpers.tpl index a8469620..ebb38e69 100644 --- a/helm-chart/eoapi/templates/_helpers.tpl +++ b/helm-chart/eoapi/templates/_helpers.tpl @@ -397,14 +397,3 @@ validate: {{- end -}} {{- end -}} - -{{/* -validate: -that you can only use traefik as ingress when `testing=true` -*/}} -{{- define "eoapi.validateTraefik" -}} -{{- if and (not .Values.testing) (eq .Values.ingress.className "traefik") $ -}} - {{- fail "you cannot use traefik yet outside of testing" -}} -{{- end -}} - -{{- end -}} diff --git a/helm-chart/eoapi/templates/_pgstac_init.tpl b/helm-chart/eoapi/templates/_pgstac_init.tpl new file mode 100644 index 00000000..f3b806d6 --- /dev/null +++ b/helm-chart/eoapi/templates/_pgstac_init.tpl @@ -0,0 +1,30 @@ +{{- define "eoapi.pgstacInitContainer" -}} +{{- if .Values.pgstacBootstrap.enabled }} +- name: wait-for-pgstac-migrate + image: bitnami/kubectl:latest + command: + - /bin/sh + - -c + - | + echo "Waiting for pgstac-migrate job to complete..." + until kubectl get job pgstac-migrate -o jsonpath='{.status.conditions[?(@.type=="Complete")].status}' | grep -q "True"; do + echo "pgstac-migrate job not complete yet, waiting..." + sleep 5 + done + echo "pgstac-migrate job completed successfully." +{{- if .Values.pgstacBootstrap.settings.loadSamples }} +- name: wait-for-pgstac-load-samples + image: bitnami/kubectl:latest + command: + - /bin/sh + - -c + - | + echo "Waiting for pgstac-load-samples job to complete..." + until kubectl get job pgstac-load-samples -o jsonpath='{.status.conditions[?(@.type=="Complete")].status}' | grep -q "True"; do + echo "pgstac-load-samples job not complete yet, waiting..." + sleep 5 + done + echo "pgstac-load-samples job completed successfully." +{{- end }} +{{- end }} +{{- end -}} diff --git a/helm-chart/eoapi/templates/pgstacbootstrap/job.yaml b/helm-chart/eoapi/templates/pgstacbootstrap/job.yaml index 4d47b5c5..fa9b60f0 100644 --- a/helm-chart/eoapi/templates/pgstacbootstrap/job.yaml +++ b/helm-chart/eoapi/templates/pgstacbootstrap/job.yaml @@ -29,7 +29,7 @@ metadata: annotations: helm.sh/hook: "post-install,post-upgrade" helm.sh/hook-weight: "-5" - helm.sh/hook-delete-policy: "before-hook-creation,hook-succeeded" + helm.sh/hook-delete-policy: "before-hook-creation" spec: template: metadata: @@ -97,7 +97,7 @@ metadata: annotations: helm.sh/hook: "post-install,post-upgrade" helm.sh/hook-weight: "-4" - helm.sh/hook-delete-policy: "before-hook-creation,hook-succeeded" + helm.sh/hook-delete-policy: "before-hook-creation" spec: template: metadata: diff --git a/helm-chart/eoapi/templates/services/deployment.yaml b/helm-chart/eoapi/templates/services/deployment.yaml index 4d92fae4..47f7c684 100644 --- a/helm-chart/eoapi/templates/services/deployment.yaml +++ b/helm-chart/eoapi/templates/services/deployment.yaml @@ -34,12 +34,38 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} spec: + {{- if $.Values.pgstacBootstrap.enabled }} + initContainers: + - name: wait-for-pgstac-jobs + image: bitnami/kubectl:latest + command: + - /bin/sh + - -c + - | + echo "Waiting for pgstac-migrate job to complete..." + until kubectl get job pgstac-migrate -o jsonpath='{.status.conditions[?(@.type=="Complete")].status}' | grep -q "True"; do + echo "pgstac-migrate job not complete yet, waiting..." + sleep 5 + done + echo "pgstac-migrate job completed successfully." + + {{- if $.Values.pgstacBootstrap.settings.loadSamples }} + echo "Waiting for pgstac-load-samples job to complete..." + until kubectl get job pgstac-load-samples -o jsonpath='{.status.conditions[?(@.type=="Complete")].status}' | grep -q "True"; do + echo "pgstac-load-samples job not complete yet, waiting..." + sleep 5 + done + echo "pgstac-load-samples job completed successfully." + {{- end }} + {{- end }} containers: - image: {{ index $v "image" "name" }}:{{ index $v "image" "tag" }} name: {{ $serviceName }} command: {{- toYaml (index $v "command") | nindent 10 }} {{- if (and ($.Values.ingress.className) (or (eq $.Values.ingress.className "nginx") (eq $.Values.ingress.className "traefik"))) }} + - "--proxy-headers" # Needed when using reverse proxy + - "--forwarded-allow-ips=*" # Needed when using reverse proxy - "--root-path=/{{ $serviceName }}" {{- end }}{{/* needed for proxies and path rewrites on NLB */}} livenessProbe: diff --git a/helm-chart/eoapi/templates/services/nginx-doc-server.yaml b/helm-chart/eoapi/templates/services/doc-server.yaml similarity index 83% rename from helm-chart/eoapi/templates/services/nginx-doc-server.yaml rename to helm-chart/eoapi/templates/services/doc-server.yaml index 487f21e1..57298e13 100644 --- a/helm-chart/eoapi/templates/services/nginx-doc-server.yaml +++ b/helm-chart/eoapi/templates/services/doc-server.yaml @@ -1,8 +1,8 @@ -{{- if (and (.Values.ingress.className) (eq .Values.ingress.className "nginx") (not .Values.testing) (.Values.docServer.enabled))}} +{{- if .Values.docServer.enabled}} apiVersion: v1 kind: ConfigMap metadata: - name: nginx-root-html-{{ .Release.Name }} + name: doc-server-html-{{ .Release.Name }} data: index.html: | @@ -11,7 +11,7 @@ data:
Your service configuration is using ingress-nginx with path rewrites. So use these paths for each service:
+Your service configuration is using path rewrites. So use these paths for each service:
Your service configuration is using Traefik with path rewrites. So use these paths for each service:
- - - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: doc-server-{{ .Release.Name }} -spec: - replicas: 1 - selector: - matchLabels: - app: doc-server-{{ .Release.Name }} - template: - metadata: - labels: - app: doc-server-{{ .Release.Name }} - spec: - containers: - - name: doc-server - image: nginx:alpine - volumeMounts: - - name: doc-html-{{ .Release.Name }} - mountPath: /usr/share/nginx/html - ports: - - containerPort: 80 - volumes: - - name: doc-html-{{ .Release.Name }} - configMap: - name: traefik-root-html-{{ .Release.Name }} ---- -apiVersion: v1 -kind: Service -metadata: - name: doc-server-{{ .Release.Name }} -spec: - selector: - app: doc-server-{{ .Release.Name }} - ports: - - protocol: TCP - port: 80 - targetPort: 80 ---- -{{- end }} diff --git a/helm-chart/eoapi/templates/services/traefik-middleware.yaml b/helm-chart/eoapi/templates/services/traefik-middleware.yaml new file mode 100644 index 00000000..7bdcf7d7 --- /dev/null +++ b/helm-chart/eoapi/templates/services/traefik-middleware.yaml @@ -0,0 +1,17 @@ +{{- if and .Values.ingress.enabled (eq .Values.ingress.className "traefik") }} +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: strip-prefix-middleware-{{ $.Release.Name }} + namespace: {{ $.Release.Namespace }} +spec: + stripPrefix: + prefixes: + {{- range $serviceName, $v := .Values }} + {{- if has $serviceName $.Values.apiServices }} + {{- if (index $v "enabled") }} + - /{{ $serviceName }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm-chart/eoapi/test-k3s-unittest-values.yaml b/helm-chart/eoapi/test-k3s-unittest-values.yaml index f22194a5..f52ac9e6 100644 --- a/helm-chart/eoapi/test-k3s-unittest-values.yaml +++ b/helm-chart/eoapi/test-k3s-unittest-values.yaml @@ -3,8 +3,8 @@ testing: true ingress: enabled: true className: "traefik" -postgrescluster: - enabled: true + pathType: "Prefix" + host: "eoapi.local" # Adding a host value to avoid "No domain found" warnings with Traefik pgstacBootstrap: enabled: true settings: diff --git a/helm-chart/eoapi/tests/ingress_tests.yaml b/helm-chart/eoapi/tests/ingress_tests.yaml new file mode 100644 index 00000000..60ca07b9 --- /dev/null +++ b/helm-chart/eoapi/tests/ingress_tests.yaml @@ -0,0 +1,94 @@ +suite: unified ingress tests +templates: + - templates/services/ingress.yaml +tests: + - it: "vector ingress with nginx controller" + set: + ingress.className: "nginx" + ingress.pathType: "ImplementationSpecific" + ingress.pathSuffix: "(/|$)(.*)" + ingress.annotations: + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$2 + nginx.ingress.kubernetes.io/enable-cors: "true" + nginx.ingress.kubernetes.io/enable-access-log: "true" + raster.enabled: false + stac.enabled: false + vector.enabled: true + multidim.enabled: false + asserts: + - isKind: + of: Ingress + - equal: + path: spec.rules[0].http.paths[0].path + value: "/vector(/|$)(.*)" + - equal: + path: spec.rules[0].http.paths[0].pathType + value: "ImplementationSpecific" + - equal: + path: metadata.annotations + value: + nginx.ingress.kubernetes.io/enable-access-log: "true" + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$2 + nginx.ingress.kubernetes.io/enable-cors: "true" + - equal: + path: spec.ingressClassName + value: "nginx" + + - it: "stac ingress with traefik controller" + set: + ingress.className: "traefik" + ingress.pathType: "Prefix" + ingress.host: "eoapi.local" + testing: true + raster.enabled: false + stac.enabled: true + vector.enabled: false + multidim.enabled: false + asserts: + - isKind: + of: Ingress + - equal: + path: spec.rules[0].http.paths[0].path + value: "/stac" + - equal: + path: spec.rules[0].http.paths[0].pathType + value: "Prefix" + - equal: + path: metadata.annotations + value: + traefik.ingress.kubernetes.io/router.entrypoints: web + traefik.ingress.kubernetes.io/router.middlewares: NAMESPACE-strip-prefix-middleware-RELEASE-NAME@kubernetescrd + - equal: + path: spec.ingressClassName + value: "traefik" + - equal: + path: spec.rules[0].host + value: "eoapi.local" + + - it: "multidim ingress in production (non-testing) with traefik controller" + set: + ingress.className: "traefik" + ingress.pathType: "Prefix" + ingress.host: "eoapi.local" + testing: false + raster.enabled: false + stac.enabled: false + vector.enabled: false + multidim.enabled: true + asserts: + - isKind: + of: Ingress + - equal: + path: spec.rules[0].http.paths[0].path + value: "/multidim" + - equal: + path: spec.rules[0].http.paths[0].pathType + value: "Prefix" + - equal: + path: spec.rules[0].http.paths[1].path + value: "/" + - equal: + path: spec.rules[0].http.paths[1].backend.service.name + value: doc-server-RELEASE-NAME diff --git a/helm-chart/eoapi/tests/ingress_tests_nginx.yaml b/helm-chart/eoapi/tests/ingress_tests_nginx.yaml deleted file mode 100644 index 22276b30..00000000 --- a/helm-chart/eoapi/tests/ingress_tests_nginx.yaml +++ /dev/null @@ -1,96 +0,0 @@ -suite: service defaults ingress -templates: - - templates/services/ingress-nginx.yaml -tests: - - it: "vector ingress defaults" - set: - ingress.className: "nginx" - raster.enabled: false - stac.enabled: false - vector.enabled: true - multidim.enabled: false - asserts: - - isKind: - of: Ingress - - matchRegex: - path: spec.rules[0].http.paths[0].path - pattern: ^/vector\(\/\|\$\)\(\.\*\)$ - - equal: - path: metadata.annotations - value: - nginx.ingress.kubernetes.io/enable-access-log: "true" - nginx.ingress.kubernetes.io/use-regex: "true" - nginx.ingress.kubernetes.io/rewrite-target: /$2 - nginx.ingress.kubernetes.io/enable-cors: "true" - - equal: - path: spec.ingressClassName - value: "nginx" - - it: "raster ingress defaults" - set: - ingress.className: "nginx" - raster.enabled: true - stac.enabled: false - vector.enabled: false - multidim.enabled: false - asserts: - - isKind: - of: Ingress - - matchRegex: - path: spec.rules[0].http.paths[0].path - pattern: ^/raster\(\/\|\$\)\(\.\*\)$ - - equal: - path: metadata.annotations - value: - nginx.ingress.kubernetes.io/enable-access-log: "true" - nginx.ingress.kubernetes.io/use-regex: "true" - nginx.ingress.kubernetes.io/rewrite-target: /$2 - nginx.ingress.kubernetes.io/enable-cors: "true" - - equal: - path: spec.ingressClassName - value: "nginx" - - it: "stac ingress defaults" - set: - ingress.className: "nginx" - raster.enabled: false - stac.enabled: true - vector.enabled: false - multidim.enabled: false - asserts: - - isKind: - of: Ingress - - matchRegex: - path: spec.rules[0].http.paths[0].path - pattern: ^/stac\(\/\|\$\)\(\.\*\)$ - - equal: - path: metadata.annotations - value: - nginx.ingress.kubernetes.io/enable-access-log: "true" - nginx.ingress.kubernetes.io/use-regex: "true" - nginx.ingress.kubernetes.io/rewrite-target: /$2 - nginx.ingress.kubernetes.io/enable-cors: "true" - - equal: - path: spec.ingressClassName - value: "nginx" - - it: "multidim ingress defaults" - set: - ingress.className: "nginx" - raster.enabled: false - stac.enabled: false - vector.enabled: false - multidim.enabled: true - asserts: - - isKind: - of: Ingress - - matchRegex: - path: spec.rules[0].http.paths[0].path - pattern: ^/multidim\(\/\|\$\)\(\.\*\)$ - - equal: - path: metadata.annotations - value: - nginx.ingress.kubernetes.io/enable-access-log: "true" - nginx.ingress.kubernetes.io/use-regex: "true" - nginx.ingress.kubernetes.io/rewrite-target: /$2 - nginx.ingress.kubernetes.io/enable-cors: "true" - - equal: - path: spec.ingressClassName - value: "nginx" diff --git a/helm-chart/eoapi/values.yaml b/helm-chart/eoapi/values.yaml index 90632a97..d905f92f 100644 --- a/helm-chart/eoapi/values.yaml +++ b/helm-chart/eoapi/values.yaml @@ -39,11 +39,21 @@ service: port: 8080 ingress: - # `"nginx"` will create a `kind:Service` with a `spec.port:ClusterIP` - # and a single Load Balancer and path rewrites for /vector, /stac, /raster + # Unified ingress configuration for both nginx and traefik enabled: true + # ingressClassName: "nginx" or "traefik" className: "nginx" + # Path configuration + pathType: "Prefix" # Can be "Prefix" or "ImplementationSpecific" based on controller + # NOTE: When using nginx ingress controller with regex in pathSuffix, + # you must set pathType to "ImplementationSpecific". See issue #189 for more context. + pathSuffix: "" # Add a suffix to service paths (e.g. "(/|$)(.*)" for nginx regex) + rootPath: "" # Root path for doc server + # Host configuration host: "" + # Custom annotations to add to the ingress + annotations: {} + # TLS configuration tls: enabled: false secretName: eoapi-tls