Repository: developmentseed/stac-auth-proxy
Pull request #44: Helm chart for stac-auth-proxy
Author: login: batpad
Created: 2025-03-27T09:07:54Z | Merged: 2025-04-18T07:51:19Z

First draft for a Helm Chart for stac-auth-proxy.

@emmanuelmathot I liberally used AI to help me write this code, and I think some of the stuff might be a bit much in terms of the configuration options.

Would be great to get just a bit of initial feedback here and then I can work on testing this and then adding the CI to publish the chart, etc.

cc @sunu for visibility


Diff:
diff --git a/.github/workflows/publish-helm.yaml b/.github/workflows/publish-helm.yaml
new file mode 100644
index 00000000..55797bdc
--- /dev/null
+++ b/.github/workflows/publish-helm.yaml
@@ -0,0 +1,44 @@
+name: Publish Helm Chart
+
+on:
+  push:
+    branches:
+      - main
+    paths:
+      - 'helm/**'
+      - '.github/workflows/publish-helm.yaml'
+  release:
+    types: [created]
+
+jobs:
+  publish-helm:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      packages: write
+    
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - name: Install Helm
+        uses: azure/setup-helm@v3
+        with:
+          version: v3.12.1
+
+      - name: Login to GHCR
+        uses: docker/login-action@v3
+        with:
+          registry: ghcr.io
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Package Helm Chart
+        run: |
+          helm package helm/
+
+      - name: Push Helm Chart
+        run: |
+          helm push *.tgz oci://ghcr.io/${{ github.repository }}/charts 
\ No newline at end of file
diff --git a/helm/Chart.yaml b/helm/Chart.yaml
new file mode 100644
index 00000000..d8e0a92d
--- /dev/null
+++ b/helm/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: stac-auth-proxy
+description: A Helm chart for stac-auth-proxy
+type: application
+version: 0.1.0
+appVersion: "1.0.0" 
\ No newline at end of file
diff --git a/helm/README.md b/helm/README.md
new file mode 100644
index 00000000..80c0eab6
--- /dev/null
+++ b/helm/README.md
@@ -0,0 +1,126 @@
+# STAC Auth Proxy Helm Chart
+
+This Helm chart deploys the STAC Auth Proxy, which provides authentication and authorization for STAC APIs.
+
+## Prerequisites
+
+- Kubernetes 1.19+
+- Helm 3.2.0+
+- An OIDC provider (e.g., Auth0, Cognito, Keycloak)
+- A STAC API endpoint
+
+## Installation
+
+### Add the Helm Repository
+
+```bash
+helm registry login ghcr.io
+helm pull oci://ghcr.io/developmentseed/stac-auth-proxy/charts/stac-auth-proxy --version 0.1.0
+```
+
+### Install the Chart
+
+Basic installation with minimal configuration:
+
+```bash
+helm install stac-auth-proxy oci://ghcr.io/developmentseed/stac-auth-proxy/charts/stac-auth-proxy \
+  --set env.UPSTREAM_URL=https://your-stac-api.com/stac \
+  --set env.OIDC_DISCOVERY_URL=https://your-auth-server/.well-known/openid-configuration \
+  --set ingress.host=stac-proxy.your-domain.com
+```
+
+### Using a Values File
+
+Create a `values.yaml` file:
+
+```yaml
+env:
+  UPSTREAM_URL: "https://your-stac-api.com/stac"
+  OIDC_DISCOVERY_URL: "https://your-auth-server/.well-known/openid-configuration"
+  OIDC_DISCOVERY_INTERNAL_URL: "http://auth-server-internal/.well-known/openid-configuration"
+  DEFAULT_PUBLIC: "false"
+  HEALTHZ_PREFIX: "/healthz"
+
+ingress:
+  enabled: true
+  host: "stac-proxy.your-domain.com"
+  tls:
+    enabled: true
+
+resources:
+  limits:
+    cpu: 500m
+    memory: 512Mi
+  requests:
+    cpu: 200m
+    memory: 256Mi
+```
+
+Install using the values file:
+
+```bash
+helm install stac-auth-proxy oci://ghcr.io/developmentseed/stac-auth-proxy/charts/stac-auth-proxy -f values.yaml
+```
+
+### Using Image Pull Secrets
+
+To use private container registries, you can configure image pull secrets:
+
+```yaml
+
+serviceAccount:
+    create: true
+    imagePullSecrets:
+        name: "my-registry-secret"
+```
+
+
+## Configuration
+
+### Required Values
+
+| Parameter | Description |
+|-----------|-------------|
+| `env.UPSTREAM_URL` | URL of the STAC API to proxy |
+| `env.OIDC_DISCOVERY_URL` | OpenID Connect discovery document URL |
+
+### Optional Values
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `env` | Environment variables passed to the container. See [STAC Auth Proxy documentation](https://github.com/developmentseed/stac-auth-proxy#configuration) for details | `{}` |
+| `ingress.enabled` | Enable ingress | `true` |
+| `ingress.className` | Ingress class name | `nginx` |
+| `ingress.host` | Hostname for the ingress | `""` |
+| `ingress.tls.enabled` | Enable TLS for ingress | `true` |
+| `replicaCount` | Number of replicas | `1` |
+
+For a complete list of values, see the [values.yaml](./values.yaml) file.
+
+## Upgrading
+
+To upgrade the release:
+
+```bash
+helm upgrade stac-auth-proxy oci://ghcr.io/developmentseed/stac-auth-proxy/charts/stac-auth-proxy -f values.yaml
+```
+
+## Uninstalling
+
+To uninstall/delete the deployment:
+
+```bash
+helm uninstall stac-auth-proxy
+```
+
+## Development
+
+To test the chart locally:
+
+```bash
+helm install stac-auth-proxy ./helm --dry-run --debug
+```
+
+## Support
+
+For support, please open an issue in the [STAC Auth Proxy repository](https://github.com/developmentseed/stac-auth-proxy/issues). 
\ No newline at end of file
diff --git a/helm/templates/NOTES.txt b/helm/templates/NOTES.txt
new file mode 100644
index 00000000..029c0c97
--- /dev/null
+++ b/helm/templates/NOTES.txt
@@ -0,0 +1,71 @@
+Thank you for installing {{ .Chart.Name }}.
+
+Your STAC Auth Proxy has been deployed with the following configuration:
+
+1. Application Access:
+{{- if .Values.ingress.enabled }}
+{{- if .Values.ingress.host }}
+   Your proxy is available at:
+   {{- if .Values.ingress.tls.enabled }}
+   https://{{ .Values.ingress.host }}
+   {{- else }}
+   http://{{ .Values.ingress.host }}
+   {{- end }}
+{{- end }}
+{{- else if contains "NodePort" .Values.service.type }}
+   Get the application URL by running these commands:
+   export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "stac-auth-proxy.fullname" . }})
+   export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
+   echo http://$NODE_IP:$NODE_PORT
+{{- else if contains "LoadBalancer" .Values.service.type }}
+   Get the application URL by running these commands:
+   NOTE: It may take a few minutes for the LoadBalancer IP to be available.
+   You can watch the status by running: 
+   kubectl get svc --namespace {{ .Release.Namespace }} {{ include "stac-auth-proxy.fullname" . }} -w
+   
+   Once ready, get the external IP/hostname with:
+   export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "stac-auth-proxy.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
+   echo http://$SERVICE_IP:{{ .Values.service.port }}
+{{- else }}
+   The service is accessible within the cluster at:
+   {{ include "stac-auth-proxy.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }}
+{{- end }}
+
+2. Configuration Details:
+   - Upstream STAC API: {{ .Values.env.UPSTREAM_URL }}
+   - OIDC Discovery URL: {{ .Values.env.OIDC_DISCOVERY_URL }}
+   - Health Check Endpoint: {{ .Values.env.HEALTHZ_PREFIX | default "/healthz" }}
+   - Default Public Access: {{ .Values.env.DEFAULT_PUBLIC | default "false" }}
+
+3. Verify the deployment:
+   kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "stac-auth-proxy.name" . }},app.kubernetes.io/instance={{ .Release.Name }}"
+
+4. View the logs:
+   kubectl logs --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "stac-auth-proxy.name" . }},app.kubernetes.io/instance={{ .Release.Name }}"
+
+5. Health check:
+{{- if .Values.ingress.enabled }}
+{{- if .Values.ingress.host }}
+   {{- if .Values.ingress.tls.enabled }}
+   curl https://{{ .Values.ingress.host }}{{ .Values.env.HEALTHZ_PREFIX | default "/healthz" }}
+   {{- else }}
+   curl http://{{ .Values.ingress.host }}{{ .Values.env.HEALTHZ_PREFIX | default "/healthz" }}
+   {{- end }}
+{{- end }}
+{{- else }}
+   kubectl port-forward --namespace {{ .Release.Namespace }} service/{{ include "stac-auth-proxy.fullname" . }} 8000:{{ .Values.service.port }}
+   curl http://localhost:8000{{ .Values.env.HEALTHZ_PREFIX | default "/healthz" }}
+{{- end }}
+
+For more information about STAC Auth Proxy, please visit:
+https://github.com/developmentseed/stac-auth-proxy
+
+{{- if or (not .Values.env.UPSTREAM_URL) (not .Values.env.OIDC_DISCOVERY_URL) }}
+WARNING: Some required configuration values are not set. Please ensure you have configured:
+{{- if not .Values.env.UPSTREAM_URL }}
+  - env.UPSTREAM_URL
+{{- end }}
+{{- if not .Values.env.OIDC_DISCOVERY_URL }}
+  - env.OIDC_DISCOVERY_URL
+{{- end }}
+{{- end }} 
\ No newline at end of file
diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl
new file mode 100644
index 00000000..8bab877e
--- /dev/null
+++ b/helm/templates/_helpers.tpl
@@ -0,0 +1,71 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "stac-auth-proxy.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+*/}}
+{{- define "stac-auth-proxy.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "stac-auth-proxy.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "stac-auth-proxy.labels" -}}
+helm.sh/chart: {{ include "stac-auth-proxy.chart" . }}
+{{ include "stac-auth-proxy.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "stac-auth-proxy.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "stac-auth-proxy.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "stac-auth-proxy.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "stac-auth-proxy.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
+
+{{/*
+Render env var value based on type
+*/}}
+{{- define "stac-auth-proxy.envValue" -}}
+{{- if kindIs "string" . -}}
+  {{- . | quote -}}
+{{- else -}}
+  {{- . | toJson | quote -}}
+{{- end -}}
+{{- end -}}
diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml
new file mode 100644
index 00000000..ce3c3f53
--- /dev/null
+++ b/helm/templates/deployment.yaml
@@ -0,0 +1,49 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "stac-auth-proxy.fullname" . }}
+  labels:
+    {{- include "stac-auth-proxy.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      {{- include "stac-auth-proxy.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      labels:
+        {{- include "stac-auth-proxy.selectorLabels" . | nindent 8 }}
+    spec:
+      serviceAccountName: {{ include "stac-auth-proxy.serviceAccountName" . }}
+      securityContext:
+        {{- toYaml .Values.securityContext | nindent 8 }}
+      containers:
+        - name: {{ .Chart.Name }}
+          securityContext:
+            {{- toYaml .Values.containerSecurityContext | nindent 12 }}
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          ports:
+            - name: http
+              containerPort: 8000
+              protocol: TCP
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+          env:
+            {{- range $key, $value := .Values.env }}
+            - name: {{ $key }}
+              value: {{ include "stac-auth-proxy.envValue" $value }}
+            {{- end }}
+
+      {{- with .Values.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml
new file mode 100644
index 00000000..6c47509c
--- /dev/null
+++ b/helm/templates/ingress.yaml
@@ -0,0 +1,40 @@
+{{- if .Values.ingress.enabled -}}
+{{- $fullName := include "stac-auth-proxy.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 }}
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: {{ $fullName }}
+  labels:
+    {{- include "stac-auth-proxy.labels" . | nindent 4 }}
+  {{- with .Values.ingress.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+spec:
+  ingressClassName: {{ .Values.ingress.className }}
+  {{- if and .Values.ingress.tls.enabled .Values.ingress.host }}
+  tls:
+    - hosts:
+        - {{ .Values.ingress.host }}
+      secretName: {{ .Values.ingress.tls.secretName | default (printf "%s-tls" .Values.ingress.host) }}
+  {{- end }}
+  rules:
+    {{- if .Values.ingress.host }}
+    - host: {{ .Values.ingress.host }}
+      http:
+        paths:
+          - path: /
+            pathType: Prefix
+            backend:
+              service:
+                name: {{ $fullName }}
+                port:
+                  number: {{ $svcPort }}
+    {{- end }}
+{{- end }} 
\ No newline at end of file
diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml
new file mode 100644
index 00000000..7d00ce9d
--- /dev/null
+++ b/helm/templates/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "stac-auth-proxy.fullname" . }}
+  labels:
+    {{- include "stac-auth-proxy.labels" . | nindent 4 }}
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: {{ .Values.service.port }}
+      targetPort: http
+      protocol: TCP
+      name: http
+  selector:
+    {{- include "stac-auth-proxy.selectorLabels" . | nindent 4 }} 
\ No newline at end of file
diff --git a/helm/templates/serviceaccount.yaml b/helm/templates/serviceaccount.yaml
new file mode 100644
index 00000000..c76b4541
--- /dev/null
+++ b/helm/templates/serviceaccount.yaml
@@ -0,0 +1,16 @@
+{{- if .Values.serviceAccount.create -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ include "stac-auth-proxy.serviceAccountName" . }}
+  labels:
+    {{- include "stac-auth-proxy.labels" . | nindent 4 }}
+  {{- with .Values.serviceAccount.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+{{- with .Values.serviceAccount.imagePullSecrets }}
+imagePullSecrets:
+  {{- toYaml . | nindent 2 }}
+{{- end }}
+{{- end }} 
\ No newline at end of file
diff --git a/helm/values.schema.yaml b/helm/values.schema.yaml
new file mode 100644
index 00000000..308bbdcb
--- /dev/null
+++ b/helm/values.schema.yaml
@@ -0,0 +1,258 @@
+"$schema": "https://json-schema.org/draft-07/schema#"
+type: object
+properties:
+  replicaCount:
+    type: integer
+    minimum: 1
+    description: "Number of replicas for the deployment"
+
+  image:
+    type: object
+    properties:
+      repository:
+        type: string
+        description: "Docker image repository"
+        default: "ghcr.io/developmentseed/stac-auth-proxy"
+      pullPolicy:
+        type: string
+        enum: ["IfNotPresent", "Always", "Never"]
+        description: "Kubernetes image pull policy"
+      tag:
+        type: string
+        description: "Docker image tag"
+        default: "latest"
+
+  env:
+    type: object
+    properties:
+      # Core
+      UPSTREAM_URL:
+        type: string
+        pattern: "^https?://.+"
+        description: "URL of the STAC API to proxy"
+      WAIT_FOR_UPSTREAM:
+        type: ["boolean", "string"]
+        description: "Wait for upstream API to become available before starting proxy"
+        default: "true"
+      CHECK_CONFORMANCE:
+        type: ["boolean", "string"]
+        description: "Ensure upstream API conforms to required conformance classes before starting proxy"
+        default: "true"
+      ENABLE_COMPRESSION:
+        type: ["boolean", "string"]
+        description: "Enable response compression"
+        default: "true"
+      HEALTHZ_PREFIX:
+        type: string
+        description: "Path prefix for health check endpoints"
+        default: "/healthz"
+      OVERRIDE_HOST:
+        type: ["boolean", "string"]
+        description: "Override the host header for the upstream API"
+        default: "true"
+      ROOT_PATH:
+        type: string
+        description: "Path prefix for the proxy API"
+        default: ""
+      # Authentication
+      OIDC_DISCOVERY_URL:
+        type: string
+        pattern: "^https?://.+"
+        description: "OpenID Connect discovery document URL"
+      OIDC_DISCOVERY_INTERNAL_URL:
+        type: string
+        pattern: "^https?://.+"
+        description: "Internal network OpenID Connect discovery document URL"
+      DEFAULT_PUBLIC:
+        type: ["boolean", "string"]
+        description: "Default access policy for endpoints"
+        default: "false"
+      PRIVATE_ENDPOINTS:
+        type: ["object", "string"]
+        description: "Endpoints explicitly marked as requiring authentication and possibly scopes"
+      PUBLIC_ENDPOINTS:
+        type: ["object", "string"]
+        description: "Endpoints explicitly marked as not requiring authentication"
+      ENABLE_AUTHENTICATION_EXTENSION:
+        type: ["boolean", "string"]
+        description: "Enable authentication extension in STAC API responses"
+        default: "true"
+      # OpenAPI
+      OPENAPI_SPEC_ENDPOINT:
+        type: ["string", "null"]
+        description: "Path of OpenAPI specification"
+        default: "/api"
+      OPENAPI_AUTH_SCHEME_NAME:
+        type: string
+        description: "Name of the auth scheme to use in the OpenAPI spec"
+        default: "oidcAuth"
+      OPENAPI_AUTH_SCHEME_OVERRIDE:
+        type: ["object", "string", "null"]
+        description: "Override for the auth scheme in the OpenAPI spec"
+      # Filtering
+      ITEMS_FILTER_CLS:
+        type: ["string", "null"]
+        description: "CQL2 expression generator for item-level filtering"
+      ITEMS_FILTER_ARGS:
+        type: ["array", "string"]
+        description: "Positional arguments for CQL2 expression generator"
+      ITEMS_FILTER_KWARGS:
+        type: ["object", "string"]
+        description: "Keyword arguments for CQL2 expression generator"
+    required:
+      - UPSTREAM_URL
+      - OIDC_DISCOVERY_URL
+
+  service:
+    type: object
+    required: ["type", "port"]
+    properties:
+      type:
+        type: string
+        enum: ["ClusterIP", "NodePort", "LoadBalancer"]
+        description: "Kubernetes service type"
+      port:
+        type: integer
+        minimum: 1
+        maximum: 65535
+        description: "Service port number"
+
+  ingress:
+    type: object
+    properties:
+      enabled:
+        type: boolean
+        description: "Enable ingress resource"
+      className:
+        type: string
+        description: "Ingress class name (e.g., nginx)"
+      annotations:
+        type: object
+        additionalProperties:
+          type: string
+        description: "Annotations for the ingress resource"
+      host:
+        type: string
+        description: "Hostname for the ingress"
+      tls:
+        type: object
+        properties:
+          enabled:
+            type: boolean
+            description: "Enable TLS configuration"
+          secretName:
+            type: string
+            description: "Name of the TLS secret (optional, will be auto-generated if empty)"
+        required: ["enabled"]
+
+  resources:
+    type: object
+    properties:
+      limits:
+        type: object
+        properties:
+          cpu:
+            type: string
+            pattern: "^[0-9]+m?$|^[0-9]+\\.[0-9]+$"
+            description: "CPU limit (e.g., 500m, 1.5)"
+          memory:
+            type: string
+            pattern: "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|[kMGTPE]i?)?$"
+            description: "Memory limit (e.g., 512Mi, 1Gi)"
+      requests:
+        type: object
+        properties:
+          cpu:
+            type: string
+            pattern: "^[0-9]+m?$|^[0-9]+\\.[0-9]+$"
+            description: "CPU request (e.g., 200m, 0.5)"
+          memory:
+            type: string
+            pattern: "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|[kMGTPE]i?)?$"
+            description: "Memory request (e.g., 256Mi, 1Gi)"
+
+  securityContext:
+    type: object
+    properties:
+      runAsNonRoot:
+        type: boolean
+        description: "Requires the container to run without root privileges"
+      runAsUser:
+        type: integer
+        description: "The UID to run the entrypoint of the container process"
+      runAsGroup:
+        type: integer
+        description: "The GID to run the entrypoint of the container process"
+    description: "Pod-level security context"
+
+  containerSecurityContext:
+    type: object
+    properties:
+      allowPrivilegeEscalation:
+        type: boolean
+        description: "Controls whether a process can gain more privileges than its parent process"
+      capabilities:
+        type: object
+        properties:
+          drop:
+            type: array
+            items:
+              type: string
+            description: "List of capabilities to drop"
+    description: "Container-level security context"
+
+  nodeSelector:
+    type: object
+    additionalProperties:
+      type: string
+    description: "Node labels for pod assignment"
+
+  tolerations:
+    type: array
+    items:
+      type: object
+      properties:
+        key:
+          type: string
+        operator:
+          type: string
+          enum: ["Exists", "Equal"]
+        value:
+          type: string
+        effect:
+          type: string
+          enum: ["NoSchedule", "PreferNoSchedule", "NoExecute"]
+    description: "Pod tolerations"
+
+  affinity:
+    type: object
+    additionalProperties: true
+    description: "Pod affinity rules"
+
+  serviceAccount:
+    type: object
+    properties:
+      create:
+        type: boolean
+        description: "Specifies whether a service account should be created"
+      annotations:
+        type: object
+        additionalProperties:
+          type: string
+        description: "Annotations to add to the service account"
+      name:
+        type: string
+        description: "The name of the service account to use. If not set and create is true, a name is generated"
+      imagePullSecrets:
+        type: array
+        description: "Image pull secrets to add to the service account"
+        items:
+          type: object
+          required: ["name"]
+          properties:
+            name:
+              type: string
+              description: "Name of the image pull secret"
+
+required:
+  - service 
\ No newline at end of file
diff --git a/helm/values.yaml b/helm/values.yaml
new file mode 100644
index 00000000..76e00d4e
--- /dev/null
+++ b/helm/values.yaml
@@ -0,0 +1,88 @@
+# Default values for stac-auth-proxy
+
+replicaCount: 1
+
+image:
+  repository: ghcr.io/developmentseed/stac-auth-proxy
+  pullPolicy: IfNotPresent
+  tag: "latest"
+
+service:
+  type: ClusterIP
+  port: 8000
+
+ingress:
+  enabled: true
+  className: "nginx"
+  annotations:
+    cert-manager.io/cluster-issuer: "letsencrypt-prod"
+  host: "stac-proxy.example.com"  # This should be overridden in production
+  tls:
+    enabled: true
+    secretName: ""  # If empty, will be auto-generated as "{host}-tls"
+
+resources:
+  limits:
+    cpu: 500m
+    memory: 512Mi
+  requests:
+    cpu: 200m
+    memory: 256Mi
+
+# Pod-level security context
+securityContext:
+  runAsNonRoot: true
+  runAsUser: 1000
+  runAsGroup: 1000
+
+# Container-level security context
+containerSecurityContext:
+  allowPrivilegeEscalation: false
+  capabilities:
+    drop:
+      - ALL
+
+nodeSelector: {}
+tolerations: []
+affinity: {}
+
+# Environment variables for the application
+env:
+  # Required configuration
+  UPSTREAM_URL: ""  # STAC API URL
+  OIDC_DISCOVERY_URL: ""  # OpenID Connect discovery URL
+
+  # Optional configuration
+  WAIT_FOR_UPSTREAM: true
+  HEALTHZ_PREFIX: "/healthz"
+  OIDC_DISCOVERY_INTERNAL_URL: ""
+  DEFAULT_PUBLIC: false
+  PRIVATE_ENDPOINTS: |
+    {
+      "^/collections$": ["POST"],
+      "^/collections/([^/]+)$": ["PUT", "PATCH", "DELETE"],
+      "^/collections/([^/]+)/items$": ["POST"],
+      "^/collections/([^/]+)/items/([^/]+)$": ["PUT", "PATCH", "DELETE"],
+      "^/collections/([^/]+)/bulk_items$": ["POST"]
+    }
+  PUBLIC_ENDPOINTS: |
+    {
+      "^/api.html$": ["GET"],
+      "^/api$": ["GET"],
+      "^/docs/oauth2-redirect": ["GET"],
+      "^/healthz": ["GET"]
+    }
+
+
+
+serviceAccount:
+  # Specifies whether a service account should be created
+  create: true
+  # Annotations to add to the service account
+  annotations: {}
+  # The name of the service account to use.
+  # If not set and create is true, a name is generated using the fullname template
+  name: ""
+  # Image pull secrets to add to the service account
+  imagePullSecrets: []
+  # - name: my-registry-secret 
\ No newline at end of file


Reviews:
user: login: alukach
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T02:37:30Z
user: login: alukach
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T02:38:15Z
user: login: batpad
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T05:43:04Z
user: login: batpad
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T05:43:36Z
user: login: batpad
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T06:59:52Z
user: login: alukach
body: Looks good, should we merge?
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-14T18:35:29Z

Review comments:
diff_hunk: @@ -0,0 +1,92 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "stac-auth-proxy.fullname" . }}
+  labels:
+    {{- include "stac-auth-proxy.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      {{- include "stac-auth-proxy.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      labels:
+        {{- include "stac-auth-proxy.selectorLabels" . | nindent 8 }}
+    spec:
+      serviceAccountName: {{ include "stac-auth-proxy.serviceAccountName" . }}
+      securityContext:
+        {{- toYaml .Values.securityContext | nindent 8 }}
+      containers:
+        - name: {{ .Chart.Name }}
+          securityContext:
+            {{- toYaml .Values.containerSecurityContext | nindent 12 }}
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          ports:
+            - name: http
+              containerPort: 8000
+              protocol: TCP
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+          env:
+            - name: UPSTREAM_URL
+              value: {{ required "A valid upstream URL is required" .Values.config.upstreamUrl | quote }}
+            - name: OIDC_DISCOVERY_URL
+              value: {{ required "An OIDC discovery URL is required" .Values.config.oidc.discoveryUrl | quote }}
+            
+            # Optional OIDC internal URL
+            {{- if .Values.config.oidc.discoveryInternalUrl }}
+            - name: OIDC_DISCOVERY_INTERNAL_URL
+              value: {{ .Values.config.oidc.discoveryInternalUrl | quote }}
+            {{- end }}
+            
+            # Core configuration
+            - name: WAIT_FOR_UPSTREAM
+              value: {{ .Values.config.waitForUpstream | quote }}
+            - name: HEALTHZ_PREFIX
+              value: {{ .Values.config.healthzPrefix | quote }}
+            
+            # Access control configuration
+            - name: DEFAULT_PUBLIC
+              value: {{ .Values.config.defaultPublic | quote }}
+            {{- if .Values.config.privateEndpoints }}
+            - name: PRIVATE_ENDPOINTS
+              value: {{ .Values.config.privateEndpoints | toJson | quote }}
+            {{- end }}
+            {{- if .Values.config.publicEndpoints }}
+            - name: PUBLIC_ENDPOINTS
+              value: {{ .Values.config.publicEndpoints | toJson | quote }}
+            {{- end }}
+            
+            # OpenAPI configuration
+            {{- if .Values.config.openapiSpecEndpoint }}
+            - name: OPENAPI_SPEC_ENDPOINT
+              value: {{ .Values.config.openapiSpecEndpoint | quote }}
+            {{- end }}
+            
+            # Filtering configuration
+            {{- if .Values.config.itemsFilter.class }}
+            - name: ITEMS_FILTER_CLS
+              value: {{ .Values.config.itemsFilter.class | quote }}
+            - name: ITEMS_FILTER_ARGS
+              value: {{ .Values.config.itemsFilter.args | toJson | quote }}
+            - name: ITEMS_FILTER_KWARGS
+              value: {{ .Values.config.itemsFilter.kwargs | toJson | quote }}
+            {{- end }}
path: helm/templates/deployment.yaml
user: login: alukach
body: Sorry to be dense, but what is the value of explicitly describing all of these env vars?  I worry that this will make maintenance a challenge.
created_at: 2025-04-14T02:37:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,71 @@
+Thank you for installing {{ .Chart.Name }}.
path: helm/templates/NOTES.txt
user: login: alukach
body: So is this rendered in your terminal at time of deployment?
created_at: 2025-04-14T02:38:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,92 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "stac-auth-proxy.fullname" . }}
+  labels:
+    {{- include "stac-auth-proxy.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      {{- include "stac-auth-proxy.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      labels:
+        {{- include "stac-auth-proxy.selectorLabels" . | nindent 8 }}
+    spec:
+      serviceAccountName: {{ include "stac-auth-proxy.serviceAccountName" . }}
+      securityContext:
+        {{- toYaml .Values.securityContext | nindent 8 }}
+      containers:
+        - name: {{ .Chart.Name }}
+          securityContext:
+            {{- toYaml .Values.containerSecurityContext | nindent 12 }}
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          ports:
+            - name: http
+              containerPort: 8000
+              protocol: TCP
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+          env:
+            - name: UPSTREAM_URL
+              value: {{ required "A valid upstream URL is required" .Values.config.upstreamUrl | quote }}
+            - name: OIDC_DISCOVERY_URL
+              value: {{ required "An OIDC discovery URL is required" .Values.config.oidc.discoveryUrl | quote }}
+            
+            # Optional OIDC internal URL
+            {{- if .Values.config.oidc.discoveryInternalUrl }}
+            - name: OIDC_DISCOVERY_INTERNAL_URL
+              value: {{ .Values.config.oidc.discoveryInternalUrl | quote }}
+            {{- end }}
+            
+            # Core configuration
+            - name: WAIT_FOR_UPSTREAM
+              value: {{ .Values.config.waitForUpstream | quote }}
+            - name: HEALTHZ_PREFIX
+              value: {{ .Values.config.healthzPrefix | quote }}
+            
+            # Access control configuration
+            - name: DEFAULT_PUBLIC
+              value: {{ .Values.config.defaultPublic | quote }}
+            {{- if .Values.config.privateEndpoints }}
+            - name: PRIVATE_ENDPOINTS
+              value: {{ .Values.config.privateEndpoints | toJson | quote }}
+            {{- end }}
+            {{- if .Values.config.publicEndpoints }}
+            - name: PUBLIC_ENDPOINTS
+              value: {{ .Values.config.publicEndpoints | toJson | quote }}
+            {{- end }}
+            
+            # OpenAPI configuration
+            {{- if .Values.config.openapiSpecEndpoint }}
+            - name: OPENAPI_SPEC_ENDPOINT
+              value: {{ .Values.config.openapiSpecEndpoint | quote }}
+            {{- end }}
+            
+            # Filtering configuration
+            {{- if .Values.config.itemsFilter.class }}
+            - name: ITEMS_FILTER_CLS
+              value: {{ .Values.config.itemsFilter.class | quote }}
+            - name: ITEMS_FILTER_ARGS
+              value: {{ .Values.config.itemsFilter.args | toJson | quote }}
+            - name: ITEMS_FILTER_KWARGS
+              value: {{ .Values.config.itemsFilter.kwargs | toJson | quote }}
+            {{- end }}
path: helm/templates/deployment.yaml
user: login: batpad
body: @alukach not dense at all :)

I will change the nested `config` to be a simple flat `env` dictionary that replicates the env names exactly, to reduce the maintanence burden.

I do think I want to be explicit here though and include all the possible `env` values to deal with different types, as well as be explicit in the `values.schema.yaml` to have proper validation.
created_at: 2025-04-14T05:43:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,71 @@
+Thank you for installing {{ .Chart.Name }}.
path: helm/templates/NOTES.txt
user: login: batpad
body: Yes, this is what renders when you do `helm install` / `helm upgrade`
created_at: 2025-04-14T05:43:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,92 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "stac-auth-proxy.fullname" . }}
+  labels:
+    {{- include "stac-auth-proxy.labels" . | nindent 4 }}
+spec:
+  replicas: {{ .Values.replicaCount }}
+  selector:
+    matchLabels:
+      {{- include "stac-auth-proxy.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+      labels:
+        {{- include "stac-auth-proxy.selectorLabels" . | nindent 8 }}
+    spec:
+      serviceAccountName: {{ include "stac-auth-proxy.serviceAccountName" . }}
+      securityContext:
+        {{- toYaml .Values.securityContext | nindent 8 }}
+      containers:
+        - name: {{ .Chart.Name }}
+          securityContext:
+            {{- toYaml .Values.containerSecurityContext | nindent 12 }}
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          ports:
+            - name: http
+              containerPort: 8000
+              protocol: TCP
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+          env:
+            - name: UPSTREAM_URL
+              value: {{ required "A valid upstream URL is required" .Values.config.upstreamUrl | quote }}
+            - name: OIDC_DISCOVERY_URL
+              value: {{ required "An OIDC discovery URL is required" .Values.config.oidc.discoveryUrl | quote }}
+            
+            # Optional OIDC internal URL
+            {{- if .Values.config.oidc.discoveryInternalUrl }}
+            - name: OIDC_DISCOVERY_INTERNAL_URL
+              value: {{ .Values.config.oidc.discoveryInternalUrl | quote }}
+            {{- end }}
+            
+            # Core configuration
+            - name: WAIT_FOR_UPSTREAM
+              value: {{ .Values.config.waitForUpstream | quote }}
+            - name: HEALTHZ_PREFIX
+              value: {{ .Values.config.healthzPrefix | quote }}
+            
+            # Access control configuration
+            - name: DEFAULT_PUBLIC
+              value: {{ .Values.config.defaultPublic | quote }}
+            {{- if .Values.config.privateEndpoints }}
+            - name: PRIVATE_ENDPOINTS
+              value: {{ .Values.config.privateEndpoints | toJson | quote }}
+            {{- end }}
+            {{- if .Values.config.publicEndpoints }}
+            - name: PUBLIC_ENDPOINTS
+              value: {{ .Values.config.publicEndpoints | toJson | quote }}
+            {{- end }}
+            
+            # OpenAPI configuration
+            {{- if .Values.config.openapiSpecEndpoint }}
+            - name: OPENAPI_SPEC_ENDPOINT
+              value: {{ .Values.config.openapiSpecEndpoint | quote }}
+            {{- end }}
+            
+            # Filtering configuration
+            {{- if .Values.config.itemsFilter.class }}
+            - name: ITEMS_FILTER_CLS
+              value: {{ .Values.config.itemsFilter.class | quote }}
+            - name: ITEMS_FILTER_ARGS
+              value: {{ .Values.config.itemsFilter.args | toJson | quote }}
+            - name: ITEMS_FILTER_KWARGS
+              value: {{ .Values.config.itemsFilter.kwargs | toJson | quote }}
+            {{- end }}
path: helm/templates/deployment.yaml
user: login: batpad
body: Have switched to just using an `env` in the `values`, being able to pass in the env values exactly as they are, and modifying documentation to point to the root README for documentation on the env vars.
created_at: 2025-04-14T06:59:52Z
author_association: MEMBER

PR comments:
user: login: emmanuelmathot
created_at: 2025-03-27T15:46:33Z
body: Dont forget serviceaccountname placeholder int deployment as we need it to access secrets in general and on azure specifically
author_association: NONE
user: login: emmanuelmathot
created_at: 2025-03-27T15:47:19Z
body: @batpad I will give a deeper look on the whole chart tomorrow.
author_association: NONE
user: login: batpad
created_at: 2025-03-28T07:01:21Z
body: > Dont forget serviceaccountname placeholder int deployment as we need it to access secrets in general and on azure specifically

@emmanuelmathot what sort of secrets would stac-auth-proxy need access to? Afaik, it should just need the `openid-configuration` URL, and so far I don't think any of the config parameters it needs need to be secret. I totally could be missing something.
author_association: MEMBER
user: login: emmanuelmathot
created_at: 2025-03-30T18:04:20Z
body: When utilizing RBAC resources, it's important to have the option to set the service account. For example, this is useful for pulling images from a secure hub when the keys are delegated to a specific service account. [Best practices](https://community.appsmith.com/content/guide/helm-best-practices-smoother-kubernetes-management) recommend always using a service account for security reasons. This approach does not necessarily complicate the chart.
author_association: NONE
user: login: batpad
created_at: 2025-03-31T07:10:25Z
body: @emmanuelmathot made a push adding a service account definition and some configurability in `values` - here I'm really not fully sure how this works, so please let me know if this is not what you had in mind. Thank you!
author_association: MEMBER
user: login: emmanuelmathot
created_at: 2025-03-31T07:29:39Z
body: that's perfect. Thank you @batpad 
author_association: NONE
user: login: batpad
created_at: 2025-04-08T11:46:20Z
body: I've used the helm chart here to deploy a test instance to our Labs cluster: https://stac-auth-proxy-test.k8s.labs.ds.io/

I used the following `values` over-ride:

```
ingress:
  enabled: true
  host: stac-auth-proxy-test.k8s.labs.ds.io
  annotations:
    cert-manager.io/cluster-issuer: "onyx-onyx-stack-letsencrypt"
  tls:
    enabled: true

config:
  # upstreamUrl: "https://montandon-eoapi-stage.ifrc.org/stac/"
  upstreamUrl: "https://eoapi.ifrc-risk.k8s.labs.ds.io/stac"
  oidc:
    discoveryUrl: "https://goadmin.ifrc.org/o/.well-known/openid-configuration"
```

The deploy seems to be working and the startup logs look good. @alukach I'll need your help to properly test this (as well as some questions around working with the JWT tokens we intend to use).

@emmanuelmathot - this seems to successfully deploy - is there anything else you feel we should do before we can merge here? Else I'll feel okay merging once I've confirmed with @alukach that things work as expected.
author_association: MEMBER
user: login: emmanuelmathot
created_at: 2025-04-10T08:47:35Z
body: LGTM
author_association: NONE
user: login: batpad
created_at: 2025-04-14T07:15:56Z
body: @alukach I have changed the structure of the `values` being passed in as `env` vars based on our discussion and your suggestion.

I do much prefer this, so thank you. So now,

 - `env` is an object in `values` where any key you add to `env` will be passed down as the same env var name to the container. The documentation then points to the README at the root for documentation on the env vars.
 - the README, NOTES.txt, etc. should all be updated accordingly.

I've still left documentation for the possible env vars in the `values.schema.yaml` so we can specify types of the config values we know about. Am not sure though, maybe it's best to remove those and just leave in the required values. Will discuss quickly with @emmanuelmathot .

@emmanuelmathot let know if this change to just using `env` var literals in values instead of inventing a nested `config` structure makes sense - this commit mostly: https://github.com/developmentseed/stac-auth-proxy/pull/44/commits/339132a8ab195d9c5e9cc5a8bf7d94251de3e578

Am really not sure why I went with the nested `config` structure on the first pass, some misguided sense of something that I did not fully think through - thanks again @alukach, I do much prefer this way.
author_association: MEMBER
user: login: batpad
created_at: 2025-04-17T06:57:31Z
body: Hmm .. tried rebasing and then testing running this on the labs cluster, and am getting errors in the logs that I'm not sure I can figure out @alukach -

```
kubectl logs stac-auth-proxy-test-759f64798b-splg9 -n batpad
INFO:     Will watch for changes in these directories: ['/app']
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [1] using StatReload
Process SpawnProcess-1:
Traceback (most recent call last):
  File "/usr/local/lib/python3.13/multiprocessing/process.py", line 313, in _bootstrap
    self.run()
    ~~~~~~~~^^
  File "/usr/local/lib/python3.13/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/uvicorn/_subprocess.py", line 80, in subprocess_started
    target(sockets=sockets)
    ~~~~~~^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/site-packages/uvicorn/server.py", line 66, in run
    return asyncio.run(self.serve(sockets=sockets))
           ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.13/asyncio/runners.py", line 195, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "/usr/local/lib/python3.13/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/usr/local/lib/python3.13/asyncio/base_events.py", line 725, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "/usr/local/lib/python3.13/site-packages/uvicorn/server.py", line 70, in serve
    await self._serve(sockets)
  File "/usr/local/lib/python3.13/site-packages/uvicorn/server.py", line 77, in _serve
    config.load()
    ~~~~~~~~~~~^^
  File "/usr/local/lib/python3.13/site-packages/uvicorn/config.py", line 441, in load
    self.loaded_app = self.loaded_app()
                      ~~~~~~~~~~~~~~~^^
  File "/app/src/stac_auth_proxy/app.py", line 32, in create_app
    settings = settings or Settings()
                           ~~~~~~~~^^
  File "/usr/local/lib/python3.13/site-packages/pydantic_settings/main.py", line 177, in __init__
    super().__init__(
    ~~~~~~~~~~~~~~~~^
        **self._settings_build_values(
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<26 lines>...
        )
        ^
    )
    ^
  File "/usr/local/lib/python3.13/site-packages/pydantic/main.py", line 214, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
pydantic_core._pydantic_core.ValidationError: 9 validation errors for Settings
upstream_url
  Input should be a valid URL, relative URL without a base [type=url_parsing, input_value='"https://montandon-eoapi-stage.ifrc.org/stac/"', input_type=str]
    For further information visit https://errors.pydantic.dev/2.10/v/url_parsing
oidc_discovery_url
  Input should be a valid URL, relative URL without a base [type=url_parsing, input_value='"https://goadmin.ifrc.or...n/openid-configuration"', input_type=str]
    For further information visit https://errors.pydantic.dev/2.10/v/url_parsing
oidc_discovery_internal_url
  Input should be a valid URL, relative URL without a base [type=url_parsing, input_value='""', input_type=str]
    For further information visit https://errors.pydantic.dev/2.10/v/url_parsing
healthz_prefix
  String should match pattern '^/.*$' [type=string_pattern_mismatch, input_value='"/healthz"', input_type=str]
    For further information visit https://errors.pydantic.dev/2.10/v/string_pattern_mismatch
openapi_spec_endpoint
  String should match pattern '^/.*$' [type=string_pattern_mismatch, input_value='null', input_type=str]
    For further information visit https://errors.pydantic.dev/2.10/v/string_pattern_mismatch
public_endpoints
  Input should be a valid dictionary [type=dict_type, input_value='{\n  "^/api.html$": ["GE.../healthz": ["GET"]\n}\n', input_type=str]
    For further information visit https://errors.pydantic.dev/2.10/v/dict_type
private_endpoints
  Input should be a valid dictionary [type=dict_type, input_value='{\n  "^/collections$": [..._items$": ["POST"]\n}\n', input_type=str]
    For further information visit https://errors.pydantic.dev/2.10/v/dict_type
items_filter.args
  'str' instances are not allowed as a Sequence value [type=sequence_str, input_value='[]', input_type=str]
items_filter.kwargs
  Input should be a valid dictionary [type=dict_type, input_value='{}', input_type=str]
    For further information visit https://errors.pydantic.dev/2.10/v/dict_type
```

I think this may be me not adding new required variables, but some of these are a bit confusing / unexpected to me.

I've verified that OIDC_DISCOVERY_URL and UPSTREAM_URL env vars are correctly set in the container:

```
OIDC_DISCOVERY_URL="https://goadmin.ifrc.org/o/.well-known/openid-configuration"
UPSTREAM_URL="https://montandon-eoapi-stage.ifrc.org/stac/"
```

@alukach I think this will be a lot quicker to debug if we can pair 👀 on it quickly. Let me know if you have a bit of time once you're up. Thanks! We are definitely __almost there__.
author_association: MEMBER
user: login: sunu
created_at: 2025-04-17T07:02:42Z
body: @batpad from the error message, looks like some of those urls have an extra pair of quotes around them?
author_association: MEMBER
user: login: batpad
created_at: 2025-04-17T07:04:00Z
body: > @batpad from the error message, looks like some of those urls have an extra pair of quotes around them?

Where do you get your eyes from. Nice catch. Can try a couple things. But that's strange.
author_association: MEMBER
user: login: batpad
created_at: 2025-04-17T07:18:54Z
body: @sunu that was it :-) - so that error is fixed.
author_association: MEMBER
user: login: emmanuelmathot
created_at: 2025-04-18T07:41:58Z
body: Maybe as a follow-up to ensure that helm chats is tested properly, a [CI/CD test deploy](https://github.com/developmentseed/titiler/blob/main/.github/workflows/check_charts.yaml) like for titiler is useful
author_association: NONE
user: login: batpad
created_at: 2025-04-18T07:43:55Z
body: @emmanuelmathot haa I was just looking into adding tests, but will do that as a separate PR. Thanks for the pointer! I'll open a separate ticket about that and would be good to get your thoughts on the multiple possible approaches to testing that I've been looking into a bit.
author_association: MEMBER
user: login: emmanuelmathot
created_at: 2025-04-18T07:45:22Z
body: Sure. eoapi has similar but slightly more complex tests: https://github.com/developmentseed/eoapi-k8s/blob/main/.github/workflows/helm-tests.yml
author_association: NONE