This is an automated email from the ASF dual-hosted git repository. yasithdev pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/airavata-portals.git
commit 00d8725b486e4b450e093b419d498252b6de49b6 Author: yasithdev <[email protected]> AuthorDate: Thu Jun 11 04:17:07 2026 -0400 added quickstart and readme --- CLAUDE.md | 5 ++ README.md | 32 +++++++++ Tiltfile | 55 +++++++++++++++ airavata-django-portal/.gitignore | 2 + airavata-django-portal/Dockerfile | 44 ++++++------ airavata-django-portal/README.md | 7 ++ airavata-django-portal/compose.yml | 34 ++++++++++ devstack/README.md | 136 +++++++++++++++++++++++++++++++++++++ devstack/devstack | 22 ++++++ devstack/lib/certs.sh | 13 ++++ devstack/lib/colima.sh | 24 +++++++ devstack/lib/commands.sh | 43 ++++++++++++ devstack/lib/config.sh | 25 +++++++ devstack/lib/dns.sh | 52 ++++++++++++++ devstack/lib/ingress.sh | 38 +++++++++++ devstack/lib/verify.sh | 13 ++++ devstack/traefik/compose.yml | 29 ++++++++ devstack/traefik/traefik.yml | 18 +++++ 18 files changed, 573 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8ddfc606a..f118ef7ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,6 +50,11 @@ uv run ty check # Python type check (ty) ./lint_js.sh # JS lint (ESLint + Prettier) ``` +Recommended: run the whole stack with Tilt — `tilt up` in the `airavata` repo, +then `tilt up --port 10351` here (serves the portal at +https://gateway.airavata.host). Prerequisite: `./devstack/devstack setup` (once) +before first use. See the `Tiltfile` at the repo root and `devstack/README.md`. + ### Django Apps Each app under `django_airavata/apps/` is self-contained with its own frontend: diff --git a/README.md b/README.md index a0809e3a8..aaa0947e8 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,38 @@ The `airavata-portals` repository is a consolidated home for all web-based user interfaces built on top of the [Apache Airavata](https://airavata.apache.org/) middleware platform. This collection of frontend components and frameworks enables seamless interaction with Airavata's powerful orchestration, identity, data, and compute services. +## Running locally with Tilt + +The Django portal runs as a container tenant on the shared `airavata-devstack` +substrate (one colima VM, one Traefik ingress serving `*.airavata.host`), managed +with [Tilt](https://tilt.dev). The two stacks run as separate Tilt instances. + +### One-time setup + +```bash +# From either repo — both carry the identical devstack kit +./devstack/devstack setup +``` + +This installs colima, mkcert, dnsmasq, creates the shared VM and Traefik ingress, +and configures wildcard DNS for `*.airavata.host` → `127.0.0.1` (trusted cert, no `-k`). + +### Daily startup + +```bash +# 1. Start the Airavata backend stack (in the apache/airavata repo) +cd ../airavata && tilt up + +# 2. Start the portals (this repo) on a distinct Tilt port +tilt up --port 10351 +``` + +The Django portal is then served at **https://gateway.airavata.host** (trusted +HTTPS via the shared Traefik ingress). The portal runs inside the shared colima VM +as a container; `settings_local.py` is generated automatically on first `tilt up` +if the file does not exist. Only the Django portal is wired into the Tiltfile today; +other portals can be added as additional resources later. + ## Repository Structure This repository contains the following sub-projects and templates: diff --git a/Tiltfile b/Tiltfile new file mode 100644 index 000000000..5e599be8d --- /dev/null +++ b/Tiltfile @@ -0,0 +1,55 @@ +# -*- mode: Python -*- +# airavata-portals tenant. Prereq: ./devstack/devstack setup (shared with airavata). +# Run on a distinct Tilt port: tilt up --port 10351 +# Tiltfiles are Starlark, not Python: no `import`; `os` is built in (getenv/putenv only). +PROFILE = os.getenv('DEVSTACK_PROFILE', 'airavata') +os.putenv('DOCKER_HOST', 'unix://%s/.colima/%s/docker.sock' % (os.getenv('HOME'), PROFILE)) +PORTAL = 'airavata-django-portal' +SETTINGS = PORTAL + '/django_airavata/settings_local.py' +SDK_SRC = '../airavata/airavata-python-sdk' + +# Stage the sibling airavata-python-sdk into the portal build context. It's an editable +# path dependency that lives in the sibling airavata repo, outside this build context, so +# the Dockerfile can't COPY it directly. This rsync mirrors it into .devstack-sdk/ (gitignored) +# and re-runs when the SDK changes, which retriggers the image build. +local_resource('stage-sdk', + cmd='mkdir -p %s/.devstack-sdk && rsync -a --delete --exclude .venv --exclude __pycache__ --exclude .git %s/ %s/.devstack-sdk/airavata-python-sdk/' % (PORTAL, SDK_SRC, PORTAL), + deps=[SDK_SRC], labels=['django-portal']) + +local_resource('portal-settings', cmd=''' +set -e +f="%s" +if [ ! -f "$f" ]; then cat > "$f" <<'EOF' +DEBUG = True +ALLOWED_HOSTS = ['.airavata.host', 'localhost', '127.0.0.1'] +USE_X_FORWARDED_HOST = True +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +CSRF_TRUSTED_ORIGINS = ['https://gateway.airavata.host'] +KEYCLOAK_CLIENT_ID = 'pga' +KEYCLOAK_CLIENT_SECRET = 'm36BXQIxX3j3VILadeHMK5IvbOeRlCCc' +KEYCLOAK_AUTHORIZE_URL = 'https://auth.airavata.host/realms/default/protocol/openid-connect/auth' +KEYCLOAK_TOKEN_URL = 'https://auth.airavata.host/realms/default/protocol/openid-connect/token' +KEYCLOAK_USERINFO_URL = 'https://auth.airavata.host/realms/default/protocol/openid-connect/userinfo' +KEYCLOAK_LOGOUT_URL = 'https://auth.airavata.host/realms/default/protocol/openid-connect/logout' +KEYCLOAK_VERIFY_SSL = False +GATEWAY_ID = 'default' +GRPC_API_HOST = 'airavata-server' +GRPC_API_PORT = 9090 +GRPC_API_SECURE = False +PORTAL_TITLE = 'Airavata Django Portal (devstack)' +EOF +fi +''' % SETTINGS, labels=['django-portal']) + +local_resource('devstack-ensure', cmd='./devstack/devstack ensure', + resource_deps=['portal-settings'], labels=['platform']) + +# NOTE: no `only=` — in Tilt that restricts the build CONTEXT (not just rebuild triggers), +# and the Dockerfile needs the whole portal source (django_airavata/, scripts/, .devstack-sdk/). +# .dockerignore already trims node_modules/.venv/dist. Runtime source edits flow through the +# bind-mount + polling reload; dep changes (uv.lock) rebuild the image. +docker_build('airavata-django-portal:dev', PORTAL, dockerfile=PORTAL + '/Dockerfile') + +docker_compose(PORTAL + '/compose.yml') +dc_resource('airavata-django-portal', resource_deps=['devstack-ensure', 'stage-sdk'], labels=['django-portal'], + links=[link('https://gateway.airavata.host', 'Gateway Portal')]) diff --git a/airavata-django-portal/.gitignore b/airavata-django-portal/.gitignore index 4824918ba..56090b860 100644 --- a/airavata-django-portal/.gitignore +++ b/airavata-django-portal/.gitignore @@ -21,3 +21,5 @@ yarn-error.log .pytest_cache/ .ruff_cache/ __pycache__/ +# devstack: staged copy of the sibling airavata-python-sdk (built into the portal image) +.devstack-sdk/ diff --git a/airavata-django-portal/Dockerfile b/airavata-django-portal/Dockerfile index e7ef0ba4a..efd548b4a 100644 --- a/airavata-django-portal/Dockerfile +++ b/airavata-django-portal/Dockerfile @@ -1,5 +1,6 @@ # node image is based on Debian and includes necessary build tools -FROM node:19.9.0 as build-stage +# node 20 LTS — vite 6 requires ^18 || ^20 || >=22 (excludes the old 19.9.0). +FROM node:20 as build-stage # build api javascript # api must come first, then common, since the others depend on these @@ -64,26 +65,32 @@ RUN yarn run build FROM python:3.12-slim as server-stage +# The portal's only Airavata dependency is the editable airavata-python-sdk, pinned in +# pyproject.toml as `[tool.uv.sources] path = "../../airavata/airavata-python-sdk"`. uv +# REFUSES a relative source path that escapes above the project root, so the app cannot +# live at /code (depth 1). We mirror the host's repo layout instead: the portal at +# /workspace/airavata-portals/airavata-django-portal and the SDK at +# /workspace/airavata/airavata-python-sdk, so `../../airavata/...` resolves in-tree — +# identically at build time, in the running container, and on the host (remote mode). ENV PYTHONUNBUFFERED=1 \ UV_COMPILE_BYTECODE=1 \ UV_LINK_MODE=copy \ - PATH="/code/.venv/bin:$PATH" + PATH="/workspace/airavata-portals/airavata-django-portal/.venv/bin:$PATH" # uv for dependency management (https://docs.astral.sh/uv/). COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ EXPOSE 8000 -WORKDIR /code +WORKDIR /workspace/airavata-portals/airavata-django-portal RUN apt-get update && apt-get install -y --no-install-recommends \ git gcc g++ zlib1g-dev libjpeg-dev && \ rm -rf /var/lib/apt/lists/* -# NOTE: the portal's only Airavata dependency is the airavata-python-sdk, resolved -# from a sibling apache/airavata checkout via [tool.uv.sources] in pyproject.toml. -# It is not yet on PyPI, so the build context must make it available at -# ../../airavata/airavata-python-sdk (e.g. build from the workspace root with an -# appropriate context) or the pin must point at a published release. +# Sibling airavata-python-sdk, staged into the portal context by the `stage-sdk` Tilt step +# (it lives in the sibling airavata repo, outside this build context). Placed two levels up +# so the pyproject `../../airavata/airavata-python-sdk` path resolves to it. +COPY .devstack-sdk/airavata-python-sdk /workspace/airavata/airavata-python-sdk COPY pyproject.toml uv.lock README.md ./ COPY ./django_airavata ./django_airavata RUN uv sync --frozen --no-dev @@ -93,23 +100,22 @@ COPY ./django_airavata/settings_local.py.sample ./django_airavata/settings_local COPY ./ . -# Copy javascript builds from build-stage -WORKDIR /code/django_airavata/apps/api/static/django_airavata_api +# Copy javascript builds from build-stage (sources stay at the build-stage's /code) +WORKDIR /workspace/airavata-portals/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api COPY --from=build-stage /code/django_airavata/apps/api/static/django_airavata_api . -WORKDIR /code/django_airavata/static/common/dist +WORKDIR /workspace/airavata-portals/airavata-django-portal/django_airavata/static/common/dist COPY --from=build-stage /code/django_airavata/static/common/dist . -WORKDIR /code/django_airavata/apps/admin/static/django_airavata_admin +WORKDIR /workspace/airavata-portals/airavata-django-portal/django_airavata/apps/admin/static/django_airavata_admin COPY --from=build-stage /code/django_airavata/apps/admin/static/django_airavata_admin . -WORKDIR /code/django_airavata/apps/groups/static/django_airavata_groups +WORKDIR /workspace/airavata-portals/airavata-django-portal/django_airavata/apps/groups/static/django_airavata_groups COPY --from=build-stage /code/django_airavata/apps/groups/static/django_airavata_groups . -WORKDIR /code/django_airavata/apps/auth/static/django_airavata_auth +WORKDIR /workspace/airavata-portals/airavata-django-portal/django_airavata/apps/auth/static/django_airavata_auth COPY --from=build-stage /code/django_airavata/apps/auth/static/django_airavata_auth . -WORKDIR /code/django_airavata/apps/workspace/static/django_airavata_workspace +WORKDIR /workspace/airavata-portals/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace COPY --from=build-stage /code/django_airavata/apps/workspace/static/django_airavata_workspace . -WORKDIR /code/django_airavata/apps/dataparsers/static/django_airavata_dataparsers +WORKDIR /workspace/airavata-portals/airavata-django-portal/django_airavata/apps/dataparsers/static/django_airavata_dataparsers COPY --from=build-stage /code/django_airavata/apps/dataparsers/static/django_airavata_dataparsers . -WORKDIR /code - -ENTRYPOINT ["/code/scripts/start-server.sh"] +WORKDIR /workspace/airavata-portals/airavata-django-portal +ENTRYPOINT ["/workspace/airavata-portals/airavata-django-portal/scripts/start-server.sh"] diff --git a/airavata-django-portal/README.md b/airavata-django-portal/README.md index 62c154c46..a25abf7a9 100644 --- a/airavata-django-portal/README.md +++ b/airavata-django-portal/README.md @@ -27,6 +27,13 @@ any yarn commands. See [the Yarn package manager](https://classic.yarnpkg.com/lang/en/) for information on how to install Yarn 1 (Classic). +> **Recommended (Tilt + devstack):** run the portal as a container against a +> local Apache Airavata stack, both managed with [Tilt](https://tilt.dev). +> One-time setup: `./devstack/devstack setup` (from the `airavata-portals` root). +> Then `cd ../airavata && tilt up`, and from the `airavata-portals` repo +> `tilt up --port 10351`. The portal is served at <https://gateway.airavata.host> +> (trusted HTTPS, no `-k`). The manual steps below are the equivalent without Tilt. + This project uses [uv](https://docs.astral.sh/uv/) for Python dependency management. The portal has **no database** — there is nothing to migrate, and all persistence goes through the Airavata gRPC API and the cache. diff --git a/airavata-django-portal/compose.yml b/airavata-django-portal/compose.yml new file mode 100644 index 000000000..6f36efd88 --- /dev/null +++ b/airavata-django-portal/compose.yml @@ -0,0 +1,34 @@ +name: airavata-portal +services: + airavata-django-portal: + image: airavata-django-portal:dev + container_name: airavata-django-portal + working_dir: /workspace/airavata-portals/airavata-django-portal + entrypoint: [] # override image ENTRYPOINT so `command` runs via uv + command: uv run python manage.py runserver 0.0.0.0:8000 + environment: + CHOKIDAR_USEPOLLING: "true" + WATCHPACK_POLLING: "true" + volumes: + - ".:/workspace/airavata-portals/airavata-django-portal" # writable source mount (autoreload via polling) + # anonymous volumes preserve image-built artifacts the host source mount would shadow: + - "/workspace/airavata-portals/airavata-django-portal/.venv" # uv venv (incl. the editable SDK) + - "/workspace/airavata-portals/airavata-django-portal/django_airavata/apps/api/static/django_airavata_api" + - "/workspace/airavata-portals/airavata-django-portal/django_airavata/static/common/dist" + - "/workspace/airavata-portals/airavata-django-portal/django_airavata/apps/admin/static/django_airavata_admin" + - "/workspace/airavata-portals/airavata-django-portal/django_airavata/apps/groups/static/django_airavata_groups" + - "/workspace/airavata-portals/airavata-django-portal/django_airavata/apps/auth/static/django_airavata_auth" + - "/workspace/airavata-portals/airavata-django-portal/django_airavata/apps/workspace/static/django_airavata_workspace" + - "/workspace/airavata-portals/airavata-django-portal/django_airavata/apps/dataparsers/static/django_airavata_dataparsers" + networks: + airavata-devstack: {} + labels: + - traefik.enable=true + - traefik.http.routers.airavata-gateway.rule=Host(`gateway.airavata.host`) + - traefik.http.routers.airavata-gateway.entrypoints=web,websecure + - traefik.http.routers.airavata-gateway.tls=true + - traefik.http.services.airavata-gateway.loadbalancer.server.port=8000 + - traefik.docker.network=airavata-devstack +networks: + airavata-devstack: + external: true diff --git a/devstack/README.md b/devstack/README.md new file mode 100644 index 000000000..e3049bf19 --- /dev/null +++ b/devstack/README.md @@ -0,0 +1,136 @@ +# devstack + +Shared dev-substrate kit. One colima VM runs all containers; a single shared Traefik on +`127.0.0.1:80/443` serves `https://*.airavata.host` (mkcert-trusted) across per-project +docker networks. dnsmasq maps the dev TLD to `127.0.0.1`. + +--- + +## One-time prerequisites + +### macOS + +```bash +# Install deps (idempotent via Homebrew) +brew install colima docker dnsmasq mkcert + +# Trust the mkcert CA in system and NSS stores +mkcert -install + +# Then run setup (writes dnsmasq drop-in, /etc/resolver, starts the colima VM and Traefik) +./devstack/devstack setup # run as your normal user — it elevates via sudo internally only for DNS +``` + +### Ubuntu / Debian Linux + +```bash +sudo apt-get update && sudo apt-get install -y dnsmasq mkcert libnss3-tools +# colima + docker for Linux must be installed separately: +# https://github.com/abiosoft/colima/blob/main/docs/INSTALL.md + +mkcert -install + +./devstack/devstack setup # run as your normal user — it elevates via sudo internally only for DNS +``` + +After `setup` completes, run `tilt up` from the repo root to start the project services. + +--- + +## Daily workflow + +```bash +# First terminal — start/resume the shared substrate (idempotent, no sudo) +./devstack/devstack ensure + +# Second terminal — start the project +tilt up +``` + +`ensure` is also the first resource in the `Tiltfile`; `tilt up` runs it automatically. + +--- + +## Status + +```bash +./devstack/devstack status +# profile=airavata project=airavata tld=airavata.host +# INFO colima [profile=airavata] is running +# traefik: Up 3 hours +``` + +--- + +## Restart matrix + +| Scenario | Command | Effect | +|----------|---------|--------| +| Reboot (colima auto-starts) | `./devstack/devstack ensure` | Idempotent bring-up | +| colima stopped manually | `colima start -p airavata` then `ensure` | Restores VM + ingress | +| Stop everything | `./devstack/devstack down` | Stops ingress + colima VM; **global** (all projects) | +| Full reset | `./devstack/devstack reset` | Deletes the VM; **global + destructive** — prompts for profile name | + +`down` and `reset` are both global — they affect every project sharing the same colima VM. +After `reset`, re-run `./devstack/devstack setup` to recreate everything from scratch. + +--- + +## Traefik provider decision + +Phase 1 uses the **docker-socket-proxy** variant (Task 1 empirically verified): + +- `tecnativa/docker-socket-proxy:0.3.0` advertises a modern Docker API version to Traefik, + bypassing the colima socket version mismatch (`client version 1.24 is too old, min 1.44`). +- Labels on project containers are the routing source of truth (no per-service file fragments). +- The file provider is also active (`traefik/dynamic/`) for the TLS store default cert and + per-project cert entries written by `ingress_register_project`. + +--- + +## Config + +Defaults live in `~/.airavata-devstack/config.env`: + +```env +DEVSTACK_PROFILE=airavata +DEVSTACK_PROJECT=airavata +DEVSTACK_TLD=airavata.host +``` + +Override any key before running `setup` or `ensure`; values are persisted on first `setup`. + +--- + +## State directory layout + +``` +~/.airavata-devstack/ + config.env # profile/project/tld + certs/ + rootCA.pem # mkcert CA (copied from CAROOT) + airavata.host.pem # wildcard+apex cert + airavata.host-key.pem + traefik/ + traefik.yml # static config (copied from devstack/traefik/) + dynamic/ + _tls-default.yml # default TLS store cert + airavata.yml # per-project cert entry +``` + +All state is under `$HOME` — colima requires host mounts to be under the home directory +(not `/tmp`). `tilt down -v` removes containers but leaves this state intact; only +`devstack reset` destroys it. + +--- + +## Hostnames + +| URL | Service | +|-----|---------| +| `https://api.airavata.host` | Airavata server (gRPC + REST) | +| `https://auth.airavata.host` | Keycloak | +| `https://rabbitmq.airavata.host` | RabbitMQ management UI | +| `https://adminer.airavata.host` | Adminer (`--profile tools`) | +| `https://gateway.airavata.host` | Django portal (portals repo) | +| `db.airavata.host:3306` | MariaDB — raw TCP on host `127.0.0.1:3306` (connect with a DB client, not a browser) | diff --git a/devstack/devstack b/devstack/devstack new file mode 100755 index 000000000..41c3254dc --- /dev/null +++ b/devstack/devstack @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# devstack — shared dev-substrate CLI. Usage: devstack {setup|ensure|status|down|reset} +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$HERE/lib/config.sh" +. "$HERE/lib/colima.sh" +. "$HERE/lib/certs.sh" +. "$HERE/lib/dns.sh" +. "$HERE/lib/ingress.sh" +. "$HERE/lib/verify.sh" +. "$HERE/lib/commands.sh" +devstack_load_config + +cmd="${1:-help}" +case "$cmd" in + setup) devstack_setup ;; + ensure) devstack_ensure ;; + status) devstack_status ;; + down) devstack_down ;; + reset) devstack_reset ;; + *) echo "usage: devstack {setup|ensure|status|down|reset}"; exit 2 ;; +esac diff --git a/devstack/lib/certs.sh b/devstack/lib/certs.sh new file mode 100644 index 000000000..82e47be49 --- /dev/null +++ b/devstack/lib/certs.sh @@ -0,0 +1,13 @@ +# devstack/lib/certs.sh — mkcert CA trust + per-project wildcard+apex cert. +devstack_certs() { + command -v mkcert >/dev/null || { echo "ERROR: mkcert not installed" >&2; exit 1; } + mkcert -install # idempotent; trusts the CA in system + NSS stores + cp "$(mkcert -CAROOT)/rootCA.pem" "$DEVSTACK_HOME/certs/rootCA.pem" + local cert="$DEVSTACK_HOME/certs/$DEVSTACK_TLD.pem" + local key="$DEVSTACK_HOME/certs/$DEVSTACK_TLD-key.pem" + if [ ! -f "$cert" ]; then + # wildcard does NOT cover the apex — list both SANs. + mkcert -cert-file "$cert" -key-file "$key" "*.$DEVSTACK_TLD" "$DEVSTACK_TLD" + echo "generated cert for *.$DEVSTACK_TLD + $DEVSTACK_TLD" + fi +} diff --git a/devstack/lib/colima.sh b/devstack/lib/colima.sh new file mode 100644 index 000000000..d9023324b --- /dev/null +++ b/devstack/lib/colima.sh @@ -0,0 +1,24 @@ +# devstack/lib/colima.sh — colima VM lifecycle for the shared substrate. +# NOTE: no --network-address (macOS-only + root-gated); ingress is via 127.0.0.1 forwarding. +DEVSTACK_CPU="${DEVSTACK_CPU:-4}" +DEVSTACK_MEM="${DEVSTACK_MEM:-8}" +DEVSTACK_DISK="${DEVSTACK_DISK:-60}" + +colima_running() { colima status -p "$DEVSTACK_PROFILE" >/dev/null 2>&1; } + +colima_create() { + echo "creating colima VM '$DEVSTACK_PROFILE' (cpu=$DEVSTACK_CPU mem=${DEVSTACK_MEM}g disk=${DEVSTACK_DISK}g)" + colima start -p "$DEVSTACK_PROFILE" \ + --cpu "$DEVSTACK_CPU" --memory "$DEVSTACK_MEM" --disk "$DEVSTACK_DISK" \ + --mount-inotify +} + +colima_start_if_stopped() { + if colima_running; then echo "colima '$DEVSTACK_PROFILE' already running"; else + echo "starting colima '$DEVSTACK_PROFILE'"; colima start -p "$DEVSTACK_PROFILE"; fi +} + +colima_require() { + colima_running || { echo "ERROR: colima '$DEVSTACK_PROFILE' is not running. Run: ./devstack/devstack setup" >&2; exit 1; } + [ -S "$DEVSTACK_SOCK" ] || { echo "ERROR: docker socket $DEVSTACK_SOCK missing" >&2; exit 1; } +} diff --git a/devstack/lib/commands.sh b/devstack/lib/commands.sh new file mode 100644 index 000000000..9314e3c92 --- /dev/null +++ b/devstack/lib/commands.sh @@ -0,0 +1,43 @@ +# devstack/lib/commands.sh — top-level verbs. +devstack_setup() { + [ "$(devstack_os)" = unknown ] && { echo "unsupported OS"; exit 1; } + devstack_save_config + if [ "$(devstack_os)" = macos ]; then + command -v brew >/dev/null || { echo "install Homebrew first"; exit 1; } + brew list colima >/dev/null 2>&1 || brew install colima docker dnsmasq mkcert + else + sudo apt-get update -y && sudo apt-get install -y dnsmasq mkcert libnss3-tools + command -v colima >/dev/null || { echo "install colima + docker for Linux first"; exit 1; } + fi + colima_running || colima_create + devstack_certs + devstack_dns # sudo steps live here + ingress_up + ingress_register_project + devstack_verify + echo "devstack setup complete — now run: tilt up" +} + +devstack_ensure() { + colima_require + ingress_up + ingress_register_project + devstack_verify +} + +devstack_status() { + echo "profile=$DEVSTACK_PROFILE project=$DEVSTACK_PROJECT tld=$DEVSTACK_TLD" + colima status -p "$DEVSTACK_PROFILE" 2>&1 | sed 's/^/ /' || true + docker ps --filter name=airavata-devstack-traefik --format ' traefik: {{.Status}}' 2>/dev/null || true +} + +devstack_down() { # stop the shared ingress + the VM (GLOBAL — affects all projects) + DEVSTACK_HOME="$DEVSTACK_HOME" docker compose -f "$(dirname "${BASH_SOURCE[0]}")/../traefik/compose.yml" down 2>/dev/null || true + colima stop -p "$DEVSTACK_PROFILE" || true +} + +devstack_reset() { # GLOBAL destructive — wipes ALL projects' state + echo "WARNING: deletes the shared VM and ALL projects' data on it." + read -r -p "type the profile name to confirm: " c + [ "$c" = "$DEVSTACK_PROFILE" ] && colima delete -p "$DEVSTACK_PROFILE" -f || echo "aborted" +} diff --git a/devstack/lib/config.sh b/devstack/lib/config.sh new file mode 100644 index 000000000..ff9cdf8be --- /dev/null +++ b/devstack/lib/config.sh @@ -0,0 +1,25 @@ +# devstack/lib/config.sh — load/persist devstack config and derive paths. +DEVSTACK_HOME="${DEVSTACK_HOME:-$HOME/.airavata-devstack}" +DEVSTACK_CONF="$DEVSTACK_HOME/config.env" + +devstack_load_config() { + mkdir -p "$DEVSTACK_HOME/certs" "$DEVSTACK_HOME/traefik/dynamic" + [ -f "$DEVSTACK_CONF" ] && . "$DEVSTACK_CONF" + : "${DEVSTACK_PROFILE:=airavata}" + : "${DEVSTACK_PROJECT:=airavata}" + : "${DEVSTACK_TLD:=airavata.host}" + DEVSTACK_SOCK="$HOME/.colima/$DEVSTACK_PROFILE/docker.sock" + export DOCKER_HOST="unix://$DEVSTACK_SOCK" + export DEVSTACK_PROFILE DEVSTACK_PROJECT DEVSTACK_TLD DEVSTACK_SOCK DEVSTACK_HOME +} + +devstack_save_config() { + cat > "$DEVSTACK_CONF" <<EOF +DEVSTACK_PROFILE=$DEVSTACK_PROFILE +DEVSTACK_PROJECT=$DEVSTACK_PROJECT +DEVSTACK_TLD=$DEVSTACK_TLD +EOF + echo "wrote $DEVSTACK_CONF" +} + +devstack_os() { case "$(uname -s)" in Darwin) echo macos;; Linux) echo linux;; *) echo unknown;; esac; } diff --git a/devstack/lib/dns.sh b/devstack/lib/dns.sh new file mode 100644 index 000000000..17561bbc2 --- /dev/null +++ b/devstack/lib/dns.sh @@ -0,0 +1,52 @@ +# devstack/lib/dns.sh — map *.<tld> -> 127.0.0.1 (host-aware). Run from `setup` (sudo). +_dnsmasq_conf_macos() { + local prefix; prefix="$(brew --prefix)" + local d="$prefix/etc/dnsmasq.d"; mkdir -p "$d" + # per-project drop-in (never edits the shared main conf) + printf 'address=/%s/127.0.0.1\n' "$DEVSTACK_TLD" > "$d/$DEVSTACK_TLD.conf" + grep -q "conf-dir=$d" "$prefix/etc/dnsmasq.conf" 2>/dev/null || \ + echo "conf-dir=$d,*.conf" >> "$prefix/etc/dnsmasq.conf" + sudo brew services restart dnsmasq + sudo mkdir -p /etc/resolver + printf 'nameserver 127.0.0.1\n' | sudo tee "/etc/resolver/$DEVSTACK_TLD" >/dev/null + # scrub any stale apex line in /etc/hosts so nsswitch 'files' can't shadow it + if grep -qE "[[:space:]]$DEVSTACK_TLD($|[[:space:]])" /etc/hosts; then + sudo sed -i '' "/[[:space:]]$DEVSTACK_TLD\$/d" /etc/hosts || true + fi + sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder 2>/dev/null || true +} + +# Linux: detect the DNS manager and apply the matching recipe (-> 127.0.0.1). +_dnsmasq_conf_linux() { + local d=/etc/dnsmasq.d; sudo mkdir -p "$d" + printf 'address=/%s/127.0.0.1\n' "$DEVSTACK_TLD" | sudo tee "$d/$DEVSTACK_TLD.conf" >/dev/null + if grep -q '^dns=dnsmasq' /etc/NetworkManager/NetworkManager.conf 2>/dev/null; then + # NetworkManager's built-in dnsmasq plugin owns resolution. + sudo mkdir -p /etc/NetworkManager/dnsmasq.d + printf 'address=/%s/127.0.0.1\n' "$DEVSTACK_TLD" | \ + sudo tee "/etc/NetworkManager/dnsmasq.d/$DEVSTACK_TLD.conf" >/dev/null + sudo systemctl reload NetworkManager + elif systemctl is-active --quiet systemd-resolved; then + # split-DNS: dnsmasq on a dedicated loopback; route only this TLD to it. + sudo sed -i 's/^#\?listen-address=.*/listen-address=127.0.0.2/' /etc/dnsmasq.conf 2>/dev/null || \ + echo 'listen-address=127.0.0.2' | sudo tee -a /etc/dnsmasq.conf >/dev/null + grep -q '^bind-interfaces' /etc/dnsmasq.conf || echo 'bind-interfaces' | sudo tee -a /etc/dnsmasq.conf >/dev/null + sudo systemctl restart dnsmasq + sudo mkdir -p /etc/systemd/resolved.conf.d + printf '[Resolve]\nDNS=127.0.0.2\nDomains=~%s\n' "$DEVSTACK_TLD" | \ + sudo tee "/etc/systemd/resolved.conf.d/$DEVSTACK_TLD.conf" >/dev/null + sudo systemctl restart systemd-resolved + else + echo "WARN: unknown Linux DNS manager; add 'address=/$DEVSTACK_TLD/127.0.0.1' to dnsmasq and restart it manually" >&2 + fi + # scrub stale apex /etc/hosts line + sudo sed -i "/[[:space:]]$DEVSTACK_TLD\$/d" /etc/hosts 2>/dev/null || true +} + +devstack_dns() { + case "$(devstack_os)" in + macos) _dnsmasq_conf_macos ;; + linux) _dnsmasq_conf_linux ;; + *) echo "unsupported OS" >&2; exit 1 ;; + esac +} diff --git a/devstack/lib/ingress.sh b/devstack/lib/ingress.sh new file mode 100644 index 000000000..bffeaa1eb --- /dev/null +++ b/devstack/lib/ingress.sh @@ -0,0 +1,38 @@ +# devstack/lib/ingress.sh — shared Traefik bring-up + per-project registration. +INGRESS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../traefik" && pwd)" + +_dc() { DEVSTACK_HOME="$DEVSTACK_HOME" docker compose -f "$INGRESS_DIR/compose.yml" "$@"; } + +ingress_up() { + cp "$INGRESS_DIR/traefik.yml" "$DEVSTACK_HOME/traefik/traefik.yml" + # default cert for no-SNI / unknown-SNI clients + cat > "$DEVSTACK_HOME/traefik/dynamic/_tls-default.yml" <<EOF +tls: + stores: + default: + defaultCertificate: + certFile: /certs/$DEVSTACK_TLD.pem + keyFile: /certs/$DEVSTACK_TLD-key.pem +EOF + _dc up -d +} + +ingress_register_project() { + # 1) per-project cert entry (additive; never edits another project's file) + cat > "$DEVSTACK_HOME/traefik/dynamic/$DEVSTACK_PROJECT.yml" <<EOF +tls: + certificates: + - certFile: /certs/$DEVSTACK_TLD.pem + keyFile: /certs/$DEVSTACK_TLD-key.pem +EOF + # 2) project network (idempotent) + connect Traefik to it WITH the dev hostname + # aliases, so in-VM containers resolve <name>.$DEVSTACK_TLD to Traefik via + # Docker's embedded DNS (server -> auth.airavata.host, portal -> auth/api, ...). + docker network inspect "$DEVSTACK_PROJECT-devstack" >/dev/null 2>&1 || \ + docker network create --attachable "$DEVSTACK_PROJECT-devstack" + docker network connect \ + --alias "api.$DEVSTACK_TLD" --alias "auth.$DEVSTACK_TLD" \ + --alias "gateway.$DEVSTACK_TLD" --alias "rabbitmq.$DEVSTACK_TLD" \ + --alias "adminer.$DEVSTACK_TLD" \ + "$DEVSTACK_PROJECT-devstack" airavata-devstack-traefik 2>/dev/null || true +} diff --git a/devstack/lib/verify.sh b/devstack/lib/verify.sh new file mode 100644 index 000000000..9b7e4d36b --- /dev/null +++ b/devstack/lib/verify.sh @@ -0,0 +1,13 @@ +# devstack/lib/verify.sh — hard-fail gate run at the end of ensure. +devstack_verify() { + local host="api.$DEVSTACK_TLD" ok=1 + case "$(devstack_os)" in + macos) + dscacheutil -q host -a name "$host" | grep -q '127.0.0.1' || { echo "DNS FAIL: $host !-> 127.0.0.1 (use dscacheutil, not dig)"; ok=0; } ;; + linux) + getent hosts "$host" | grep -q '127.0.0.1' || { echo "DNS FAIL: $host !-> 127.0.0.1"; ok=0; } ;; + esac + nc -z -w2 127.0.0.1 443 || { echo "INGRESS FAIL: nothing on 127.0.0.1:443"; ok=0; } + [ "$ok" = 1 ] && echo "devstack verify: OK ($host -> 127.0.0.1, Traefik :443 up)" + [ "$ok" = 1 ] +} diff --git a/devstack/traefik/compose.yml b/devstack/traefik/compose.yml new file mode 100644 index 000000000..ef1c549b7 --- /dev/null +++ b/devstack/traefik/compose.yml @@ -0,0 +1,29 @@ +# Shared substrate ingress — ONE instance for all projects. Operates on ~/.airavata-devstack. +# Bound to 127.0.0.1 only. Cert + router fragments are dropped into traefik/dynamic by ensure. +name: airavata-devstack-ingress +services: + proxy: + image: tecnativa/docker-socket-proxy:0.3.0 + container_name: airavata-devstack-proxy + environment: { CONTAINERS: 1, NETWORKS: 1, SERVICES: 1, TASKS: 1, ENDPOINTS: 1, INFO: 1 } + volumes: [ "/var/run/docker.sock:/var/run/docker.sock:ro" ] + networks: [ ingress ] + restart: unless-stopped + traefik: + # v3.7+ negotiates the Docker API version. v3.3 was stuck at the ancient default 1.24, + # which colima's daemon (min API 1.40) rejects, breaking the docker provider entirely. + # Do NOT downgrade below v3.7. + image: traefik:v3.7 + container_name: airavata-devstack-traefik + depends_on: [ proxy ] + ports: [ "127.0.0.1:80:80", "127.0.0.1:443:443" ] + volumes: + - "${DEVSTACK_HOME}/traefik/traefik.yml:/etc/traefik/traefik.yml:ro" + - "${DEVSTACK_HOME}/traefik/dynamic:/etc/traefik/dynamic:ro" + - "${DEVSTACK_HOME}/certs:/certs:ro" + networks: [ ingress ] + restart: unless-stopped +networks: + ingress: + name: airavata-devstack-ingress + attachable: true diff --git a/devstack/traefik/traefik.yml b/devstack/traefik/traefik.yml new file mode 100644 index 000000000..e35cb9f11 --- /dev/null +++ b/devstack/traefik/traefik.yml @@ -0,0 +1,18 @@ +# Shared Traefik static config (staged to ~/.airavata-devstack/traefik/traefik.yml). +# Entrypoints listen on ALL container interfaces (:80/:443); the host-side +# 127.0.0.1 restriction lives in compose's `ports:` mapping, not here. +entryPoints: + web: + address: ":80" + websecure: + address: ":443" + transport: + respondingTimeouts: + readTimeout: 0 # keep long-lived gRPC (agent bidi) streams alive — spec R8 +providers: + file: + directory: /etc/traefik/dynamic + watch: true + docker: + endpoint: "tcp://airavata-devstack-proxy:2375" + exposedByDefault: false
