From d42c3a409e46c702f4ae6da07ea274b0efcde12d Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Tue, 18 Nov 2025 16:32:15 +0100 Subject: [PATCH 1/8] Added stac-auth-proxy. --- CHANGELOG.md | 1 + charts/eoapi/Chart.yaml | 4 + charts/eoapi/README.md | 16 ++++ charts/eoapi/profiles/experimental.yaml | 9 +++ .../eoapi/templates/_helpers/validation.tpl | 12 +++ charts/eoapi/templates/core/validation.yaml | 3 +- .../eoapi/templates/networking/ingress.yaml | 8 ++ .../tests/stac-auth-proxy-ingress_test.yaml | 79 +++++++++++++++++++ charts/eoapi/values.yaml | 11 +++ 9 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 charts/eoapi/tests/stac-auth-proxy-ingress_test.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index f50b35c5..020be593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Changed - Unified scripts and removed Makefile, combined all into one CLI command `eoapi-cli` [#359](https://github.com/developmentseed/eoapi-k8s/pull/359) +- Added stac-auth-proxy for authentication and authorization on the STAC API [#358](https://github.com/developmentseed/eoapi-k8s/pull/358) ## [0.8.0] - 2025-11-20 diff --git a/charts/eoapi/Chart.yaml b/charts/eoapi/Chart.yaml index 42f94c2b..399ac920 100644 --- a/charts/eoapi/Chart.yaml +++ b/charts/eoapi/Chart.yaml @@ -77,3 +77,7 @@ dependencies: version: 10.2.0 repository: https://grafana.github.io/helm-charts condition: observability.grafana.enabled + - name: stac-auth-proxy + version: "0.1.0" + repository: "oci://ghcr.io/developmentseed/stac-auth-proxy/charts" + condition: stac-auth-proxy.enabled diff --git a/charts/eoapi/README.md b/charts/eoapi/README.md index f9779e81..deb0f553 100644 --- a/charts/eoapi/README.md +++ b/charts/eoapi/README.md @@ -7,6 +7,7 @@ A Helm chart for deploying Earth Observation APIs with integrated STAC, raster, ## Features - STAC API for metadata discovery and search +- STAC Auth Proxy for authentication/authorization (optional) - Raster tile services (TiTiler) - Vector tile services (TIPG) - Multidimensional data support @@ -42,6 +43,21 @@ helm install eoapi eoapi/eoapi -f profiles/experimental.yaml - PV provisioner support - PostgreSQL operator +## STAC Auth Proxy (Optional) + +The chart includes support for [stac-auth-proxy](https://github.com/developmentseed/stac-auth-proxy) to add authentication and authorization to your STAC API. This feature is disabled by default and can be enabled, and will need a valid OIDC discovery URL. + +### Configuration + +```yaml +stac-auth-proxy: + enabled: true + env: + OIDC_DISCOVERY_URL: "https://your-auth-server/.well-known/openid-configuration" +``` + +When enabled, the ingress will automatically route STAC API requests through the auth proxy instead of directly to the STAC service. + ## Quick Start with Profiles Use pre-configured profiles for common deployment scenarios: diff --git a/charts/eoapi/profiles/experimental.yaml b/charts/eoapi/profiles/experimental.yaml index 505a59f4..63fd635b 100644 --- a/charts/eoapi/profiles/experimental.yaml +++ b/charts/eoapi/profiles/experimental.yaml @@ -364,6 +364,15 @@ ingress: tls: enabled: false +stac-auth-proxy: + enabled: false + env: + UPSTREAM_URL: "http://eoapi-stac:8080/stac" + # For testing one could deploy a mock OIDC server (https://github.com/alukach/mock-oidc-server) + OIDC_DISCOVERY_URL: "http://mock-oidc-server.default.svc.cluster.local/.well-known/openid-configuration" + # For production, point to your actual OpenID Connect provider + # OIDC_DISCOVERY_URL: "https://your-auth-provider.com/.well-known/openid-configuration" + ###################### # SERVICE ###################### diff --git a/charts/eoapi/templates/_helpers/validation.tpl b/charts/eoapi/templates/_helpers/validation.tpl index dc795e33..ea9b49dc 100644 --- a/charts/eoapi/templates/_helpers/validation.tpl +++ b/charts/eoapi/templates/_helpers/validation.tpl @@ -26,3 +26,15 @@ so we use this helper function to check autoscaling rules {{- end }} {{- end }} {{- end -}} + +{{/* +Validate stac-auth-proxy configuration +Ensures OIDC_DISCOVERY_URL is set when stac-auth-proxy is enabled +*/}} +{{- define "eoapi.validateStacAuthProxy" -}} +{{- if index .Values "stac-auth-proxy" "enabled" }} +{{- if not (index .Values "stac-auth-proxy" "env" "OIDC_DISCOVERY_URL") }} +{{- fail "stac-auth-proxy.env.OIDC_DISCOVERY_URL is required when stac-auth-proxy is enabled. Set it to your OpenID Connect discovery URL (e.g., https://your-auth-server/.well-known/openid-configuration)" }} +{{- end }} +{{- end }} +{{- end -}} diff --git a/charts/eoapi/templates/core/validation.yaml b/charts/eoapi/templates/core/validation.yaml index 785d2376..95aaf128 100644 --- a/charts/eoapi/templates/core/validation.yaml +++ b/charts/eoapi/templates/core/validation.yaml @@ -1,5 +1,6 @@ {{/* -This template validates the PostgreSQL configuration. +This template validates various configurations. It doesn't create any resources but ensures configuration consistency. */}} {{- include "eoapi.validatePostgresql" . }} +{{- include "eoapi.validateStacAuthProxy" . }} diff --git a/charts/eoapi/templates/networking/ingress.yaml b/charts/eoapi/templates/networking/ingress.yaml index da2d69f7..ed4dde92 100644 --- a/charts/eoapi/templates/networking/ingress.yaml +++ b/charts/eoapi/templates/networking/ingress.yaml @@ -49,7 +49,11 @@ spec: path: {{ $.Values.stac.ingress.path }}{{ if eq $.Values.ingress.className "nginx" }}(/|$)(.*){{ end }} backend: service: + {{- if index $.Values "stac-auth-proxy" "enabled" }} + name: {{ $.Release.Name }}-stac-auth-proxy + {{- else }} name: {{ $.Release.Name }}-stac + {{- end }} port: number: {{ $.Values.service.port }} {{- end }} @@ -105,7 +109,11 @@ spec: path: {{ .Values.stac.ingress.path }}{{ if eq .Values.ingress.className "nginx" }}(/|$)(.*){{ end }} backend: service: + {{- if index .Values "stac-auth-proxy" "enabled" }} + name: {{ .Release.Name }}-stac-auth-proxy + {{- else }} name: {{ .Release.Name }}-stac + {{- end }} port: number: {{ .Values.service.port }} {{- end }} diff --git a/charts/eoapi/tests/stac-auth-proxy-ingress_test.yaml b/charts/eoapi/tests/stac-auth-proxy-ingress_test.yaml new file mode 100644 index 00000000..c4007016 --- /dev/null +++ b/charts/eoapi/tests/stac-auth-proxy-ingress_test.yaml @@ -0,0 +1,79 @@ +suite: test stac-auth-proxy ingress routing +templates: + - networking/ingress.yaml + +tests: + - it: should route ingress to stac-auth-proxy when enabled + set: + ingress.enabled: true + ingress.className: nginx + stac.enabled: true + stac.ingress.enabled: true + stac.ingress.path: "/stac" + stac-auth-proxy.enabled: true + service.port: 8080 + asserts: + - contains: + path: spec.rules[0].http.paths + content: + pathType: ImplementationSpecific + path: /stac(/|$)(.*) + backend: + service: + name: RELEASE-NAME-stac-auth-proxy + port: + number: 8080 + template: networking/ingress.yaml + + - it: should route ingress directly to stac when auth-proxy is disabled + set: + ingress.enabled: true + ingress.className: nginx + stac.enabled: true + stac.ingress.enabled: true + stac.ingress.path: "/stac" + stac-auth-proxy.enabled: false + service.port: 8080 + asserts: + - contains: + path: spec.rules[0].http.paths + content: + pathType: ImplementationSpecific + path: /stac(/|$)(.*) + backend: + service: + name: RELEASE-NAME-stac + port: + number: 8080 + template: networking/ingress.yaml + + - it: should not create stac routes when stac is disabled + set: + ingress.enabled: true + stac.enabled: false + stac-auth-proxy.enabled: true + asserts: + - notContains: + path: spec.rules[0].http.paths + any: true + content: + path: /stac(/|$)(.*) + template: networking/ingress.yaml + + - it: should route correctly with experimental profile + values: + - ../profiles/experimental.yaml + set: + ingress.enabled: true + asserts: + - contains: + path: spec.rules[0].http.paths + content: + pathType: ImplementationSpecific + path: /stac(/|$)(.*) + backend: + service: + name: RELEASE-NAME-stac-auth-proxy + port: + number: 8080 + template: networking/ingress.yaml diff --git a/charts/eoapi/values.yaml b/charts/eoapi/values.yaml index 52934e70..8786fd37 100644 --- a/charts/eoapi/values.yaml +++ b/charts/eoapi/values.yaml @@ -410,6 +410,17 @@ stac: DB_MIN_CONN_SIZE: "1" DB_MAX_CONN_SIZE: "5" # Quite intensive (queries, transactions, searches) +# STAC Auth Proxy - authentication layer for STAC API +stac-auth-proxy: + enabled: false + env: + DEFAULT_PUBLIC: "true" + # UPSTREAM_URL will be set dynamically in template to point to stac service + # OIDC_DISCOVERY_URL must be configured when enabling auth + ingress: + enabled: false # Handled by main eoapi ingress + resources: {} + vector: enabled: true ingress: From 30cceacb3c2d3910f96bd679c657c8f5e2b4557d Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Sat, 22 Nov 2025 02:10:24 +0100 Subject: [PATCH 2/8] Added integration tests for stac-auth-proxy. --- charts/eoapi/profiles/experimental.yaml | 47 +- .../eoapi/templates/mock-oidc/deployment.yaml | 72 +++ charts/eoapi/templates/mock-oidc/service.yaml | 20 + charts/eoapi/values.yaml | 7 + scripts/deploy.sh | 574 ++++++++++++++++++ scripts/deployment.sh | 14 + tests/conftest.py | 6 +- tests/integration/test_stac_auth.py | 107 ++++ 8 files changed, 841 insertions(+), 6 deletions(-) create mode 100644 charts/eoapi/templates/mock-oidc/deployment.yaml create mode 100644 charts/eoapi/templates/mock-oidc/service.yaml create mode 100755 scripts/deploy.sh create mode 100644 tests/integration/test_stac_auth.py diff --git a/charts/eoapi/profiles/experimental.yaml b/charts/eoapi/profiles/experimental.yaml index 63fd635b..52472ee6 100644 --- a/charts/eoapi/profiles/experimental.yaml +++ b/charts/eoapi/profiles/experimental.yaml @@ -365,14 +365,55 @@ ingress: enabled: false stac-auth-proxy: - enabled: false + enabled: true + service: + port: 8080 + # Wait for dependencies to be ready before starting stac-auth-proxy + initContainers: + - name: wait-for-mock-oidc + image: busybox:1.35 + command: ['sh', '-c', 'until nc -z eoapi-mock-oidc-server.eoapi.svc.cluster.local 8080; do echo waiting for mock-oidc; sleep 2; done'] + - name: wait-for-stac + image: busybox:1.35 + command: ['sh', '-c', 'until nc -z eoapi-stac.eoapi.svc.cluster.local 8080; do echo waiting for stac service; sleep 2; done'] env: - UPSTREAM_URL: "http://eoapi-stac:8080/stac" + UPSTREAM_URL: "http://eoapi-stac:8080" # For testing one could deploy a mock OIDC server (https://github.com/alukach/mock-oidc-server) - OIDC_DISCOVERY_URL: "http://mock-oidc-server.default.svc.cluster.local/.well-known/openid-configuration" + OIDC_DISCOVERY_URL: "http://eoapi-mock-oidc-server.eoapi.svc.cluster.local:8080/.well-known/openid-configuration" # For production, point to your actual OpenID Connect provider # OIDC_DISCOVERY_URL: "https://your-auth-provider.com/.well-known/openid-configuration" +###################### +# MOCK OIDC SERVER +###################### +# Mock OIDC server for testing authentication +# WARNING: Only for development/testing, never use in production! +mockOidcServer: + enabled: true + replicaCount: 1 + image: + repository: ghcr.io/alukach/mock-oidc-server + tag: latest + pullPolicy: IfNotPresent + port: 8888 + clientId: "test-client" + clientSecret: "test-secret" + service: + type: ClusterIP + port: 8080 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi + nodeSelector: {} + tolerations: [] + affinity: {} + imagePullSecrets: [] + extraEnv: [] + ###################### # SERVICE ###################### diff --git a/charts/eoapi/templates/mock-oidc/deployment.yaml b/charts/eoapi/templates/mock-oidc/deployment.yaml new file mode 100644 index 00000000..d985d223 --- /dev/null +++ b/charts/eoapi/templates/mock-oidc/deployment.yaml @@ -0,0 +1,72 @@ +{{- if .Values.mockOidcServer.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "eoapi.fullname" . }}-mock-oidc-server + namespace: {{ .Release.Namespace }} + labels: + {{- include "eoapi.labels" . | nindent 4 }} + app.kubernetes.io/component: mock-oidc-server +spec: + replicas: {{ .Values.mockOidcServer.replicaCount | default 1 }} + selector: + matchLabels: + {{- include "eoapi.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: mock-oidc-server + template: + metadata: + labels: + {{- include "eoapi.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: mock-oidc-server + spec: + {{- if .Values.mockOidcServer.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.mockOidcServer.imagePullSecrets | nindent 8 }} + {{- end }} + containers: + - name: mock-oidc + image: "{{ if .Values.mockOidcServer.image }}{{ .Values.mockOidcServer.image.repository | default "ghcr.io/alukach/mock-oidc-server" }}:{{ .Values.mockOidcServer.image.tag | default "latest" }}{{ else }}ghcr.io/alukach/mock-oidc-server:latest{{ end }}" + imagePullPolicy: {{ if .Values.mockOidcServer.image }}{{ .Values.mockOidcServer.image.pullPolicy | default "IfNotPresent" }}{{ else }}IfNotPresent{{ end }} + env: + - name: MOCK_OIDC_PORT + value: "{{ .Values.mockOidcServer.port | default 8888 }}" + - name: MOCK_OIDC_CLIENT_ID + value: "{{ .Values.mockOidcServer.clientId | default "test-client" }}" + - name: MOCK_OIDC_CLIENT_SECRET + value: "{{ .Values.mockOidcServer.clientSecret | default "test-secret" }}" + {{- if .Values.mockOidcServer.extraEnv }} + {{- toYaml .Values.mockOidcServer.extraEnv | nindent 8 }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.mockOidcServer.port | default 8888 }} + protocol: TCP + livenessProbe: + httpGet: + path: /.well-known/openid-configuration + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /.well-known/openid-configuration + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + {{- if .Values.mockOidcServer.resources }} + resources: + {{- toYaml .Values.mockOidcServer.resources | nindent 10 }} + {{- end }} + {{- if .Values.mockOidcServer.nodeSelector }} + nodeSelector: + {{- toYaml .Values.mockOidcServer.nodeSelector | nindent 8 }} + {{- end }} + {{- if .Values.mockOidcServer.affinity }} + affinity: + {{- toYaml .Values.mockOidcServer.affinity | nindent 8 }} + {{- end }} + {{- if .Values.mockOidcServer.tolerations }} + tolerations: + {{- toYaml .Values.mockOidcServer.tolerations | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/eoapi/templates/mock-oidc/service.yaml b/charts/eoapi/templates/mock-oidc/service.yaml new file mode 100644 index 00000000..34a241bd --- /dev/null +++ b/charts/eoapi/templates/mock-oidc/service.yaml @@ -0,0 +1,20 @@ +{{- if .Values.mockOidcServer.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "eoapi.fullname" . }}-mock-oidc-server + namespace: {{ .Release.Namespace }} + labels: + {{- include "eoapi.labels" . | nindent 4 }} + app.kubernetes.io/component: mock-oidc-server +spec: + type: {{ if .Values.mockOidcServer.service }}{{ .Values.mockOidcServer.service.type | default "ClusterIP" }}{{ else }}ClusterIP{{ end }} + ports: + - port: {{ if .Values.mockOidcServer.service }}{{ .Values.mockOidcServer.service.port | default 8080 }}{{ else }}8080{{ end }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "eoapi.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: mock-oidc-server +{{- end }} diff --git a/charts/eoapi/values.yaml b/charts/eoapi/values.yaml index 8786fd37..582301a5 100644 --- a/charts/eoapi/values.yaml +++ b/charts/eoapi/values.yaml @@ -535,6 +535,13 @@ eoapi-notifier: namespace: serverless # For HTTP endpoints, use: endpoint: https://webhook.example.com +###################### +# MOCK OIDC SERVER +###################### +# Mock OIDC server for testing authentication (experimental profile only) +mockOidcServer: + enabled: false + ###################### # KNATIVE ###################### diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 00000000..84edde4e --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,574 @@ +#!/bin/bash + +# eoAPI Deployment Script + +# Source shared utilities +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +source "$SCRIPT_DIR/lib/common.sh" + +# Default values +PGO_VERSION="${PGO_VERSION:-5.7.4}" +RELEASE_NAME="${RELEASE_NAME:-eoapi}" +NAMESPACE="${NAMESPACE:-eoapi}" +TIMEOUT="${TIMEOUT:-10m}" +CI_MODE=false +COMMAND="" + +CI_MODE=$(is_ci_environment && echo true || echo false) + +log_info "=== eoAPI Deployment Script Starting ===" +log_debug "Script location: $0" +log_debug "Script directory: $SCRIPT_DIR" +log_debug "Working directory: $(pwd)" +log_debug "Environment variables:" +log_debug " PGO_VERSION: $PGO_VERSION" +log_debug " RELEASE_NAME: $RELEASE_NAME" +log_debug " NAMESPACE: $NAMESPACE" +log_debug " TIMEOUT: $TIMEOUT" +log_debug " CI_MODE: $CI_MODE" + +log_debug "=== Environment Validation ===" +log_debug "Bash version: $BASH_VERSION" +log_debug "Available tools check:" +if command -v kubectl >/dev/null 2>&1; then + log_debug " kubectl: $(kubectl version --client --short 2>/dev/null || echo 'version unavailable')" +else + log_error "kubectl not found in PATH" + exit 1 +fi + +if command -v helm >/dev/null 2>&1; then + log_debug " helm: $(helm version --short 2>/dev/null || echo 'version unavailable')" +else + log_error "helm not found in PATH" + exit 1 +fi + +log_debug "Kubernetes connectivity check deferred until needed" + +log_debug "Project structure validation:" +if [ -d "charts" ]; then + log_debug " ✅ charts/ directory found" + charts_list="" + for chart_dir in charts/*/; do + if [ -d "$chart_dir" ]; then + chart_name=$(basename "$chart_dir") + charts_list="$charts_list$chart_name " + fi + done + log_debug " Available charts: ${charts_list:-none}" +else + log_error " ❌ charts/ directory not found in $(pwd)" + # shellcheck disable=SC2012 + log_debug " Directory contents: $(ls -la | head -10)" + exit 1 +fi + +log_debug "=== Environment validation complete ===" + +while [[ $# -gt 0 ]]; do + case $1 in + deploy|setup|cleanup) + COMMAND="$1"; shift ;; + --ci) CI_MODE=true; shift ;; + --help|-h) + echo "eoAPI Deployment Script" + echo "Usage: $(basename "$0") [COMMAND] [OPTIONS]" + echo "" + echo "Commands:" + echo " deploy Deploy eoAPI (includes setup) [default]" + echo " setup Setup Helm dependencies only" + echo " cleanup Cleanup deployment resources" + echo "" + echo "Options:" + echo " --ci Enable CI mode" + echo " --help Show this help message" + echo "" + echo "Environment variables:" + echo " PGO_VERSION PostgreSQL Operator version (default: 5.7.4)" + echo " RELEASE_NAME Helm release name (default: eoapi)" + echo " NAMESPACE Kubernetes namespace (default: eoapi)" + echo " TIMEOUT Helm install timeout (default: 10m)" + exit 0 ;; + *) log_error "Unknown option: $1"; exit 1 ;; + esac +done + +if [ -z "$COMMAND" ]; then + COMMAND="deploy" +fi + +log_info "Starting eoAPI $COMMAND$([ "$CI_MODE" = true ] && echo " (CI MODE)" || echo "")..." +log_info "Release: $RELEASE_NAME | Namespace: $NAMESPACE | PGO Version: $PGO_VERSION" + +# Check Kubernetes connectivity for commands that need it +if [ "$COMMAND" != "setup" ]; then + log_debug "Validating Kubernetes connectivity for command: $COMMAND" + if kubectl cluster-info --request-timeout=10s >/dev/null 2>&1; then + log_debug " ✅ Cluster connection successful" + log_debug " Current context: $(kubectl config current-context 2>/dev/null || echo 'unknown')" + else + log_error " ❌ Cannot connect to Kubernetes cluster" + exit 1 + fi +fi + +pre_deployment_debug() { + log_info "=== Pre-deployment State Check ===" + + log_info "Cluster nodes:" + kubectl get nodes -o wide || log_error "Cannot get cluster nodes" + echo "" + + log_info "All namespaces:" + kubectl get namespaces || log_error "Cannot get namespaces" + echo "" + + log_info "PostgreSQL Operator status:" + kubectl get deployment pgo -o wide 2>/dev/null || log_info "PGO not found (expected for fresh install)" + kubectl get pods -l postgres-operator.crunchydata.com/control-plane=postgres-operator -o wide 2>/dev/null || log_info "No PGO pods found (expected for fresh install)" + echo "" + + log_info "Looking for knative-operator before deployment:" + kubectl get deployment knative-operator --all-namespaces -o wide 2>/dev/null || log_info "knative-operator not found yet (expected)" + echo "" + + log_info "Helm repositories:" + helm repo list 2>/dev/null || log_info "No helm repositories configured yet" + echo "" + + log_info "$NAMESPACE namespace check:" + kubectl get namespace "$NAMESPACE" 2>/dev/null || log_info "$NAMESPACE namespace doesn't exist yet (expected)" + echo "" + + log_info "Script validation complete" + log_debug "Working directory: $(pwd)" + log_debug "Environment: RELEASE_NAME=$RELEASE_NAME, PGO_VERSION=$PGO_VERSION" + + return 0 +} + +if [ "$COMMAND" != "setup" ]; then + preflight_deploy || exit 1 + + if [ "$CI_MODE" = true ]; then + pre_deployment_debug || exit 1 + fi +fi + +install_pgo() { + log_info "Installing PostgreSQL Operator..." + + log_debug "Current working directory: $(pwd)" + log_debug "Checking for existing PGO installation..." + + existing_pgo=$(helm list -A -q 2>/dev/null | grep "^pgo$" || echo "") + + if [ -n "$existing_pgo" ]; then + log_info "PGO already installed, upgrading..." + log_debug "Existing PGO release: $existing_pgo" + + if ! helm upgrade pgo oci://registry.developers.crunchydata.com/crunchydata/pgo \ + --version "$PGO_VERSION" --set disable_check_for_upgrades=true 2>&1; then + log_error "Failed to upgrade PostgreSQL Operator" + log_debug "Helm list output:" + helm list -A || true + log_debug "Available helm repositories:" + helm repo list || echo "No repositories configured" + exit 1 + fi + log_info "✅ PGO upgrade completed" + else + log_info "Installing new PGO instance..." + + if ! helm install pgo oci://registry.developers.crunchydata.com/crunchydata/pgo \ + --version "$PGO_VERSION" --set disable_check_for_upgrades=true 2>&1; then + log_error "Failed to install PostgreSQL Operator" + log_debug "Helm installation failed. Checking environment..." + log_debug "Kubernetes connectivity:" + kubectl cluster-info || echo "Cluster info unavailable" + log_debug "Available namespaces:" + kubectl get namespaces || echo "Cannot list namespaces" + log_debug "Helm version:" + helm version || echo "Helm version unavailable" + exit 1 + fi + log_info "✅ PGO installation completed" + fi + + log_info "Waiting for PostgreSQL Operator to be ready..." + log_debug "Checking for PGO deployment..." + + if ! kubectl get deployment pgo >/dev/null 2>&1; then + log_warn "PGO deployment not found, waiting for it to be created..." + sleep 10 + + if ! kubectl get deployment pgo >/dev/null 2>&1; then + log_error "PGO deployment was not created" + log_debug "All deployments in default namespace:" + kubectl get deployments -o wide || echo "Cannot list deployments" + log_debug "All pods in default namespace:" + kubectl get pods -o wide || echo "Cannot list pods" + log_debug "Recent events:" + kubectl get events --sort-by='.lastTimestamp' | tail -10 || echo "Cannot get events" + exit 1 + fi + fi + + log_debug "PGO deployment found, waiting for readiness..." + if ! kubectl wait --for=condition=Available deployment/pgo --timeout=300s; then + log_error "PostgreSQL Operator failed to become ready within timeout" + + log_debug "=== PGO Debugging Information ===" + log_debug "PGO deployment status:" + kubectl describe deployment pgo || echo "Cannot describe PGO deployment" + log_debug "PGO pods:" + kubectl get pods -l postgres-operator.crunchydata.com/control-plane=postgres-operator -o wide || echo "Cannot get PGO pods" + log_debug "PGO pod logs:" + kubectl logs -l postgres-operator.crunchydata.com/control-plane=postgres-operator --tail=30 || echo "Cannot get PGO logs" + log_debug "Recent events:" + kubectl get events --sort-by='.lastTimestamp' | tail -15 || echo "Cannot get events" + + exit 1 + fi + + log_info "✅ PostgreSQL Operator is ready" + kubectl get pods -l postgres-operator.crunchydata.com/control-plane=postgres-operator -o wide +} + +setup_helm_dependencies() { + log_info "Setting up Helm dependencies..." + + SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" + PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + + log_debug "Script directory: $SCRIPT_DIR" + log_debug "Project root: $PROJECT_ROOT" + + cd "$PROJECT_ROOT" || { + log_error "Failed to change to project root directory: $PROJECT_ROOT" + exit 1 + } + + if [ ! -d "charts" ]; then + log_error "charts/ directory not found in $(pwd)" + log_error "Directory contents:" + ls -la || true + exit 1 + fi + + log_debug "Current working directory: $(pwd)" + log_debug "Available charts directories:" + ls -la charts/ || log_error "Failed to list charts/ directory" + + log_debug "Initial helm repositories:" + helm repo list 2>/dev/null || log_debug "No repositories configured yet" + + for chart in charts/*/; do + if [ -f "$chart/Chart.yaml" ]; then + log_info "Processing $chart..." + log_debug "Chart.yaml content for $chart:" + cat "$chart/Chart.yaml" | grep -A5 -B5 "repository:" || log_debug "No repository section found" + + # Extract unique repository URLs + if grep -q "repository:" "$chart/Chart.yaml" 2>/dev/null; then + log_debug "Found repository entries in $chart" + repositories=$(grep "repository:" "$chart/Chart.yaml" 2>/dev/null | sed "s/.*repository: *//" | grep -v "file://" | sort -u) + log_debug "Extracted repositories: $repositories" + + echo "$repositories" | while read -r repo; do + if [ -n "$repo" ]; then + # Clean up repository URL and create name + clean_repo=$(echo "$repo" | sed 's/"//g' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') + repo_name=$(echo "$clean_repo" | sed "s|https://||" | sed "s|oci://||" | sed "s|/.*||" | sed "s/\./-/g") + log_info "Adding repository $repo_name -> $clean_repo" + + # Add repository with error checking + if helm repo add "$repo_name" "$clean_repo" 2>&1; then + log_info "✅ Successfully added repository: $repo_name" + else + log_warn "⚠️ Failed to add repository: $repo_name ($clean_repo)" + fi + fi + done + else + log_debug "No repository entries found in $chart/Chart.yaml" + fi + else + log_warn "Chart.yaml not found in $chart" + fi + done + + log_debug "Repositories after adding:" + helm repo list || log_debug "Still no repositories configured" + + log_info "Updating helm repositories..." + if helm repo update 2>&1; then + log_info "✅ Repository update successful" + else + log_error "❌ Repository update failed" + helm repo list || log_debug "No repositories to update" + fi + + for chart in charts/*/; do + if [ -f "$chart/Chart.yaml" ]; then + log_info "Building dependencies for $chart..." + log_debug "Chart directory contents:" + ls -la "$chart/" || true + + ( + cd "$chart" || exit + log_debug "Building dependencies in $(pwd)" + if helm dependency build 2>&1; then + log_info "✅ Dependencies built successfully for $chart" + log_debug "Dependencies after build:" + ls -la charts/ 2>/dev/null || log_debug "No charts/ subdirectory" + else + log_error "❌ Failed to build dependencies for $chart" + fi + ) + fi + done + + log_debug "Final helm repository state:" + helm repo list || log_debug "No repositories configured" + log_debug "Final Chart.lock files:" + find charts/ -name "Chart.lock" -exec ls -la {} \; || log_debug "No Chart.lock files found" + + log_info "✅ Helm dependency setup complete" +} + +deploy_eoapi() { + log_info "Deploying eoAPI..." + + SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" + PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + + cd "$PROJECT_ROOT" || { + log_error "Failed to change to project root directory: $PROJECT_ROOT" + exit 1 + } + + if [ ! -d "charts" ]; then + log_error "charts/ directory not found in $(pwd)" + exit 1 + fi + + cd charts || exit + + HELM_CMD="helm upgrade --install $RELEASE_NAME ./eoapi" + HELM_CMD="$HELM_CMD --namespace $NAMESPACE --create-namespace" + HELM_CMD="$HELM_CMD --timeout=$TIMEOUT" + + if [ -f "./eoapi/values.yaml" ]; then + HELM_CMD="$HELM_CMD -f ./eoapi/values.yaml" + fi + + if [ -f "./eoapi/profiles/experimental.yaml" ]; then + case "$(kubectl config current-context 2>/dev/null || echo "unknown")" in + *"minikube"*|*"k3d"*|"default") + log_info "Using experimental profile configuration..." + HELM_CMD="$HELM_CMD -f ./eoapi/profiles/experimental.yaml" + ;; + esac + fi + + if [ "$CI_MODE" = true ]; then + log_info "Applying CI-specific overrides..." + # Use experimental + k3s profiles, then override for CI + if [ -f "./eoapi/profiles/experimental.yaml" ]; then + HELM_CMD="$HELM_CMD -f ./eoapi/profiles/experimental.yaml" + fi + if [ -f "./eoapi/profiles/local/k3s.yaml" ]; then + HELM_CMD="$HELM_CMD -f ./eoapi/profiles/local/k3s.yaml" + fi + HELM_CMD="$HELM_CMD --set testing=true" + HELM_CMD="$HELM_CMD --set ingress.host=eoapi.local" + HELM_CMD="$HELM_CMD --set ingress.className=traefik" + + HELM_CMD="$HELM_CMD --set monitoring.prometheus.enabled=true" + HELM_CMD="$HELM_CMD --set monitoring.prometheusAdapter.enabled=true" + HELM_CMD="$HELM_CMD --set observability.grafana.enabled=true" + HELM_CMD="$HELM_CMD --set monitoring.prometheusAdapter.prometheus.url=http://$RELEASE_NAME-prometheus-server.eoapi.svc.cluster.local" + + # Set UPSTREAM_URL and OIDC_DISCOVERY_URL dynamically for stac-auth-proxy when experimental profile is used + # The experimental profile enables stac-auth-proxy, so we need to set the correct service names + # Also configure STAC service to run without root path when behind auth proxy + HELM_CMD="$HELM_CMD --set stac-auth-proxy.env.UPSTREAM_URL=http://$RELEASE_NAME-stac:8080" + HELM_CMD="$HELM_CMD --set stac-auth-proxy.env.OIDC_DISCOVERY_URL=http://$RELEASE_NAME-mock-oidc-server.$NAMESPACE.svc.cluster.local:8080/.well-known/openid-configuration" + # Note: initContainer service names are dynamically replaced by the stac-auth-proxy-patch job + # Configure STAC service to run without root path when behind auth proxy + # Empty string makes STAC service run at root path (no --root-path argument) + HELM_CMD="$HELM_CMD --set 'stac.overrideRootPath='" + + HELM_CMD="$HELM_CMD --set eoapi-notifier.enabled=true" + HELM_CMD="$HELM_CMD --set eoapi-notifier.config.sources[0].config.connection.existingSecret.name=$RELEASE_NAME-pguser-eoapi" + + elif [ -f "./eoapi/test-local-values.yaml" ]; then + log_info "Using local test configuration..." + HELM_CMD="$HELM_CMD -f ./eoapi/test-local-values.yaml" + HELM_CMD="$HELM_CMD --set eoapi-notifier.config.sources[0].config.connection.existingSecret.name=$RELEASE_NAME-pguser-eoapi" + + else + local current_context + current_context=$(kubectl config current-context 2>/dev/null || echo "") + + case "$current_context" in + *"k3d"*) + if [ -f "./eoapi/profiles/local/k3s.yaml" ]; then + log_info "Adding k3s-specific overrides..." + HELM_CMD="$HELM_CMD -f ./eoapi/profiles/local/k3s.yaml" + fi + ;; + "minikube") + if [ -f "./eoapi/profiles/local/minikube.yaml" ]; then + log_info "Adding minikube-specific overrides..." + HELM_CMD="$HELM_CMD -f ./eoapi/profiles/local/minikube.yaml" + fi + ;; + esac + fi + + GITHUB_SHA=${GITHUB_SHA:-} + if [ -n "$GITHUB_SHA" ]; then + HELM_CMD="$HELM_CMD --set gitSha=$GITHUB_SHA" + elif [ -n "$(git rev-parse HEAD 2>/dev/null)" ]; then + HELM_CMD="$HELM_CMD --set gitSha=$(git rev-parse HEAD | cut -c1-10)" + fi + + log_info "Running: $HELM_CMD" + eval "$HELM_CMD" + + cd "$PROJECT_ROOT" || exit + + if kubectl get job -n "$NAMESPACE" -l "app=$RELEASE_NAME-pgstac-migrate" >/dev/null 2>&1; then + log_info "Waiting for pgstac-migrate job to complete..." + if ! kubectl wait --for=condition=complete job -l "app=$RELEASE_NAME-pgstac-migrate" -n "$NAMESPACE" --timeout=600s; then + log_error "pgstac-migrate job failed to complete" + kubectl describe job -l "app=$RELEASE_NAME-pgstac-migrate" -n "$NAMESPACE" + kubectl logs -l "app=$RELEASE_NAME-pgstac-migrate" -n "$NAMESPACE" --tail=50 || true + exit 1 + fi + fi + + if kubectl get job -n "$NAMESPACE" -l "app=$RELEASE_NAME-pgstac-load-samples" >/dev/null 2>&1; then + log_info "Waiting for pgstac-load-samples job to complete..." + if ! kubectl wait --for=condition=complete job -l "app=$RELEASE_NAME-pgstac-load-samples" -n "$NAMESPACE" --timeout=600s; then + log_error "pgstac-load-samples job failed to complete" + kubectl describe job -l "app=$RELEASE_NAME-pgstac-load-samples" -n "$NAMESPACE" + kubectl logs -l "app=$RELEASE_NAME-pgstac-load-samples" -n "$NAMESPACE" --tail=50 || true + exit 1 + fi + fi + + log_info "eoAPI deployment completed successfully!" + log_info "Services available in namespace: $NAMESPACE" + + if [ "$CI_MODE" != true ]; then + log_info "To run integration tests: make integration" + log_info "To check status: kubectl get pods -n $NAMESPACE" + fi +} + +cleanup_deployment() { + log_info "Cleaning up resources for release: $RELEASE_NAME" + + if ! validate_namespace "$NAMESPACE"; then + log_warn "Namespace '$NAMESPACE' not found, skipping cleanup" + return 0 + fi + + cleanup_resource() { + local resource_type="$1" + local resources + + log_info "Cleaning up ${resource_type}..." + resources=$(kubectl get "$resource_type" -n "$NAMESPACE" --no-headers 2>/dev/null | grep "$RELEASE_NAME" | awk '{print $1}' || true) + + if [ -n "$resources" ]; then + log_info " Found ${resource_type}: $resources" + echo "$resources" | xargs -r kubectl delete "$resource_type" -n "$NAMESPACE" + else + log_info " No ${resource_type} found for $RELEASE_NAME" + fi + } + + cleanup_resource "ingress" + cleanup_resource "service" + cleanup_resource "deployment" + cleanup_resource "job" + cleanup_resource "configmap" + cleanup_resource "secret" + cleanup_resource "pvc" + + log_info "Attempting helm uninstall..." + helm uninstall "$RELEASE_NAME" -n "$NAMESPACE" 2>/dev/null || log_warn "No helm release found for $RELEASE_NAME" + + log_info "✅ Cleanup complete for release: $RELEASE_NAME" +} + +validate_ci_deployment() { + log_info "=== CI Post-Deployment Validation ===" + log_info "Validating Helm Dependencies Post-Deployment..." + log_info "Configured helm repositories:" + helm repo list 2>/dev/null || log_warn "No repositories configured" + echo "" + + log_info "Chart.lock files:" + find charts/ -name "Chart.lock" -exec ls -la {} \; 2>/dev/null || log_info "No Chart.lock files found" + echo "" + + log_info "Downloaded chart dependencies:" + find charts/ -name "charts" -type d -exec ls -la {} \; 2>/dev/null || log_info "No chart dependencies found" + echo "" + + log_info "Checking for knative-operator deployment:" + kubectl get deployment knative-operator --all-namespaces -o wide 2>/dev/null || log_info "knative-operator deployment not found" + echo "" + + log_info "Helm release status:" + helm status "$RELEASE_NAME" -n "$NAMESPACE" 2>/dev/null || log_warn "Release status unavailable" + echo "" + + log_info "Resources in $NAMESPACE namespace:" + kubectl get all -n "$NAMESPACE" -o wide 2>/dev/null || log_warn "No resources in $NAMESPACE namespace" + echo "" + + log_info "Pod status:" + kubectl get pods -n "$NAMESPACE" -o wide 2>/dev/null || log_warn "No pods in $NAMESPACE namespace" + + log_info "=== Knative Integration Debug ===" + kubectl get deployments -l app.kubernetes.io/name=knative-operator --all-namespaces 2>/dev/null || log_info "Knative operator not found" + kubectl get crd | grep knative 2>/dev/null || log_info "No Knative CRDs found" + kubectl get knativeservings --all-namespaces -o wide 2>/dev/null || log_info "No KnativeServing resources" + kubectl get knativeeventings --all-namespaces -o wide 2>/dev/null || log_info "No KnativeEventing resources" + kubectl get pods -n knative-serving 2>/dev/null || log_info "No knative-serving namespace" + kubectl get pods -n knative-eventing 2>/dev/null || log_info "No knative-eventing namespace" + kubectl get pods -l app.kubernetes.io/name=eoapi-notifier -n "$NAMESPACE" 2>/dev/null || log_info "No eoapi-notifier pods" + kubectl get ksvc -n "$NAMESPACE" 2>/dev/null || log_info "No Knative services in $NAMESPACE namespace" + kubectl get sinkbindings -n "$NAMESPACE" 2>/dev/null || log_info "No SinkBindings in $NAMESPACE namespace" + + return 0 +} + +case $COMMAND in + setup) + setup_helm_dependencies + ;; + cleanup) + cleanup_deployment + ;; + deploy) + install_pgo + setup_helm_dependencies + deploy_eoapi + + if [ "$CI_MODE" = true ]; then + validate_ci_deployment || exit 1 + fi + ;; + *) + log_error "Unknown command: $COMMAND" + exit 1 + ;; +esac diff --git a/scripts/deployment.sh b/scripts/deployment.sh index bcfcbcfc..20df5901 100755 --- a/scripts/deployment.sh +++ b/scripts/deployment.sh @@ -69,10 +69,12 @@ run_deployment() { helm dependency update charts/eoapi local helm_cmd="helm upgrade --install $RELEASE_NAME charts/eoapi -n $NAMESPACE --create-namespace" + local use_experimental=false if [[ -f "charts/eoapi/profiles/experimental.yaml" ]]; then log_info "Applying experimental profile..." helm_cmd="$helm_cmd -f charts/eoapi/profiles/experimental.yaml" + use_experimental=true fi if [[ -f "charts/eoapi/profiles/local/k3s.yaml" ]]; then log_info "Applying k3s local profile..." @@ -82,6 +84,18 @@ run_deployment() { helm_cmd="$helm_cmd --set eoapi-notifier.config.sources[0].type=pgstac" helm_cmd="$helm_cmd --set eoapi-notifier.config.sources[0].config.connection.existingSecret.name=$RELEASE_NAME-pguser-eoapi" + # Set UPSTREAM_URL and OIDC_DISCOVERY_URL dynamically for stac-auth-proxy when experimental profile is used + # The experimental profile enables stac-auth-proxy, so we need to set the correct service names + # Also configure STAC service to run without root path when behind auth proxy + if [[ "$use_experimental" == "true" ]]; then + helm_cmd="$helm_cmd --set stac-auth-proxy.env.UPSTREAM_URL=http://$RELEASE_NAME-stac:8080" + helm_cmd="$helm_cmd --set stac-auth-proxy.env.OIDC_DISCOVERY_URL=http://$RELEASE_NAME-mock-oidc-server.$NAMESPACE.svc.cluster.local:8080/.well-known/openid-configuration" + # Note: initContainer service names are dynamically replaced by the stac-auth-proxy-patch job + # Configure STAC service to run without root path when behind auth proxy + # Empty string makes STAC service run at root path (no --root-path argument) + helm_cmd="$helm_cmd --set 'stac.overrideRootPath='" + fi + if is_ci; then log_info "Applying CI-specific configurations..." diff --git a/tests/conftest.py b/tests/conftest.py index aaa782d8..be4d6ec2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,17 +11,17 @@ @pytest.fixture(scope="session") def raster_endpoint() -> str: - return os.getenv("RASTER_ENDPOINT", "http://127.0.0.1/raster") + return os.getenv("RASTER_ENDPOINT", "http://localhost/raster") @pytest.fixture(scope="session") def vector_endpoint() -> str: - return os.getenv("VECTOR_ENDPOINT", "http://127.0.0.1/vector") + return os.getenv("VECTOR_ENDPOINT", "http://localhost/vector") @pytest.fixture(scope="session") def stac_endpoint() -> str: - return os.getenv("STAC_ENDPOINT", "http://127.0.0.1/stac") + return os.getenv("STAC_ENDPOINT", "http://localhost/stac") def get_namespace() -> str: diff --git a/tests/integration/test_stac_auth.py b/tests/integration/test_stac_auth.py new file mode 100644 index 00000000..5a2870ef --- /dev/null +++ b/tests/integration/test_stac_auth.py @@ -0,0 +1,107 @@ +"""Test STAC API with auth proxy authentication.""" + +import os + +import httpx +import pytest + +timeout = httpx.Timeout(15.0, connect=60.0) +client = httpx.Client( + timeout=timeout, + verify=not bool(os.getenv("IGNORE_SSL_VERIFICATION", False)), +) + + +@pytest.fixture +def valid_token() -> str: + """Generate a realistic JWT token for auth testing.""" + return "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJzdWIiOiJ0ZXN0LXVzZXIiLCJhdWQiOiJzdGFjLWFwaSIsImlhdCI6MTcwNDEwNDQwMCwiZXhwIjoyMDA0MTA0NDAwLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIHN0YWM6cmVhZCBzdGFjOndyaXRlIn0.fake-signature" + + +def test_stac_auth_without_token(stac_endpoint: str) -> None: + """Test write operation without token - should be rejected.""" + resp = client.post( + f"{stac_endpoint}/collections/noaa-emergency-response/items", + headers={"Content-Type": "application/json"}, + json={ + "id": "test-no-token", + "type": "Feature", + "stac_version": "1.0.0", + "properties": {"datetime": "2024-01-01T00:00:00Z"}, + "geometry": {"type": "Point", "coordinates": [0, 0]}, + "links": [], + "assets": {}, + "collection": "noaa-emergency-response", + "bbox": [-0.1, -0.1, 0.1, 0.1], + }, + ) + + if resp.status_code in [200, 201]: + # Auth proxy should reject requests without tokens + assert resp.status_code in [401, 403], ( + f"Expected auth error, got {resp.status_code}: {resp.text[:100]}" + ) + + +def test_stac_auth_with_invalid_token(stac_endpoint: str) -> None: + """Test write operation with invalid token - should be rejected.""" + resp = client.post( + f"{stac_endpoint}/collections/noaa-emergency-response/items", + headers={ + "Authorization": "Bearer invalid-token", + "Content-Type": "application/json", + }, + json={ + "id": "test-invalid-token", + "type": "Feature", + "stac_version": "1.0.0", + "properties": {"datetime": "2024-01-01T00:00:00Z"}, + "geometry": {"type": "Point", "coordinates": [0, 0]}, + "links": [], + "assets": {}, + "collection": "noaa-emergency-response", + "bbox": [-0.1, -0.1, 0.1, 0.1], + }, + ) + + assert resp.status_code in [401, 403], ( + f"Expected auth error with invalid token, got {resp.status_code}: {resp.text[:100]}" + ) + + +def test_stac_auth_with_valid_token( + stac_endpoint: str, valid_token: str +) -> None: + """Test write operation with valid token - tests actual auth proxy behavior.""" + resp = client.post( + f"{stac_endpoint}/collections/noaa-emergency-response/items", + headers={ + "Authorization": valid_token, + "Content-Type": "application/json", + }, + json={ + "id": "test-valid-token", + "type": "Feature", + "stac_version": "1.0.0", + "properties": {"datetime": "2024-01-01T00:00:00Z"}, + "geometry": {"type": "Point", "coordinates": [0, 0]}, + "links": [], + "assets": {}, + "collection": "noaa-emergency-response", + "bbox": [-0.1, -0.1, 0.1, 0.1], + }, + ) + + # With mock OIDC server deployed, the token should be rejected (invalid signature) + assert resp.status_code in [401, 403], ( + f"Expected auth rejection with test token, got {resp.status_code}: {resp.text[:100]}" + ) + + +def test_stac_read_operations_work(stac_endpoint: str) -> None: + """Test that read operations work without auth.""" + resp = client.get(stac_endpoint) + assert resp.status_code == 200 + + resp = client.get(f"{stac_endpoint}/collections") + assert resp.status_code == 200 From 38391286cb920c6ca972faea0a704e012acb67c5 Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Sat, 22 Nov 2025 15:23:10 +0100 Subject: [PATCH 3/8] Temporarily patch stac-auth-proxy chart until PR comes in. --- .../core/stac-auth-proxy-patch-rbac.yaml | 34 ++++++++++++ .../templates/core/stac-auth-proxy-patch.yaml | 54 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 charts/eoapi/templates/core/stac-auth-proxy-patch-rbac.yaml create mode 100644 charts/eoapi/templates/core/stac-auth-proxy-patch.yaml diff --git a/charts/eoapi/templates/core/stac-auth-proxy-patch-rbac.yaml b/charts/eoapi/templates/core/stac-auth-proxy-patch-rbac.yaml new file mode 100644 index 00000000..fc7bbe8f --- /dev/null +++ b/charts/eoapi/templates/core/stac-auth-proxy-patch-rbac.yaml @@ -0,0 +1,34 @@ +{{- if index .Values "stac-auth-proxy" "enabled" }} +{{- if index .Values "stac-auth-proxy" "initContainers" }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Release.Name }}-stac-auth-proxy-patch + namespace: {{ .Release.Namespace }} + labels: + {{- include "eoapi.labels" . | nindent 4 }} +rules: +- apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get", "patch"] + resourceNames: + - {{ .Release.Name }}-stac-auth-proxy +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Release.Name }}-stac-auth-proxy-patch + namespace: {{ .Release.Namespace }} + labels: + {{- include "eoapi.labels" . | nindent 4 }} +subjects: +- kind: ServiceAccount + name: {{ include "eoapi.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +roleRef: + kind: Role + name: {{ .Release.Name }}-stac-auth-proxy-patch + apiGroup: rbac.authorization.k8s.io +{{- end }} +{{- end }} diff --git a/charts/eoapi/templates/core/stac-auth-proxy-patch.yaml b/charts/eoapi/templates/core/stac-auth-proxy-patch.yaml new file mode 100644 index 00000000..9307bfdf --- /dev/null +++ b/charts/eoapi/templates/core/stac-auth-proxy-patch.yaml @@ -0,0 +1,54 @@ +{{- if index .Values "stac-auth-proxy" "enabled" }} +{{- if index .Values "stac-auth-proxy" "initContainers" }} +# NOTE: This hook patches the stac-auth-proxy deployment to add initContainers. +# Since Helm hooks run AFTER all resources (including subcharts) are installed, +# the deployment will start once without initContainers, then be patched and rolled out. +# This is a temporary workaround until upstream stac-auth-proxy chart adds native initContainers support. +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Release.Name }}-stac-auth-proxy-patch + namespace: {{ .Release.Namespace }} + labels: + {{- include "eoapi.labels" . | nindent 4 }} + annotations: + helm.sh/hook: "post-install,post-upgrade" + helm.sh/hook-weight: "5" + helm.sh/hook-delete-policy: "before-hook-creation,hook-succeeded" +spec: + template: + metadata: + labels: + {{- include "eoapi.labels" . | nindent 8 }} + spec: + restartPolicy: Never + serviceAccountName: {{ include "eoapi.serviceAccountName" . }} + containers: + - name: patch-deployment + image: bitnami/kubectl:latest + imagePullPolicy: IfNotPresent + command: + - /bin/bash + - -c + - | + set -e + DEPLOYMENT_NAME="{{ .Release.Name }}-stac-auth-proxy" + NAMESPACE="{{ .Release.Namespace }}" + echo "Waiting for deployment $DEPLOYMENT_NAME..." + for i in {1..30}; do + kubectl get deployment "$DEPLOYMENT_NAME" -n "$NAMESPACE" &>/dev/null && break + sleep 2 + done + EXISTING_INIT=$(kubectl get deployment "$DEPLOYMENT_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.template.spec.initContainers}' 2>/dev/null || echo "") + if [ -n "$EXISTING_INIT" ] && [ "$EXISTING_INIT" != "null" ]; then + echo "Deployment already has initContainers, skipping" + exit 0 + fi + echo "Patching deployment with initContainers..." + INIT_CONTAINERS_JSON='{{ index .Values "stac-auth-proxy" "initContainers" | toJson }}' + kubectl patch deployment "$DEPLOYMENT_NAME" -n "$NAMESPACE" --type='json' -p="[{\"op\":\"add\",\"path\":\"/spec/template/spec/initContainers\",\"value\":$INIT_CONTAINERS_JSON}]" + echo "Waiting for rollout..." + kubectl rollout status deployment/"$DEPLOYMENT_NAME" -n "$NAMESPACE" --timeout=300s + backoffLimit: 2 +{{- end }} +{{- end }} From 41a2bd68a424254285ed083c22aff8ba477eb253 Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Tue, 25 Nov 2025 18:27:08 +0100 Subject: [PATCH 4/8] Actually use mock-oidc-server. --- charts/eoapi/profiles/experimental.yaml | 3 ++ charts/eoapi/templates/mock-oidc/ingress.yaml | 38 ++++++++++++++++++ scripts/test/integration.sh | 2 + scripts/test/notification.sh | 2 + tests/conftest.py | 40 ++++++++++++++++++- tests/integration/test_stac_auth.py | 12 +++--- tests/notification/test_notifications.py | 14 +++++-- .../notification/test_pgstac_notifications.py | 37 +++++++++++++++-- 8 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 charts/eoapi/templates/mock-oidc/ingress.yaml diff --git a/charts/eoapi/profiles/experimental.yaml b/charts/eoapi/profiles/experimental.yaml index 52472ee6..d54f917f 100644 --- a/charts/eoapi/profiles/experimental.yaml +++ b/charts/eoapi/profiles/experimental.yaml @@ -401,6 +401,9 @@ mockOidcServer: service: type: ClusterIP port: 8080 + ingress: + enabled: true + path: "/mock-oidc" resources: limits: cpu: 100m diff --git a/charts/eoapi/templates/mock-oidc/ingress.yaml b/charts/eoapi/templates/mock-oidc/ingress.yaml new file mode 100644 index 00000000..d54ad691 --- /dev/null +++ b/charts/eoapi/templates/mock-oidc/ingress.yaml @@ -0,0 +1,38 @@ +{{- if and .Values.mockOidcServer.enabled .Values.mockOidcServer.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "eoapi.fullname" . }}-mock-oidc-server + namespace: {{ .Release.Namespace }} + labels: + {{- include "eoapi.labels" . | nindent 4 }} + app.kubernetes.io/component: mock-oidc-server + annotations: + nginx.ingress.kubernetes.io/rewrite-target: /$2 + {{- if .Values.mockOidcServer.ingress.annotations }} + {{- toYaml .Values.mockOidcServer.ingress.annotations | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls.enabled }} + tls: + - hosts: + - {{ .Values.ingress.host | quote }} + {{- if .Values.ingress.tls.secretName }} + secretName: {{ .Values.ingress.tls.secretName }} + {{- end }} + {{- end }} + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: {{ .Values.mockOidcServer.ingress.path }}(/|$)(.*) + pathType: {{ .Values.ingress.pathType | default "Prefix" }} + backend: + service: + name: {{ include "eoapi.fullname" . }}-mock-oidc-server + port: + number: {{ .Values.mockOidcServer.service.port | default 8080 }} +{{- end }} diff --git a/scripts/test/integration.sh b/scripts/test/integration.sh index b500f8a4..f1a2ecb6 100755 --- a/scripts/test/integration.sh +++ b/scripts/test/integration.sh @@ -98,11 +98,13 @@ run_integration_tests() { export STAC_ENDPOINT="${STAC_ENDPOINT:-http://$actual_host/stac}" export RASTER_ENDPOINT="${RASTER_ENDPOINT:-http://$actual_host/raster}" export VECTOR_ENDPOINT="${VECTOR_ENDPOINT:-http://$actual_host/vector}" + export MOCK_OIDC_ENDPOINT="${MOCK_OIDC_ENDPOINT:-http://$actual_host/mock-oidc}" log_info "Test endpoints configured:" log_info " STAC: $STAC_ENDPOINT" log_info " Raster: $RASTER_ENDPOINT" log_info " Vector: $VECTOR_ENDPOINT" + log_info " Mock OIDC: $MOCK_OIDC_ENDPOINT" log_info "Running service warmup..." for endpoint in "$STAC_ENDPOINT" "$RASTER_ENDPOINT/healthz" "$VECTOR_ENDPOINT/healthz"; do diff --git a/scripts/test/notification.sh b/scripts/test/notification.sh index 399cb98c..1e0a819a 100755 --- a/scripts/test/notification.sh +++ b/scripts/test/notification.sh @@ -38,11 +38,13 @@ run_notification_tests() { export STAC_ENDPOINT="http://${ingress_host}/stac" export RASTER_ENDPOINT="http://${ingress_host}/raster" export VECTOR_ENDPOINT="http://${ingress_host}/vector" + export MOCK_OIDC_ENDPOINT="http://${ingress_host}/mock-oidc" else # Fall back to localhost (assumes port-forward or local ingress) export STAC_ENDPOINT="http://localhost/stac" export RASTER_ENDPOINT="http://localhost/raster" export VECTOR_ENDPOINT="http://localhost/vector" + export MOCK_OIDC_ENDPOINT="http://localhost/mock-oidc" fi fi export NAMESPACE diff --git a/tests/conftest.py b/tests/conftest.py index be4d6ec2..c6d7ae98 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,6 +24,44 @@ def stac_endpoint() -> str: return os.getenv("STAC_ENDPOINT", "http://localhost/stac") +@pytest.fixture(scope="session") +def mock_oidc_endpoint() -> str: + return os.getenv("MOCK_OIDC_ENDPOINT", "http://localhost/mock-oidc") + + +def get_mock_token(mock_oidc_endpoint: Optional[str] = None) -> str: + """Get valid JWT token from mock OIDC server.""" + if mock_oidc_endpoint is None: + mock_oidc_endpoint = os.getenv( + "MOCK_OIDC_ENDPOINT", "http://localhost/mock-oidc" + ) + + response = requests.post( + mock_oidc_endpoint, + data={ + "username": "test-user", + "scopes": "openid profile stac:read stac:write", + }, + timeout=5, + ) + + if response.status_code == 200: + html = response.text + if 'value="' in html: + token = html.split('value="')[1].split('"')[0] + return f"Bearer {token}" + + raise Exception( + f"Could not get token from mock OIDC server at {mock_oidc_endpoint}" + ) + + +@pytest.fixture +def auth_token(mock_oidc_endpoint: str) -> str: + """Get valid JWT token for auth testing.""" + return get_mock_token(mock_oidc_endpoint) + + def get_namespace() -> str: """Get the namespace from environment variable.""" return os.environ.get("NAMESPACE", "eoapi") @@ -76,7 +114,7 @@ def kubectl_port_forward( def kubectl_proxy( - port: int = 8001, namespace: str = None + port: int = 8001, namespace: Optional[str] = None ) -> subprocess.Popen[str]: """Start kubectl proxy for accessing services via Kubernetes API.""" cmd = ["kubectl", "proxy", f"--port={port}"] diff --git a/tests/integration/test_stac_auth.py b/tests/integration/test_stac_auth.py index 5a2870ef..b2a05d67 100644 --- a/tests/integration/test_stac_auth.py +++ b/tests/integration/test_stac_auth.py @@ -13,9 +13,9 @@ @pytest.fixture -def valid_token() -> str: - """Generate a realistic JWT token for auth testing.""" - return "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJzdWIiOiJ0ZXN0LXVzZXIiLCJhdWQiOiJzdGFjLWFwaSIsImlhdCI6MTcwNDEwNDQwMCwiZXhwIjoyMDA0MTA0NDAwLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIHN0YWM6cmVhZCBzdGFjOndyaXRlIn0.fake-signature" +def valid_token(auth_token: str) -> str: + """Get valid JWT token for auth testing.""" + return auth_token def test_stac_auth_without_token(stac_endpoint: str) -> None: @@ -92,9 +92,9 @@ def test_stac_auth_with_valid_token( }, ) - # With mock OIDC server deployed, the token should be rejected (invalid signature) - assert resp.status_code in [401, 403], ( - f"Expected auth rejection with test token, got {resp.status_code}: {resp.text[:100]}" + # With valid token from mock OIDC server, request should succeed + assert resp.status_code in [200, 201], ( + f"Expected success with valid token, got {resp.status_code}: {resp.text[:100]}" ) diff --git a/tests/notification/test_notifications.py b/tests/notification/test_notifications.py index 0e3ab023..5fb60ef8 100644 --- a/tests/notification/test_notifications.py +++ b/tests/notification/test_notifications.py @@ -198,7 +198,7 @@ def test_database_notification_triggers_exist() -> None: assert "True" in result.stdout, "eoapi-notifier pod should be ready" -def test_end_to_end_notification_flow() -> None: +def test_end_to_end_notification_flow(auth_token: str) -> None: """Test complete flow: database item change → eoapi-notifier → Knative CloudEvents sink.""" # Check if notifications are enabled @@ -261,11 +261,14 @@ def test_end_to_end_notification_flow() -> None: text=True, ).stdout - # Create item via STAC API + # Create item via STAC API using auth token response = requests.post( f"{stac_endpoint}/collections/noaa-emergency-response/items", json=test_item, - headers={"Content-Type": "application/json"}, + headers={ + "Content-Type": "application/json", + "Authorization": auth_token, + }, timeout=10, ) @@ -294,7 +297,10 @@ def test_end_to_end_notification_flow() -> None: # Clean up requests.delete( f"{stac_endpoint}/collections/noaa-emergency-response/items/{test_item['id']}", - headers={"Content-Type": "application/json"}, + headers={ + "Content-Type": "application/json", + "Authorization": auth_token, + }, timeout=10, ) diff --git a/tests/notification/test_pgstac_notifications.py b/tests/notification/test_pgstac_notifications.py index 176d4d62..1e33b4a9 100644 --- a/tests/notification/test_pgstac_notifications.py +++ b/tests/notification/test_pgstac_notifications.py @@ -49,13 +49,16 @@ def notifications_enabled() -> bool: @pytest.fixture -def stac_client() -> Dict[str, Any]: - """Create a STAC API client configuration.""" +def stac_client(auth_token: str) -> Dict[str, Any]: + """Create a STAC API client configuration with valid token from mock OIDC.""" stac_endpoint = os.getenv("STAC_ENDPOINT", "http://localhost/stac") return { "base_url": stac_endpoint, - "headers": {"Content-Type": "application/json"}, + "headers": { + "Content-Type": "application/json", + "Authorization": auth_token, + }, "timeout": 10, } @@ -234,6 +237,21 @@ def test_update_notification( f"Failed to create item: {response.text}" ) + before_time = time.time() + + updated_item["properties"]["description"] = "Updated for notification test" + + response = requests.put( + f"{stac_client['base_url']}/collections/noaa-emergency-response/items/{test_item['id']}", + json=updated_item, + headers=stac_client["headers"], + timeout=stac_client["timeout"], + ) + + assert response.status_code in [200, 201], ( + f"Failed to create item: {response.text}" + ) + test_item["properties"]["test_version"] = "v2" before_time = time.time() @@ -299,6 +317,18 @@ def test_delete_notification( before_time = time.time() + response = requests.delete( + f"{stac_client['base_url']}/collections/noaa-emergency-response/items/{test_item['id']}", + headers=stac_client["headers"], + timeout=stac_client["timeout"], + ) + + assert response.status_code in [200, 201], ( + f"Failed to create item: {response.text}" + ) + + before_time = time.time() + response = requests.delete( f"{stac_client['base_url']}/collections/noaa-emergency-response/items/{test_item_id}", headers=stac_client["headers"], @@ -351,6 +381,7 @@ def test_bulk_operations_notification( headers=stac_client["headers"], timeout=stac_client["timeout"], ) + assert response.status_code in [200, 201], ( f"Failed to create item: {response.text}" ) From 5da9e464afc68735b4ec55004353d4e7668706c1 Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Tue, 25 Nov 2025 18:38:09 +0100 Subject: [PATCH 5/8] Removed stac-auth-proxy charts patch. --- charts/eoapi/Chart.yaml | 2 +- .../core/stac-auth-proxy-patch-rbac.yaml | 34 ------------ .../templates/core/stac-auth-proxy-patch.yaml | 54 ------------------- charts/eoapi/templates/mock-oidc/ingress.yaml | 38 ------------- .../eoapi/templates/networking/ingress.yaml | 20 +++++++ .../networking/traefik-middleware.yaml | 3 ++ tests/conftest.py | 15 ++++-- tests/integration/test_stac_auth.py | 7 +-- .../notification/test_pgstac_notifications.py | 30 +---------- 9 files changed, 41 insertions(+), 162 deletions(-) delete mode 100644 charts/eoapi/templates/core/stac-auth-proxy-patch-rbac.yaml delete mode 100644 charts/eoapi/templates/core/stac-auth-proxy-patch.yaml delete mode 100644 charts/eoapi/templates/mock-oidc/ingress.yaml diff --git a/charts/eoapi/Chart.yaml b/charts/eoapi/Chart.yaml index 399ac920..4dd472b0 100644 --- a/charts/eoapi/Chart.yaml +++ b/charts/eoapi/Chart.yaml @@ -78,6 +78,6 @@ dependencies: repository: https://grafana.github.io/helm-charts condition: observability.grafana.enabled - name: stac-auth-proxy - version: "0.1.0" + version: "0.1.1" repository: "oci://ghcr.io/developmentseed/stac-auth-proxy/charts" condition: stac-auth-proxy.enabled diff --git a/charts/eoapi/templates/core/stac-auth-proxy-patch-rbac.yaml b/charts/eoapi/templates/core/stac-auth-proxy-patch-rbac.yaml deleted file mode 100644 index fc7bbe8f..00000000 --- a/charts/eoapi/templates/core/stac-auth-proxy-patch-rbac.yaml +++ /dev/null @@ -1,34 +0,0 @@ -{{- if index .Values "stac-auth-proxy" "enabled" }} -{{- if index .Values "stac-auth-proxy" "initContainers" }} ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: {{ .Release.Name }}-stac-auth-proxy-patch - namespace: {{ .Release.Namespace }} - labels: - {{- include "eoapi.labels" . | nindent 4 }} -rules: -- apiGroups: ["apps"] - resources: ["deployments"] - verbs: ["get", "patch"] - resourceNames: - - {{ .Release.Name }}-stac-auth-proxy ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ .Release.Name }}-stac-auth-proxy-patch - namespace: {{ .Release.Namespace }} - labels: - {{- include "eoapi.labels" . | nindent 4 }} -subjects: -- kind: ServiceAccount - name: {{ include "eoapi.serviceAccountName" . }} - namespace: {{ .Release.Namespace }} -roleRef: - kind: Role - name: {{ .Release.Name }}-stac-auth-proxy-patch - apiGroup: rbac.authorization.k8s.io -{{- end }} -{{- end }} diff --git a/charts/eoapi/templates/core/stac-auth-proxy-patch.yaml b/charts/eoapi/templates/core/stac-auth-proxy-patch.yaml deleted file mode 100644 index 9307bfdf..00000000 --- a/charts/eoapi/templates/core/stac-auth-proxy-patch.yaml +++ /dev/null @@ -1,54 +0,0 @@ -{{- if index .Values "stac-auth-proxy" "enabled" }} -{{- if index .Values "stac-auth-proxy" "initContainers" }} -# NOTE: This hook patches the stac-auth-proxy deployment to add initContainers. -# Since Helm hooks run AFTER all resources (including subcharts) are installed, -# the deployment will start once without initContainers, then be patched and rolled out. -# This is a temporary workaround until upstream stac-auth-proxy chart adds native initContainers support. -apiVersion: batch/v1 -kind: Job -metadata: - name: {{ .Release.Name }}-stac-auth-proxy-patch - namespace: {{ .Release.Namespace }} - labels: - {{- include "eoapi.labels" . | nindent 4 }} - annotations: - helm.sh/hook: "post-install,post-upgrade" - helm.sh/hook-weight: "5" - helm.sh/hook-delete-policy: "before-hook-creation,hook-succeeded" -spec: - template: - metadata: - labels: - {{- include "eoapi.labels" . | nindent 8 }} - spec: - restartPolicy: Never - serviceAccountName: {{ include "eoapi.serviceAccountName" . }} - containers: - - name: patch-deployment - image: bitnami/kubectl:latest - imagePullPolicy: IfNotPresent - command: - - /bin/bash - - -c - - | - set -e - DEPLOYMENT_NAME="{{ .Release.Name }}-stac-auth-proxy" - NAMESPACE="{{ .Release.Namespace }}" - echo "Waiting for deployment $DEPLOYMENT_NAME..." - for i in {1..30}; do - kubectl get deployment "$DEPLOYMENT_NAME" -n "$NAMESPACE" &>/dev/null && break - sleep 2 - done - EXISTING_INIT=$(kubectl get deployment "$DEPLOYMENT_NAME" -n "$NAMESPACE" -o jsonpath='{.spec.template.spec.initContainers}' 2>/dev/null || echo "") - if [ -n "$EXISTING_INIT" ] && [ "$EXISTING_INIT" != "null" ]; then - echo "Deployment already has initContainers, skipping" - exit 0 - fi - echo "Patching deployment with initContainers..." - INIT_CONTAINERS_JSON='{{ index .Values "stac-auth-proxy" "initContainers" | toJson }}' - kubectl patch deployment "$DEPLOYMENT_NAME" -n "$NAMESPACE" --type='json' -p="[{\"op\":\"add\",\"path\":\"/spec/template/spec/initContainers\",\"value\":$INIT_CONTAINERS_JSON}]" - echo "Waiting for rollout..." - kubectl rollout status deployment/"$DEPLOYMENT_NAME" -n "$NAMESPACE" --timeout=300s - backoffLimit: 2 -{{- end }} -{{- end }} diff --git a/charts/eoapi/templates/mock-oidc/ingress.yaml b/charts/eoapi/templates/mock-oidc/ingress.yaml deleted file mode 100644 index d54ad691..00000000 --- a/charts/eoapi/templates/mock-oidc/ingress.yaml +++ /dev/null @@ -1,38 +0,0 @@ -{{- if and .Values.mockOidcServer.enabled .Values.mockOidcServer.ingress.enabled }} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: {{ include "eoapi.fullname" . }}-mock-oidc-server - namespace: {{ .Release.Namespace }} - labels: - {{- include "eoapi.labels" . | nindent 4 }} - app.kubernetes.io/component: mock-oidc-server - annotations: - nginx.ingress.kubernetes.io/rewrite-target: /$2 - {{- if .Values.mockOidcServer.ingress.annotations }} - {{- toYaml .Values.mockOidcServer.ingress.annotations | nindent 4 }} - {{- end }} -spec: - {{- if .Values.ingress.className }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - {{- if .Values.ingress.tls.enabled }} - tls: - - hosts: - - {{ .Values.ingress.host | quote }} - {{- if .Values.ingress.tls.secretName }} - secretName: {{ .Values.ingress.tls.secretName }} - {{- end }} - {{- end }} - rules: - - host: {{ .Values.ingress.host | quote }} - http: - paths: - - path: {{ .Values.mockOidcServer.ingress.path }}(/|$)(.*) - pathType: {{ .Values.ingress.pathType | default "Prefix" }} - backend: - service: - name: {{ include "eoapi.fullname" . }}-mock-oidc-server - port: - number: {{ .Values.mockOidcServer.service.port | default 8080 }} -{{- end }} diff --git a/charts/eoapi/templates/networking/ingress.yaml b/charts/eoapi/templates/networking/ingress.yaml index ed4dde92..dc49bb70 100644 --- a/charts/eoapi/templates/networking/ingress.yaml +++ b/charts/eoapi/templates/networking/ingress.yaml @@ -78,6 +78,16 @@ spec: number: {{ $.Values.service.port }} {{- end }} + {{- if and $.Values.mockOidcServer.enabled $.Values.mockOidcServer.ingress.enabled }} + - pathType: {{ if eq $.Values.ingress.className "nginx" }}ImplementationSpecific{{ else }}Prefix{{ end }} + path: {{ $.Values.mockOidcServer.ingress.path }}{{ if eq $.Values.ingress.className "nginx" }}(/|$)(.*){{ end }} + backend: + service: + name: {{ $.Release.Name }}-mock-oidc-server + port: + number: {{ $.Values.mockOidcServer.service.port | default 8080 }} + {{- end }} + {{- if $.Values.docServer.enabled }} - pathType: Prefix path: "/{{ $.Values.ingress.rootPath | default "" }}" @@ -138,6 +148,16 @@ spec: number: {{ .Values.service.port }} {{- end }} + {{- if and .Values.mockOidcServer.enabled .Values.mockOidcServer.ingress.enabled }} + - pathType: {{ if eq .Values.ingress.className "nginx" }}ImplementationSpecific{{ else }}Prefix{{ end }} + path: {{ .Values.mockOidcServer.ingress.path }}{{ if eq .Values.ingress.className "nginx" }}(/|$)(.*){{ end }} + backend: + service: + name: {{ .Release.Name }}-mock-oidc-server + port: + number: {{ .Values.mockOidcServer.service.port | default 8080 }} + {{- end }} + {{- if .Values.docServer.enabled }} - pathType: Prefix path: "/{{ $.Values.ingress.rootPath | default "" }}" diff --git a/charts/eoapi/templates/networking/traefik-middleware.yaml b/charts/eoapi/templates/networking/traefik-middleware.yaml index f314c4a2..82bc926c 100644 --- a/charts/eoapi/templates/networking/traefik-middleware.yaml +++ b/charts/eoapi/templates/networking/traefik-middleware.yaml @@ -19,4 +19,7 @@ spec: {{- if .Values.multidim.enabled }} - {{ .Values.multidim.ingress.path }} {{- end }} + {{- if and .Values.mockOidcServer.enabled .Values.mockOidcServer.ingress.enabled }} + - {{ .Values.mockOidcServer.ingress.path }} + {{- end }} {{- end }} diff --git a/tests/conftest.py b/tests/conftest.py index c6d7ae98..e5a0188a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,7 +37,7 @@ def get_mock_token(mock_oidc_endpoint: Optional[str] = None) -> str: ) response = requests.post( - mock_oidc_endpoint, + f"{mock_oidc_endpoint}/", data={ "username": "test-user", "scopes": "openid profile stac:read stac:write", @@ -47,9 +47,16 @@ def get_mock_token(mock_oidc_endpoint: Optional[str] = None) -> str: if response.status_code == 200: html = response.text - if 'value="' in html: - token = html.split('value="')[1].split('"')[0] - return f"Bearer {token}" + # Extract token from textarea element + if "" in html: + # Find the textarea content between > and + start_marker = html.find("", start_marker) + 1 + content_end = html.find("", content_start) + if content_start != -1 and content_end != -1: + token = html[content_start:content_end].strip() + return f"Bearer {token}" raise Exception( f"Could not get token from mock OIDC server at {mock_oidc_endpoint}" diff --git a/tests/integration/test_stac_auth.py b/tests/integration/test_stac_auth.py index b2a05d67..00d252e5 100644 --- a/tests/integration/test_stac_auth.py +++ b/tests/integration/test_stac_auth.py @@ -1,6 +1,7 @@ """Test STAC API with auth proxy authentication.""" import os +import time import httpx import pytest @@ -24,7 +25,7 @@ def test_stac_auth_without_token(stac_endpoint: str) -> None: f"{stac_endpoint}/collections/noaa-emergency-response/items", headers={"Content-Type": "application/json"}, json={ - "id": "test-no-token", + "id": f"test-no-token-{int(time.time() * 1000)}", "type": "Feature", "stac_version": "1.0.0", "properties": {"datetime": "2024-01-01T00:00:00Z"}, @@ -52,7 +53,7 @@ def test_stac_auth_with_invalid_token(stac_endpoint: str) -> None: "Content-Type": "application/json", }, json={ - "id": "test-invalid-token", + "id": f"test-invalid-token-{int(time.time() * 1000)}", "type": "Feature", "stac_version": "1.0.0", "properties": {"datetime": "2024-01-01T00:00:00Z"}, @@ -80,7 +81,7 @@ def test_stac_auth_with_valid_token( "Content-Type": "application/json", }, json={ - "id": "test-valid-token", + "id": f"test-valid-token-{int(time.time() * 1000)}", "type": "Feature", "stac_version": "1.0.0", "properties": {"datetime": "2024-01-01T00:00:00Z"}, diff --git a/tests/notification/test_pgstac_notifications.py b/tests/notification/test_pgstac_notifications.py index 1e33b4a9..752f3486 100644 --- a/tests/notification/test_pgstac_notifications.py +++ b/tests/notification/test_pgstac_notifications.py @@ -239,23 +239,9 @@ def test_update_notification( before_time = time.time() - updated_item["properties"]["description"] = "Updated for notification test" - - response = requests.put( - f"{stac_client['base_url']}/collections/noaa-emergency-response/items/{test_item['id']}", - json=updated_item, - headers=stac_client["headers"], - timeout=stac_client["timeout"], - ) - - assert response.status_code in [200, 201], ( - f"Failed to create item: {response.text}" - ) - + test_item["properties"]["description"] = "Updated for notification test" test_item["properties"]["test_version"] = "v2" - before_time = time.time() - response = requests.put( f"{stac_client['base_url']}/collections/noaa-emergency-response/items/{test_item_id}", json=test_item, @@ -263,7 +249,7 @@ def test_update_notification( timeout=stac_client["timeout"], ) - assert response.status_code in [200, 204], ( + assert response.status_code in [200, 201], ( f"Failed to update item: {response.text}" ) @@ -317,18 +303,6 @@ def test_delete_notification( before_time = time.time() - response = requests.delete( - f"{stac_client['base_url']}/collections/noaa-emergency-response/items/{test_item['id']}", - headers=stac_client["headers"], - timeout=stac_client["timeout"], - ) - - assert response.status_code in [200, 201], ( - f"Failed to create item: {response.text}" - ) - - before_time = time.time() - response = requests.delete( f"{stac_client['base_url']}/collections/noaa-emergency-response/items/{test_item_id}", headers=stac_client["headers"], From 12c3352eb243adba27ab07d5cd55eacbf0cbcc8f Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Thu, 27 Nov 2025 11:01:37 +0100 Subject: [PATCH 6/8] Fixed tests. --- charts/eoapi/profiles/experimental.yaml | 17 ++-------- scripts/deploy.sh | 10 +----- scripts/deployment.sh | 42 ++++++++++++++++++------- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/charts/eoapi/profiles/experimental.yaml b/charts/eoapi/profiles/experimental.yaml index d54f917f..7290988f 100644 --- a/charts/eoapi/profiles/experimental.yaml +++ b/charts/eoapi/profiles/experimental.yaml @@ -368,20 +368,9 @@ stac-auth-proxy: enabled: true service: port: 8080 - # Wait for dependencies to be ready before starting stac-auth-proxy - initContainers: - - name: wait-for-mock-oidc - image: busybox:1.35 - command: ['sh', '-c', 'until nc -z eoapi-mock-oidc-server.eoapi.svc.cluster.local 8080; do echo waiting for mock-oidc; sleep 2; done'] - - name: wait-for-stac - image: busybox:1.35 - command: ['sh', '-c', 'until nc -z eoapi-stac.eoapi.svc.cluster.local 8080; do echo waiting for stac service; sleep 2; done'] - env: - UPSTREAM_URL: "http://eoapi-stac:8080" - # For testing one could deploy a mock OIDC server (https://github.com/alukach/mock-oidc-server) - OIDC_DISCOVERY_URL: "http://eoapi-mock-oidc-server.eoapi.svc.cluster.local:8080/.well-known/openid-configuration" - # For production, point to your actual OpenID Connect provider - # OIDC_DISCOVERY_URL: "https://your-auth-provider.com/.well-known/openid-configuration" + # For testing this will be set dynamically; for production, point to your OIDC server + # env: + # OIDC_DISCOVERY_URL: "http://eoapi-mock-oidc-server.eoapi.svc.cluster.local:8080/.well-known/openid-configuration" ###################### # MOCK OIDC SERVER diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 84edde4e..29fbff60 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -391,15 +391,7 @@ deploy_eoapi() { HELM_CMD="$HELM_CMD --set observability.grafana.enabled=true" HELM_CMD="$HELM_CMD --set monitoring.prometheusAdapter.prometheus.url=http://$RELEASE_NAME-prometheus-server.eoapi.svc.cluster.local" - # Set UPSTREAM_URL and OIDC_DISCOVERY_URL dynamically for stac-auth-proxy when experimental profile is used - # The experimental profile enables stac-auth-proxy, so we need to set the correct service names - # Also configure STAC service to run without root path when behind auth proxy - HELM_CMD="$HELM_CMD --set stac-auth-proxy.env.UPSTREAM_URL=http://$RELEASE_NAME-stac:8080" - HELM_CMD="$HELM_CMD --set stac-auth-proxy.env.OIDC_DISCOVERY_URL=http://$RELEASE_NAME-mock-oidc-server.$NAMESPACE.svc.cluster.local:8080/.well-known/openid-configuration" - # Note: initContainer service names are dynamically replaced by the stac-auth-proxy-patch job - # Configure STAC service to run without root path when behind auth proxy - # Empty string makes STAC service run at root path (no --root-path argument) - HELM_CMD="$HELM_CMD --set 'stac.overrideRootPath='" + HELM_CMD="$HELM_CMD --set eoapi-notifier.enabled=true" HELM_CMD="$HELM_CMD --set eoapi-notifier.config.sources[0].config.connection.existingSecret.name=$RELEASE_NAME-pguser-eoapi" diff --git a/scripts/deployment.sh b/scripts/deployment.sh index 20df5901..d9e769a2 100755 --- a/scripts/deployment.sh +++ b/scripts/deployment.sh @@ -69,12 +69,12 @@ run_deployment() { helm dependency update charts/eoapi local helm_cmd="helm upgrade --install $RELEASE_NAME charts/eoapi -n $NAMESPACE --create-namespace" - local use_experimental=false + local testing_mode=false if [[ -f "charts/eoapi/profiles/experimental.yaml" ]]; then log_info "Applying experimental profile..." helm_cmd="$helm_cmd -f charts/eoapi/profiles/experimental.yaml" - use_experimental=true + testing_mode=true fi if [[ -f "charts/eoapi/profiles/local/k3s.yaml" ]]; then log_info "Applying k3s local profile..." @@ -84,13 +84,26 @@ run_deployment() { helm_cmd="$helm_cmd --set eoapi-notifier.config.sources[0].type=pgstac" helm_cmd="$helm_cmd --set eoapi-notifier.config.sources[0].config.connection.existingSecret.name=$RELEASE_NAME-pguser-eoapi" - # Set UPSTREAM_URL and OIDC_DISCOVERY_URL dynamically for stac-auth-proxy when experimental profile is used - # The experimental profile enables stac-auth-proxy, so we need to set the correct service names + # Set UPSTREAM_URL and OIDC_DISCOVERY_URL dynamically for stac-auth-proxy when testing mode is enabled + # Testing mode enables stac-auth-proxy, so we need to set the correct service names # Also configure STAC service to run without root path when behind auth proxy - if [[ "$use_experimental" == "true" ]]; then - helm_cmd="$helm_cmd --set stac-auth-proxy.env.UPSTREAM_URL=http://$RELEASE_NAME-stac:8080" - helm_cmd="$helm_cmd --set stac-auth-proxy.env.OIDC_DISCOVERY_URL=http://$RELEASE_NAME-mock-oidc-server.$NAMESPACE.svc.cluster.local:8080/.well-known/openid-configuration" - # Note: initContainer service names are dynamically replaced by the stac-auth-proxy-patch job + if [[ "$testing_mode" == "true" ]]; then + # Create temporary values file for dynamic stac-auth-proxy configuration + local temp_values="/tmp/stac-auth-proxy-values-${RELEASE_NAME}.yaml" + cat > "$temp_values" < Date: Thu, 27 Nov 2025 12:37:26 +0100 Subject: [PATCH 7/8] Let's be a bit more patient. --- charts/eoapi/templates/mock-oidc/deployment.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/charts/eoapi/templates/mock-oidc/deployment.yaml b/charts/eoapi/templates/mock-oidc/deployment.yaml index d985d223..d71a1c5f 100644 --- a/charts/eoapi/templates/mock-oidc/deployment.yaml +++ b/charts/eoapi/templates/mock-oidc/deployment.yaml @@ -41,18 +41,30 @@ spec: - name: http containerPort: {{ .Values.mockOidcServer.port | default 8888 }} protocol: TCP - livenessProbe: + startupProbe: httpGet: path: /.well-known/openid-configuration port: http initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 12 + timeoutSeconds: 3 + livenessProbe: + httpGet: + path: /.well-known/openid-configuration + port: http + initialDelaySeconds: 30 periodSeconds: 10 + failureThreshold: 3 + timeoutSeconds: 3 readinessProbe: httpGet: path: /.well-known/openid-configuration port: http initialDelaySeconds: 5 periodSeconds: 5 + failureThreshold: 3 + timeoutSeconds: 3 {{- if .Values.mockOidcServer.resources }} resources: {{- toYaml .Values.mockOidcServer.resources | nindent 10 }} From fae1619784b032e05e3cb758cbb682f542896f84 Mon Sep 17 00:00:00 2001 From: Felix Delattre Date: Fri, 28 Nov 2025 12:21:16 +0100 Subject: [PATCH 8/8] Check that stac-auth-proxy only makes sens with stac enabled. --- charts/eoapi/templates/_helpers/validation.tpl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/charts/eoapi/templates/_helpers/validation.tpl b/charts/eoapi/templates/_helpers/validation.tpl index ea9b49dc..bb4ac3b9 100644 --- a/charts/eoapi/templates/_helpers/validation.tpl +++ b/charts/eoapi/templates/_helpers/validation.tpl @@ -30,9 +30,13 @@ so we use this helper function to check autoscaling rules {{/* Validate stac-auth-proxy configuration Ensures OIDC_DISCOVERY_URL is set when stac-auth-proxy is enabled +Ensures stac-auth-proxy cannot be enabled when stac is disabled */}} {{- define "eoapi.validateStacAuthProxy" -}} {{- if index .Values "stac-auth-proxy" "enabled" }} +{{- if not .Values.stac.enabled }} +{{- fail "stac-auth-proxy cannot be enabled when stac.enabled is false. Enable stac first or disable stac-auth-proxy." }} +{{- end }} {{- if not (index .Values "stac-auth-proxy" "env" "OIDC_DISCOVERY_URL") }} {{- fail "stac-auth-proxy.env.OIDC_DISCOVERY_URL is required when stac-auth-proxy is enabled. Set it to your OpenID Connect discovery URL (e.g., https://your-auth-server/.well-known/openid-configuration)" }} {{- end }}