Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,20 @@ jobs:
run: uv run ruff check
- name: Format
run: uv run ruff format --check

helm:
name: Helm chart
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: azure/setup-helm@v4
- name: Register dependency repos
run: |
helm repo add eoapi https://developmentseed.org/eoapi-k8s/
helm repo add stac-manager https://stac-manager.ds.io/
- name: Build chart dependencies
run: helm dependency build infrastructure/charts/eoapi-workshop
- name: Lint
run: helm lint infrastructure/charts/eoapi-workshop
- name: Render checks
run: ./infrastructure/charts/eoapi-workshop/tests/render-checks.sh
53 changes: 53 additions & 0 deletions .github/workflows/publish-workshop-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Publish workshop image

# Builds the JupyterLab workshop image (Dockerfile.local + environment.yml) and
# pushes it to GHCR. Consumed by the Helm chart's per-participant Labs
# (infrastructure/charts/eoapi-workshop, values key `jupyter.image`).
on:
push:
branches:
- main
- foss4geu-helmchart # workshop branch — image is built from here for now
paths:
- Dockerfile.local
- environment.yml
- docs/**
- .github/workflows/publish-workshop-image.yml
workflow_dispatch: {} # manual run (once this workflow is on the default branch)

jobs:
publish:
name: Build and push
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v5

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Image metadata
id: meta
uses: docker/metadata-action@v5
with:
# Portable: resolves to ghcr.io/developmentseed/eoapi-workshop in this repo.
images: ghcr.io/${{ github.repository_owner }}/eoapi-workshop
tags: |
type=raw,value=latest
type=sha,format=long
type=ref,event=branch

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.local
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,8 @@ cdk.context.json
node_modules/

config.yaml

# Helm: pulled chart dependencies (rebuilt with `helm dependency update`).
# Chart.lock IS tracked for reproducible builds.
charts/*/charts/
charts/**/*.tgz
6 changes: 6 additions & 0 deletions infrastructure/charts/eoapi-workshop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Host-specific install overrides generated by deploy.sh — never committed.
.deploy/

# Vendored chart dependencies — rebuilt from Chart.lock by `helm dependency
# build` (deploy.sh runs it). Chart.lock IS tracked; the archives are not.
charts/
13 changes: 13 additions & 0 deletions infrastructure/charts/eoapi-workshop/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Patterns to ignore when building Helm packages.
.DS_Store
.git/
.gitignore
*.tmproj
*.swp
*.bak
*.orig
.idea/
.vscode/
# Local tooling / generated artifacts — not part of the packaged chart.
deploy.sh
.deploy/
9 changes: 9 additions & 0 deletions infrastructure/charts/eoapi-workshop/Chart.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
dependencies:
- name: eoapi
repository: https://developmentseed.org/eoapi-k8s/
version: 0.13.1
- name: stac-manager
repository: https://stac-manager.ds.io/
version: 1.0.3
digest: sha256:c266058775f9745e48df835657208d23f5785875904049c880093faa5a7886bd
generated: "2026-07-01T17:27:13.34478+03:00"
27 changes: 27 additions & 0 deletions infrastructure/charts/eoapi-workshop/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apiVersion: v2
name: eoapi-workshop
description: Minimal, docker-compose-aligned eoAPI deployment for the workshop (no observability/monitoring stack)
type: application
version: 0.1.0
# appVersion tracks the eoAPI application shipped by the eoapi dependency below.
appVersion: "6.3.1"
icon: https://eoapi.dev/img/eoAPI.png
home: https://github.com/developmentseed/eoapi-workshop
sources:
- https://github.com/developmentseed/eoapi-workshop
- https://github.com/developmentseed/eoapi-k8s
dependencies:
# Upstream eoAPI chart. Its packaged .tgz vendors its own subcharts
# (postgrescluster, stac-auth-proxy, prometheus, grafana, knative, ...).
# Disabled components are turned off via values; their `condition` flags
# keep them from rendering, so only the workshop services are deployed.
# NOTE: devseed.com/eoapi-k8s/ 301-redirects to developmentseed.org/eoapi-k8s/.
- name: eoapi
version: 0.13.1
repository: https://developmentseed.org/eoapi-k8s/
# stac-manager: STAC collection/item editing UI (deployed the EOEPCA way —
# its published chart). Routed at /manager via the passthrough ingress.
- name: stac-manager
version: 1.0.3
repository: https://stac-manager.ds.io/
condition: stac-manager.enabled
158 changes: 158 additions & 0 deletions infrastructure/charts/eoapi-workshop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# eoapi-workshop Helm chart

A docker-compose-aligned Helm deployment of [eoAPI](https://eoapi.dev) for the
workshop: an *umbrella* chart over the upstream
[`eoapi`](https://github.com/developmentseed/eoapi-k8s) and
[`stac-manager`](https://github.com/developmentseed/stac-manager) charts, plus
per-participant **JupyterLab** environments — no observability/monitoring stack.

Every service is served at the **root of its own subdomain** under a wildcard
domain (`*.<baseDomain>`, default `eoapi-workshop.ds.io`).

## What gets deployed

| Component | Subdomain of `eoapi-workshop.ds.io` | Notes |
|---|---|---|
| STAC API (via stac-auth-proxy) | `stac.` | pgstac + stac-fastapi, fronted by the auth proxy |
| Raster (titiler-pgstac) | `raster.` | |
| Vector (tipg) | `vector.` | serves `features.ecoregions` (loaded by the features-loader Job) |
| STAC Browser | `browser.` | root-serving `radiantearth/stac-browser` |
| STAC Manager (editing UI) | `manager.` | `stac-manager` chart 1.0.3 |
| Mock OIDC server | `mock-oidc.` | test-only auth |
| JupyterLab × N | `lab-01.`…`lab-05.` | one isolated pod + PVC + token each |
| Database (pgstac) | in-cluster only | Crunchy `PostgresCluster` |

Disabled (unlike upstream `experimental.yaml`): `multidim`, `docServer`,
`eoapi-notifier`, `knative`, `monitoring.*`, `observability.grafana`, autoscaling.

## Contracts (read first)

- **Wildcard DNS required** — `*.<baseDomain>` must A-record to the ingress
LoadBalancer IP (check: `dig +short stac.eoapi-workshop.ds.io`).
- **Release name and namespace must both be `eoapi`** — the proxy's in-cluster
OIDC URL (`eoapi-mock-oidc-server.eoapi.svc…`) is derived from them. `deploy.sh`
defaults to this.
- **Test-only auth, http by default** — the mock OIDC ships `test-client` /
`test-secret` and reads are public (`DEFAULT_PUBLIC=true`). STAC Manager (and
Browser) *login/editing* needs a secure context, so enable `routing.tls` for
HTTPS; over http the UIs are browse/read-only. Not for production.

## Prerequisites

Kubernetes 1.23+ with an **NGINX ingress controller**, the **Crunchy Postgres
Operator (PGO)** (hard requirement — `postgrescluster` only reconciles if PGO/CRDs
are installed), Helm 3.8+, and the wildcard DNS above. `deploy.sh` installs the
two operators for you (unless `SKIP_PREREQS=1`).

## Deploy

`deploy.sh` installs prerequisites, generates host overrides (per-subdomain URLs +
a stable per-participant token), installs the release, waits for rollouts, and
verifies end-to-end. Idempotent — tokens/URLs stay stable across re-runs.

```bash
cd infrastructure/charts/eoapi-workshop
./deploy.sh deploy # prerequisites + chart + verify
./deploy.sh verify # re-run endpoint/auth checks, print Lab URLs
./deploy.sh urls # print participant Lab URLs (+ tokens)
./deploy.sh teardown [--all] # remove release (--all also removes operators)
```

Env vars: `BASE_DOMAIN` (default `eoapi-workshop.ds.io`), `SKIP_PREREQS=1`,
`GHCR_USER`+`GHCR_TOKEN` (pull secret for a private image — see
[Participant JupyterLabs](#participant-jupyterlabs)). `RELEASE`/`NAMESPACE` must
stay `eoapi`.

The pgstac DB is created asynchronously by PGO and seeded with sample STAC data,
so API pods may restart a few times before `Ready` on first install.

To install without `deploy.sh`: `helm dependency update`, then `helm install eoapi
. -n eoapi --create-namespace` with a `-f` overrides file (generate one for a
non-default domain via `BASE_DOMAIN=… ./deploy.sh overrides`).

## Routing

All routing is one Ingress (`templates/subdomain-ingress.yaml`): a host rule per
service, each serving at `/` with no rewrite. The upstream path-based ingress is
off and each app serves at its subdomain root — stac/raster/vector with
`--root-path=`, proxy `ROOT_PATH=""`, browser via the root-serving
`radiantearth/stac-browser`, Labs without `--ServerApp.base_url`. Per-subdomain
URLs default to the workshop domain in `values.yaml`; `deploy.sh` rewrites them for
another `BASE_DOMAIN` via the gitignored `.deploy/overrides.yaml`.

## Verify

`./deploy.sh verify` checks every service subdomain, runs the auth test, and prints
the Lab URLs. Manually:

```bash
kubectl -n eoapi get pods
curl -s http://stac.eoapi-workshop.ds.io/healthz # also raster. / vector.
curl -s http://stac.eoapi-workshop.ds.io/collections # sample items
# UIs: browser. manager. mock-oidc./.well-known/openid-configuration
```

## Participant JupyterLabs

`jupyter.participants` (default `lab-01`…`lab-05`; edit for any N) → one Deployment
+ Service + PVC each at `<name>.<baseDomain>`, running the GHCR image
`ghcr.io/developmentseed/eoapi-workshop` (built by
`.github/workflows/publish-workshop-image.yml`). Each Lab gets the eoAPI endpoints
+ DB creds injected (from the `eoapi-pguser-eoapi` PGO secret) and an access token
(`./deploy.sh urls` prints them).

- **Persistence:** notebooks come fresh from the image (`/home/jovyan/docs`) on
every start, so updates always appear; only `/home/jovyan/work` persists (save
work there — edits to the provided notebooks reset on restart).
- **Private image:** GHCR packages are private by default. Either make the package
public, or pass a pull token — `GHCR_USER=<u> GHCR_TOKEN=<read:packages token>
./deploy.sh deploy` creates the `ghcr-pull` secret and wires it to the default
ServiceAccount before the Labs start.

## Testing auth

`stac-auth-proxy` fronts STAC at `stac.<baseDomain>`: **GET is public, mutations
need a bearer token** from the mock OIDC server (`jq` required).

```bash
b=eoapi-workshop.ds.io
curl -s -o/dev/null -w '%{http_code}\n' http://stac.$b/collections # 200 (public read)
curl -s -o/dev/null -w '%{http_code}\n' -X POST http://stac.$b/collections \
-H 'Content-Type: application/json' -d '{}' # 401 (no token)
TOKEN=$(curl -s http://mock-oidc.$b/ \
--data-raw 'username=testuser&scopes=openid+stac:read+stac:write' \
-H 'Accept: application/json' | jq -r .token)
curl -s -o/dev/null -w '%{http_code}\n' -X POST http://stac.$b/collections \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}' # NOT 401
```

**401 without a token, non-401 with one** = working. If it stays 401, check
`kubectl -n eoapi logs deploy/eoapi-stac-auth-proxy` (usual cause: release/namespace
not `eoapi`).

## Upgrade / uninstall

```bash
./deploy.sh deploy # idempotent re-deploy (tokens preserved)
helm uninstall eoapi -n eoapi # or ./deploy.sh teardown
kubectl -n eoapi delete pvc --all # PVCs (DB + Lab work) are retained by design
```

## Notebook data

The workshop notebooks (`docs/00`–`06`) run in the Labs against this deployment:
- `pgstacBootstrap.loadSamples` is **off** — the upstream sample collection
`noaa-emergency-response` is stored without a STAC `type` field and breaks
`pystac_client` (notebook 03). The notebooks create their own STAC data.
- the **features-loader Job** (`featuresLoader.enabled`) loads the NA CEC Level III
Ecoregions into `features.ecoregions`, and tipg is configured with
`TIPG_DB_SCHEMAS=["features","public"]`, so notebook 05 has vector data.

## Limitations
- **UI login needs TLS** — STAC Manager / Browser OIDC login uses PKCE (needs
HTTPS); over http they're read-only. Enable `routing.tls`. (Browser's
`redirect_uri` also still derives from the apex host upstream.)
- **Capacity** — N always-on Labs at `limit 2 CPU / 4Gi` (default 5 ≈ ≤10 CPU /
20Gi) + stac-manager's ~4Gi startup build + the backend. Size nodes to N.
- **Not production** — test auth, single 1-replica DB (5Gi), http. For production
use the CDK/AWS stack in [`DEPLOYMENT.md`](../../../DEPLOYMENT.md).
Loading
Loading