diff --git a/applications/argocd/production/applications/montandon-eoapi/application.yaml b/applications/argocd/production/applications/montandon-eoapi/application.yaml index 60534a83..1a3fbc34 100644 --- a/applications/argocd/production/applications/montandon-eoapi/application.yaml +++ b/applications/argocd/production/applications/montandon-eoapi/application.yaml @@ -8,25 +8,48 @@ metadata: spec: project: default sources: + - repoURL: https://devseed.com/eoapi-k8s/ chart: eoapi - targetRevision: 0.10.0 + targetRevision: 0.11.2 helm: + valueFiles: + - values/argocd.yaml valuesObject: - ingress: + postgrescluster: + # Using azure databae + enabled: false + vector: enabled: false - # host: "montandon-eoapi.ifrc.org" - # tls: - # enabled: true - # secretName: montandon-eoapi-helm-secret-cert - # annotations: - # # increase the max body size to 100MB - # nginx.ingress.kubernetes.io/proxy-body-size: "100m" - # nginx.ingress.kubernetes.io/proxy-read-timeout: "600" - # nginx.ingress.kubernetes.io/proxy-send-timeout: "600" - # nginx.ingress.kubernetes.io/proxy-connect-timeout: "600" raster: enabled: false + ingress: + # Using stac-auth-proxy + enabled: false + + serviceAccount: + create: true + automount: true + annotations: + azure.workload.identity/client-id : "8bf208ec-d73c-42d1-a4a9-817d2936a883" + labels: + azure.workload.identity/use: "true" + + postgresql: + type: "external-secret" + external: + existingSecret: + # Defined here: internal/montandon-eoapi-spc.yaml + name: pgstac-secrets-montandon-eoapi + keys: + username: "DB_USER" + password: "DB_PASSWORD" + # Optional: if these are provided in the secret + # Note: These values override external.host, external.port and external.database if defined + host: "DB_HOST" + database: "DB_NAME" + port: "DB_PORT" + stac: image: tag: 6.1.2 @@ -59,77 +82,39 @@ spec: mountPath: /mnt/secrets-store readOnly: true extraVolumes: + # Not required for eoAPI, but secrets-store.csi.k8s.io needs at least one pod to mount SecretProviderClass to sync Azure Key Vault with the Kubernetes secret pgstac-secrets-montandon-eoapi - name: azure-keyvault-secrets csi: driver: secrets-store.csi.k8s.io readOnly: true volumeAttributes: secretProviderClass: azure-secret-provider-montandon-eoapi - vector: - enabled: false - serviceAccount: - create: true - automount: true - annotations: - azure.workload.identity/client-id : "8bf208ec-d73c-42d1-a4a9-817d2936a883" - labels: - azure.workload.identity/use: "true" - - # pgstacBootstrap: - # enabled: true - # settings: - # annotations: - # argocd.argoproj.io/hook: Sync - # # labels: - # # azure.workload.identity/use: "true" - # # extraVolumes: - # # - name: azure-keyvault-secrets - # # csi: - # # driver: secrets-store.csi.k8s.io - # # readOnly: true - # # volumeAttributes: - # # secretProviderClass: azure-secret-provider-montandon-eoapi - # queryables: - # # configMap - # - name: "stac-queryables.json" - # configMapRef: - # name: montandon-eoapi-stac-queryables - # key: stac_queryables.json - # indexFields: ["monty:hazard_codes", "monty:country_codes", "roles"] - # deleteMissing: true - postgresql: - type: "external-secret" - external: - existingSecret: - name: pgstac-secrets-montandon-eoapi - keys: - username: "DB_USER" - password: "DB_PASSWORD" - # Optional: if these are provided in the secret - # Note: These values override external.host, external.port and external.database if defined - host: "DB_HOST" - database: "DB_NAME" - port: "DB_PORT" + pgstacBootstrap: + enabled: true + settings: + loadSamples: false + queryables: + - name: "stac_queryables.json" + indexFields: ["monty:hazard_codes","monty:country_codes","roles"] + deleteMissing: true + configMapRef: + name: montandon-eoapi-stac-queryables + key: stac_queryables.json - postgrescluster: - enabled: false - # instances: - # - name: eoapi - # replicas: 1 - # dataVolumeClaimSpec: - # accessModes: - # - "ReadWriteOnce" - # resources: - # requests: - # storage: "600Gi" - # cpu: "1024m" - # memory: "3048Mi" - path: applications/argocd/production/applications/montandon-eoapi/internal/ targetRevision: develop repoURL: https://github.com/IFRCGo/go-deploy.git + helm: + valuesObject: + azure: + clientID: 8bf208ec-d73c-42d1-a4a9-817d2936a883 + secretProviderClass: + enabled: true + keyvaultName: montandon-eoapi-producti + - repoURL: https://github.com/developmentseed/stac-auth-proxy.git - targetRevision: v0.9.2 + targetRevision: v0.11.1 path: helm/ helm: valuesObject: @@ -139,6 +124,8 @@ spec: OIDC_DISCOVERY_URL: "https://goadmin.ifrc.org/o/.well-known/openid-configuration" OVERRIDE_HOST: "0" ROOT_PATH: "/stac" + COLLECTIONS_FILTER_CLS: stac_auth_proxy.montandon_filters:CollectionsFilter + ITEMS_FILTER_CLS: stac_auth_proxy.montandon_filters:ItemsFilter ingress: enabled: "true" host: "montandon-eoapi.ifrc.org" @@ -147,6 +134,15 @@ spec: enabled: "true" secretName: "montandon-eoapi-helm-secret-cert" replicaCount: 1 + extraVolumes: + - name: filters + configMap: + name: stac-auth-proxy-filters + extraVolumeMounts: + - name: filters + mountPath: /app/src/stac_auth_proxy/montandon_filters.py + subPath: montandon_filters.py + readOnly: true destination: server: https://kubernetes.default.svc namespace: montandon-eoapi diff --git a/applications/argocd/production/applications/montandon-eoapi/internal/Chart.yaml b/applications/argocd/production/applications/montandon-eoapi/internal/Chart.yaml new file mode 100644 index 00000000..cb7747f1 --- /dev/null +++ b/applications/argocd/production/applications/montandon-eoapi/internal/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: montandon-eoapi-extra-manifests +description: Montandon eoAPI extra manifests +type: application + +version: 0.1.0 +appVersion: "1.0" diff --git a/applications/argocd/production/applications/montandon-eoapi/internal/files/stac-auth-proxy/montandon_filters.py b/applications/argocd/production/applications/montandon-eoapi/internal/files/stac-auth-proxy/montandon_filters.py new file mode 100644 index 00000000..46000e20 --- /dev/null +++ b/applications/argocd/production/applications/montandon-eoapi/internal/files/stac-auth-proxy/montandon_filters.py @@ -0,0 +1,192 @@ +""" +CQL2 filter factories. + +These classes will be initialized at the startup of the STAC Auth Proxy service and will +be called for each request to collections/items endpoints in order to generate CQL2 +filters based on the JWT permissions. + +docs: https://developmentseed.org/stac-auth-proxy/user-guide/record-level-auth/ +""" + +import asyncio +import dataclasses +import os +import time +import logging +from typing import Any, Literal, Optional, Sequence + +import httpx + +logger = logging.getLogger(__name__) + +if not (UPSTREAM_URL := os.environ.get("UPSTREAM_URL")): + raise ValueError("Failed to retrieve upstream URL") + + +def cql2_in_query( + variable: Literal["collection", "id"], collection_ids: Sequence[str] +) -> str: + """ + Generate CQL2 query to see if value of variable matches any element of sequence of + strings. Due to CQL2 syntax ambiguities around single element arrays with the "in" + operator, we use a direct comparison when there's only one permitted collection. + """ + if not collection_ids: + return "1=0" + + if len(collection_ids) == 1: + return f"{variable} = " + repr(list(collection_ids)[0]) + + return f"{variable} IN ({','.join(repr(c_id) for c_id in collection_ids)})" + + +@dataclasses.dataclass +class CollectionsFilter: + """ + CQL2 filter factory for collections based on JWT permissions. + """ + + collections_claim: str = "collections" # JWT claim with allowed collection IDs + admin_claim: str = "superuser" # JWT claim indicating superuser status + public_collections_filter: str = "(private IS NULL OR private = false)" + + async def __call__(self, context: dict[str, Any]) -> str: + jwt_payload: Optional[dict[str, Any]] = context.get("payload") + + # Anonymous: no data + if not jwt_payload: + logger.debug("Anonymous user, no collections permitted to be viewed") + return "1=0" + + # Superuser: all data + if jwt_payload.get(self.admin_claim) == "true": + logger.debug( + f"Superuser detected for sub {jwt_payload.get('sub')}, " + "no filter applied for collections" + ) + return "1=1" # No filter for superusers + + # Authenticated user: Allowed to access collections mentioned in JWT + permitted_collections = jwt_payload.get(self.collections_claim, []) + return " OR ".join( + [ + self.public_collections_filter, + cql2_in_query("id", permitted_collections), + ] + ) + + +@dataclasses.dataclass +class ItemsFilter: + """ + CQL2 filter factory for items based on JWT permissions. + """ + + collections_claim: str = "collections" # JWT claim with allowed collection IDs + admin_claim: str = "superuser" # JWT claim indicating superuser status + public_collections_filter: str = "(private IS NULL OR private = false)" + + cache_ttl: int = 30 # TTL for caching public collections, in seconds + _client: httpx.AsyncClient = dataclasses.field( + init=False, + repr=False, + default_factory=lambda: httpx.AsyncClient(base_url=UPSTREAM_URL), + ) + _public_collections_cache: Optional[list[str]] = dataclasses.field( + init=False, default=None, repr=False + ) + _cache_expiry: float = dataclasses.field(init=False, default=0, repr=False) + _cache_lock: asyncio.Lock = dataclasses.field( + init=False, repr=False, default_factory=asyncio.Lock + ) + + @property + def _cached_public_collections(self) -> Optional[list[str]]: + """Return cached public collections if still valid, otherwise None.""" + if time.time() < self._cache_expiry: + return self._public_collections_cache + return None + + @_cached_public_collections.setter + def _cached_public_collections(self, value: list[str]) -> None: + """Set the cache with a new value and expiry time.""" + self._public_collections_cache = value + self._cache_expiry = time.time() + self.cache_ttl + + async def _get_public_collections_ids(self) -> list[str]: + """ + Retrieve IDs of public collections from the upstream API. + Uses a lock to prevent concurrent requests from fetching the same data. + """ + # Return cached value if still valid (fast path without lock) + if (cached := self._cached_public_collections) is not None: + logger.debug("Using cached public collections") + return cached + + # Acquire lock to prevent concurrent fetches + async with self._cache_lock: + # Double-check cache after acquiring lock + # Another coroutine might have populated it while we waited + if (cached := self._cached_public_collections) is not None: + logger.debug("Using cached public collections (after lock)") + return cached + + logger.debug("Fetching public collections from upstream API") + + # First request uses params dict + url: Optional[str] = "/collections" + params: Optional[dict[str, Any]] = { + "filter": self.public_collections_filter, + "limit": 100, + } + + ids = [] + while url: + try: + response = await self._client.get(url, params=params) + response.raise_for_status() + data = response.json() + except httpx.HTTPError: + logger.exception(f"Failed to fetch {url!r}.") + raise + ids.extend(collection["id"] for collection in data["collections"]) + + # Subsequent requests use the "next" link URL directly (already has params) + url = next( + (link["href"] for link in data["links"] if link["rel"] == "next"), + None, + ) + params = None # Clear params after first request + + # Update cache + self._cached_public_collections = ids + return ids + + async def __call__(self, context: dict[str, Any]) -> str: + jwt_payload: Optional[dict[str, Any]] = context.get("payload") + + # Anonymous: no data + if not jwt_payload: + logger.debug("Anonymous user, no items permitted to be viewed") + return "1=0" + + # Superuser: all data + if jwt_payload.get(self.admin_claim) == "true": + logger.debug( + f"Superuser detected for sub {jwt_payload.get('sub')}, " + "no filter applied for items" + ) + return "1=1" + + # Everyone: Allowed access to items in public collections + try: + permitted_collections = set(await self._get_public_collections_ids()) + except httpx.HTTPError: + logger.warning("Failed to fetch public collections.") + permitted_collections = set() + + # Authenticated user: Allowed to access items in collections mentioned in JWT + if jwt_payload: + permitted_collections.update(jwt_payload.get(self.collections_claim, [])) + + return cql2_in_query("collection", permitted_collections) diff --git a/applications/argocd/production/applications/montandon-eoapi/internal/files/stac_queryables.json b/applications/argocd/production/applications/montandon-eoapi/internal/files/stac_queryables.json new file mode 100644 index 00000000..625a5d81 --- /dev/null +++ b/applications/argocd/production/applications/montandon-eoapi/internal/files/stac_queryables.json @@ -0,0 +1,121 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/stac/queryables", + "type": "object", + "title": "Queryables for Monty STAC API", + "description": "Queryable names for the Monty STAC API", + "properties": { + "id": { + "description": "Item identifier", + "type": "string" + }, + "collection": { + "description": "Collection identifier", + "type": "string" + }, + "datetime": { + "description": "Datetime", + "type": "string", + "format": "date-time" + }, + "geometry": { + "description": "Geometry", + "type": "object" + }, + "monty:episode_number": { + "description": "The episode number of the event (deprecated)", + "type": "integer" + }, + "monty:country_codes": { + "description": "The country codes of the countries affected by the event, hazard, impact or response", + "type": "array", + "items": { + "type": "string", + "pattern": "^([A-Z]{3})|AB9$" + } + }, + "monty:corr_id": { + "description": "The unique identifier assigned by the Monty system to the reference event", + "type": "string" + }, + "monty:hazard_codes": { + "description": "The hazard codes of the hazards affecting the event", + "type": "array", + "items": { + "type": "string", + "pattern": "^([A-Z]{2}(?:\\d{4}$){0,1})|([a-z]{3}-[a-z]{3}-[a-z]{3}-[a-z]{3})|([A-Z]{2})$" + } + }, + "roles": { + "description": "The roles of the item", + "type": "array", + "items": { + "type": "string", + "enum": ["event", "reference", "source", "hazard", "impact", "response"] + } + }, + "monty:hazard_detail.cluster": { + "description": "The cluster of the hazard (deprecated)", + "type": "string" + }, + "monty:hazard_detail.severity_value": { + "description": "The estimated maximum hazard intensity/magnitude/severity value", + "type": "number" + }, + "monty:hazard_detail.severity_unit": { + "description": "The unit of the severity value", + "type": "string" + }, + "monty:hazard_detail.estimate_type": { + "description": "The type of the estimate", + "type": "string", + "enum": ["primary", "secondary", "modelled"] + }, + "monty:impact_detail.category": { + "description": "The category of impact", + "type": "string", + "enum": [ + "people", "crops", "women", "men", "children_0_4", "children_5_9", + "children_10_14", "children_15_19", "adult_20_24", "adult_25_29", + "adult_30_34", "adult_35_39", "adult_40_44", "adult_45_49", + "adult_50_54", "adult_55_59", "adult_60_64", "elderly", + "wheelchair_users", "roads", "railways", "vulnerable_employment", + "buildings", "reconstruction_costs", "hospitals", "schools", + "local_currency", "global_currency", "local_currency_adj", + "global_currency_adj", "usd_uncertain", "cattle", "aid_general", + "ifrc_contribution", "ifrc_requested", "alertscore", "households" + ] + }, + "monty:impact_detail.type": { + "description": "The estimated value type of the impact", + "type": "string", + "enum": [ + "unspecified", "unaffected", "damaged", "destroyed", "potentially_damaged", + "affected_total", "affected_direct", "affected_indirect", "death", + "missing", "injured", "evacuated", "relocated", "assisted", + "shelter_emergency", "shelter_temporary", "shelter_longterm", "in_need", + "targeted", "disrupted", "cost", "homeless", "displaced_internal", + "displaced_external", "displaced_total", "alertscore", "potentially_affected", + "highest_risk" + ] + }, + "monty:impact_detail.value": { + "description": "The estimated impact value", + "type": "number" + }, + "monty:impact_detail.unit": { + "description": "The units of the impact estimate", + "type": "string" + }, + "monty:impact_detail.estimate_type": { + "description": "The type of the estimate", + "type": "string", + "enum": ["primary", "secondary", "modelled"] + }, + "monty:impact_detail.description": { + "description": "The description of the impact", + "type": "string" + } + }, + "additionalProperties": true +} diff --git a/applications/argocd/production/applications/montandon-eoapi/internal/pgstac-load-samples.yaml b/applications/argocd/production/applications/montandon-eoapi/internal/pgstac-load-samples.yaml deleted file mode 100644 index 461677ca..00000000 --- a/applications/argocd/production/applications/montandon-eoapi/internal/pgstac-load-samples.yaml +++ /dev/null @@ -1,90 +0,0 @@ -# Static job for loading PgSTAC sample data -# This job can be used to load sample data into PgSTAC -# Currently, this is a dummy job to fix dependencies in STAC API start ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: montandon-eoapi-pgstac-load-samples - labels: - app: montandon-eoapi-pgstac-load-samples - component: database -spec: - template: - metadata: - labels: - app: montandon-eoapi-pgstac-load-samples - component: database - spec: - restartPolicy: Never - containers: - - name: dummy-load-samples - image: ghcr.io/stac-utils/pgstac-pypgstac:v0.9.8 # Customize: image version - command: - - "/bin/sh" - - "-c" - args: - - | - # Wait for database readiness - echo "Double-checking database readiness..." - pypgstac pgready - env: - # Database connection settings - - name: POSTGRES_HOST - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_HOST - - name: PGHOST - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_HOST - - name: POSTGRES_PORT - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PORT - - name: PGPORT - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PORT - - name: POSTGRES_USER - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_USER - - name: PGUSER - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_USER - - name: POSTGRES_DB - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_NAME - - name: PGDATABASE - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_NAME - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PASSWORD - - name: PGPASSWORD - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PASSWORD - - # Optional PgSTAC settings - - name: PGSTAC_USE_QUEUE - value: "false" # Customize: enable/disable queue - - name: PGSTAC_QUEUE_TIMEOUT - value: "30" # Customize: queue timeout in seconds - - backoffLimit: 3 # Customize: number of retries diff --git a/applications/argocd/production/applications/montandon-eoapi/internal/pgstac-migrate-job.yaml b/applications/argocd/production/applications/montandon-eoapi/internal/pgstac-migrate-job.yaml deleted file mode 100644 index abc7cbbb..00000000 --- a/applications/argocd/production/applications/montandon-eoapi/internal/pgstac-migrate-job.yaml +++ /dev/null @@ -1,98 +0,0 @@ -# Static job for PgSTAC schema migration -# This job can be used independently of the pgstacBootstrap feature -# Uncomment and customize the values below for your deployment ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: montandon-eoapi-pgstac-migrate - labels: - app: montandon-eoapi-pgstac-migrate - component: database -spec: - template: - metadata: - labels: - app: montandon-eoapi-pgstac-migrate - component: database - spec: - restartPolicy: Never - containers: - - name: pgstac-migrate - image: ghcr.io/stac-utils/pgstac-pypgstac:v0.9.8 # Customize: image version - command: - - "/bin/sh" - - "-c" - args: - - | - # Exit on any error - set -e - - # Wait for database readiness - echo "Double-checking database readiness..." - pypgstac pgready - # Run PgSTAC migrations - echo "Running PgSTAC migrations..." - pypgstac migrate - - echo "PgSTAC migration complete" - env: - # Database connection settings - - name: POSTGRES_HOST - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_HOST - - name: PGHOST - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_HOST - - name: POSTGRES_PORT - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PORT - - name: PGPORT - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PORT - - name: POSTGRES_USER - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_USER - - name: PGUSER - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_USER - - name: POSTGRES_DB - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_NAME - - name: PGDATABASE - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_NAME - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PASSWORD - - name: PGPASSWORD - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PASSWORD - - # Optional PgSTAC settings - - name: PGSTAC_USE_QUEUE - value: "false" # Customize: enable/disable queue - - name: PGSTAC_QUEUE_TIMEOUT - value: "30" # Customize: queue timeout in seconds - - backoffLimit: 3 # Customize: number of retries diff --git a/applications/argocd/production/applications/montandon-eoapi/internal/pgstac-queryables-job.yaml b/applications/argocd/production/applications/montandon-eoapi/internal/pgstac-queryables-job.yaml deleted file mode 100644 index a1a2d1ef..00000000 --- a/applications/argocd/production/applications/montandon-eoapi/internal/pgstac-queryables-job.yaml +++ /dev/null @@ -1,207 +0,0 @@ -# Static job for loading PgSTAC queryables -# This job should run after pgstac-migrate completes -# Uncomment and customize the values below for your deployment ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: montandon-eoapi-load-queryables - labels: - app: montandon-eoapi-load-queryables - component: database - # annotations: - # Optional: Add FluxCD/ArgoCD annotations here - # For FluxCD HelmRelease dependencies, use dependsOn field instead - # argocd.argoproj.io/hook: "PreSync" - # argocd.argoproj.io/sync-wave: "0" # Run after migrate (wave -1) -spec: - template: - metadata: - labels: - app: montandon-eoapi-load-queryables - component: database - spec: - restartPolicy: Never - initContainers: - - name: wait-for-pgstac-schema - image: postgres:16-alpine - command: - - "/bin/sh" - - "-c" - args: - - | - echo "Waiting for PgSTAC schema to be ready..." - until psql -c "SELECT 1 FROM pgstac.migrations WHERE version IS NOT NULL LIMIT 1;" > /dev/null 2>&1; do - echo "PgSTAC schema not ready, waiting..." - sleep 10 - done - echo "PgSTAC schema is ready!" - env: - # Database connection settings - - name: POSTGRES_HOST - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_HOST - - name: PGHOST - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_HOST - - name: POSTGRES_PORT - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PORT - - name: PGPORT - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PORT - - name: POSTGRES_USER - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_USER - - name: PGUSER - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_USER - - name: POSTGRES_DB - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_NAME - - name: PGDATABASE - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_NAME - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PASSWORD - - name: PGPASSWORD - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PASSWORD - containers: - - name: pgstac-load-queryables - image: ghcr.io/stac-utils/pgstac-pypgstac:v0.9.8 # Customize: image version - command: - - "/bin/sh" - - "-c" - args: - - | - # Exit on any error - set -e - - # Wait for database readiness (redundant check) - echo "Checking database readiness..." - pypgstac pgready - - echo "Loading queryables configurations..." - - # Example queryables loading - customize as needed - # Replace with your actual queryables files/configurations - - pypgstac load-queryables /opt/queryables/stac_queryables.json \ - --index-fields ["monty:hazard_codes","monty:country_codes","roles"] \ - --delete-missing - - echo "Queryables loading complete" - env: - # Database connection settings - - name: POSTGRES_HOST - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_HOST - - name: PGHOST - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_HOST - - name: POSTGRES_PORT - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PORT - - name: PGPORT - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PORT - - name: POSTGRES_USER - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_USER - - name: PGUSER - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_USER - - name: POSTGRES_DB - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_NAME - - name: PGDATABASE - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_NAME - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PASSWORD - - name: PGPASSWORD - valueFrom: - secretKeyRef: - name: pgstac-secrets-montandon-eoapi - key: DB_PASSWORD - - # resources: - # requests: - # memory: "256Mi" - # cpu: "100m" - # limits: - # memory: "512Mi" - # cpu: "250m" - volumeMounts: - - name: queryables-config - mountPath: /opt/queryables - readOnly: true - volumes: - - name: queryables-config - configMap: - name: montandon-eoapi-stac-queryables # Customize: ConfigMap name - # Optional: specify specific files - # items: - # - key: test-queryables.json - # path: test-queryables.json - # - key: custom-queryables.json - # path: custom-queryables.json - - # Optional: Node affinity and tolerations - # affinity: - # nodeAffinity: - # preferredDuringSchedulingIgnoredDuringExecution: - # - weight: 100 - # preference: - # matchExpressions: - # - key: node-type - # operator: In - # values: ["database"] - # tolerations: - # - key: "database" - # operator: "Equal" - # value: "true" - # effect: "NoSchedule" - backoffLimit: 3 # Customize: number of retries - # activeDeadlineSeconds: 600 # Customize: job timeout (10 minutes) diff --git a/applications/argocd/production/applications/montandon-eoapi/internal/queryables-cm.yaml b/applications/argocd/production/applications/montandon-eoapi/internal/queryables-cm.yaml deleted file mode 100644 index 53b601c4..00000000 --- a/applications/argocd/production/applications/montandon-eoapi/internal/queryables-cm.yaml +++ /dev/null @@ -1,127 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: montandon-eoapi-stac-queryables -data: - stac_queryables.json: | - { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$id": "https://example.com/stac/queryables", - "type": "object", - "title": "Queryables for Monty STAC API", - "description": "Queryable names for the Monty STAC API", - "properties": { - "id": { - "description": "Item identifier", - "type": "string" - }, - "collection": { - "description": "Collection identifier", - "type": "string" - }, - "datetime": { - "description": "Datetime", - "type": "string", - "format": "date-time" - }, - "geometry": { - "description": "Geometry", - "type": "object" - }, - "monty:episode_number": { - "description": "The episode number of the event (deprecated)", - "type": "integer" - }, - "monty:country_codes": { - "description": "The country codes of the countries affected by the event, hazard, impact or response", - "type": "array", - "items": { - "type": "string", - "pattern": "^([A-Z]{3})|AB9$" - } - }, - "monty:corr_id": { - "description": "The unique identifier assigned by the Monty system to the reference event", - "type": "string" - }, - "monty:hazard_codes": { - "description": "The hazard codes of the hazards affecting the event", - "type": "array", - "items": { - "type": "string", - "pattern": "^([A-Z]{2}(?:\\d{4}$){0,1})|([a-z]{3}-[a-z]{3}-[a-z]{3}-[a-z]{3})|([A-Z]{2})$" - } - }, - "roles": { - "description": "The roles of the item", - "type": "array", - "items": { - "type": "string", - "enum": ["event", "reference", "source", "hazard", "impact", "response"] - } - }, - "monty:hazard_detail.cluster": { - "description": "The cluster of the hazard (deprecated)", - "type": "string" - }, - "monty:hazard_detail.severity_value": { - "description": "The estimated maximum hazard intensity/magnitude/severity value", - "type": "number" - }, - "monty:hazard_detail.severity_unit": { - "description": "The unit of the severity value", - "type": "string" - }, - "monty:hazard_detail.estimate_type": { - "description": "The type of the estimate", - "type": "string", - "enum": ["primary", "secondary", "modelled"] - }, - "monty:impact_detail.category": { - "description": "The category of impact", - "type": "string", - "enum": [ - "people", "crops", "women", "men", "children_0_4", "children_5_9", - "children_10_14", "children_15_19", "adult_20_24", "adult_25_29", - "adult_30_34", "adult_35_39", "adult_40_44", "adult_45_49", - "adult_50_54", "adult_55_59", "adult_60_64", "elderly", - "wheelchair_users", "roads", "railways", "vulnerable_employment", - "buildings", "reconstruction_costs", "hospitals", "schools", - "local_currency", "global_currency", "local_currency_adj", - "global_currency_adj", "usd_uncertain", "cattle", "aid_general", - "ifrc_contribution", "ifrc_requested", "alertscore", "households" - ] - }, - "monty:impact_detail.type": { - "description": "The estimated value type of the impact", - "type": "string", - "enum": [ - "unspecified", "unaffected", "damaged", "destroyed", "potentially_damaged", - "affected_total", "affected_direct", "affected_indirect", "death", - "missing", "injured", "evacuated", "relocated", "assisted", - "shelter_emergency", "shelter_temporary", "shelter_longterm", "in_need", - "targeted", "disrupted", "cost", "homeless", "displaced_internal", - "displaced_external", "displaced_total", "alertscore", "potentially_affected", - "highest_risk" - ] - }, - "monty:impact_detail.value": { - "description": "The estimated impact value", - "type": "number" - }, - "monty:impact_detail.unit": { - "description": "The units of the impact estimate", - "type": "string" - }, - "monty:impact_detail.estimate_type": { - "description": "The type of the estimate", - "type": "string", - "enum": ["primary", "secondary", "modelled"] - }, - "monty:impact_detail.description": { - "description": "The description of the impact", - "type": "string" - } - }, - "additionalProperties": true - } diff --git a/applications/argocd/production/applications/montandon-eoapi/internal/montandon-eoapi-spc.yaml b/applications/argocd/production/applications/montandon-eoapi/internal/templates/montandon-eoapi-spc.yaml similarity index 81% rename from applications/argocd/production/applications/montandon-eoapi/internal/montandon-eoapi-spc.yaml rename to applications/argocd/production/applications/montandon-eoapi/internal/templates/montandon-eoapi-spc.yaml index 25563dd2..1e7b20da 100644 --- a/applications/argocd/production/applications/montandon-eoapi/internal/montandon-eoapi-spc.yaml +++ b/applications/argocd/production/applications/montandon-eoapi/internal/templates/montandon-eoapi-spc.yaml @@ -1,14 +1,20 @@ +{{- if .Values.azure.secretProviderClass.enabled -}} + apiVersion: secrets-store.csi.x-k8s.io/v1 kind: SecretProviderClass metadata: name: azure-secret-provider-montandon-eoapi + {{- with .Values.commonAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} spec: provider: azure parameters: usePodIdentity: "false" - clientID: "8bf208ec-d73c-42d1-a4a9-817d2936a883" - keyvaultName: "montandon-eoapi-producti" - tenantId: "a2b53be5-734e-4e6c-ab0d-d184f60fd917" + clientID: {{ required "azure.clientID" .Values.azure.clientID }} + tenantId: {{ required "azure.tenantId" .Values.azure.tenantId }} + keyvaultName: {{ required "azure.secretProviderClass.keyvaultName" .Values.azure.secretProviderClass.keyvaultName }} objects: | array: - | @@ -70,3 +76,5 @@ spec: key: DB_PORT - objectName: DB-PORT key: port + +{{- end }} diff --git a/applications/argocd/production/applications/montandon-eoapi/internal/templates/stac-auth-proxy/filters-cm.yaml b/applications/argocd/production/applications/montandon-eoapi/internal/templates/stac-auth-proxy/filters-cm.yaml new file mode 100644 index 00000000..5fbb4ebf --- /dev/null +++ b/applications/argocd/production/applications/montandon-eoapi/internal/templates/stac-auth-proxy/filters-cm.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: stac-auth-proxy-filters + {{- with .Values.commonAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +data: + montandon_filters.py: | +{{ .Files.Get "files/stac-auth-proxy/montandon_filters.py" | indent 4 }} diff --git a/applications/argocd/production/applications/montandon-eoapi/internal/templates/stac-queryables-cm.yaml b/applications/argocd/production/applications/montandon-eoapi/internal/templates/stac-queryables-cm.yaml new file mode 100644 index 00000000..5b30f7e3 --- /dev/null +++ b/applications/argocd/production/applications/montandon-eoapi/internal/templates/stac-queryables-cm.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: montandon-eoapi-stac-queryables + {{- with .Values.commonAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +data: + stac_queryables.json: | +{{ .Files.Get "files/stac_queryables.json" | indent 4 }} diff --git a/applications/argocd/production/applications/montandon-eoapi/internal/values.yaml b/applications/argocd/production/applications/montandon-eoapi/internal/values.yaml new file mode 100644 index 00000000..38358581 --- /dev/null +++ b/applications/argocd/production/applications/montandon-eoapi/internal/values.yaml @@ -0,0 +1,12 @@ +# Create all resources before normal sync +commonAnnotations: + argocd.argoproj.io/hook: "Sync" + argocd.argoproj.io/sync-wave: "-7" + argocd.argoproj.io/hook-delete-policy: "BeforeHookCreation" + +azure: + tenantId: a2b53be5-734e-4e6c-ab0d-d184f60fd917 + clientID: + secretProviderClass: + enabled: true + keyvaultName: