diff --git a/.github/actions/encrypt-env/README.md b/.github/actions/encrypt-env/README.md index 482da85..c0766e6 100644 --- a/.github/actions/encrypt-env/README.md +++ b/.github/actions/encrypt-env/README.md @@ -33,8 +33,8 @@ asset: mainframe.sops.env keys: - mainframe env: - APPS_DOMAIN: APPS_DOMAIN # output name: GitHub Secret/Variable name - APPS_TIMEZONE: APPS_TIMEZONE + DOMAIN: DOMAIN # output name: GitHub Secret/Variable name + TIMEZONE: TIMEZONE ``` For each name in `keys`, it loads `/.pub`. Secrets take precedence over Variables when both contain the same source key. Every source key must resolve or the action fails. The manifest has no `apps` field — app selection lives on the deploy target, not the vault; see the main README's "Vaults And Targets" section. diff --git a/AGENTS.md b/AGENTS.md index f0136cc..a4ee7ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -109,35 +109,53 @@ Compose files mount the rendered file by its plain relative path (`./traefik.yml When adding config for a new app: only reach for a template if the image has no env-var-driven config path at all. If it does (most well-behaved images do), prefer plain `environment:` entries over a template - fewer moving parts, and the value never touches disk as a separate file. -### Per-app and per-server overrides +### App-specific vs. shared variables -Compose files explicitly declare which variables are overridable using bash fallback syntax: +Inside a `docker-compose.yml`/`*.tpl` file, variable names are always bare - never prefixed with the app's own name. Each app already has its own compose file and its own generated `.env`, so an app-name prefix inside it would disambiguate nothing; `${PUBLIC_KEY}` in `apps/beszel-agent/docker-compose.yml` and `${DOMAIN}` shared across a dozen apps look exactly the same from inside the file, because the file is already scoped to one app. -```yaml -# App-specific override, falls back to server-wide value -SHOWS_PATH: ${JELLYFIN_SHOWS_PATH:-${APPS_SHOWS_PATH}} +What makes a variable "shared" vs. "app-specific" is a fact about the *vault*, not the compose file: whether multiple apps' vaults map the same variable name to the same GitHub Secret/Variable, or only one app's vault ever references it at all. -# App-specific only — must be set per app -SHOWS_PATH: ${JELLYFIN_SHOWS_PATH} +App-name prefixing happens on the *other* side of a vault manifest's `env:` mapping - the GitHub Secret/Variable name - and only as a judgment call when it helps a human scanning a flat list of a target's Secrets/Variables tell which app a value belongs to (e.g. `TRAEFIK_HTTP_PORT` keeps its prefix because a bare `HTTP_PORT` wouldn't self-document; `ADMIN_MAIL` doesn't need one because it doesn't need explaining). See "Vaults And Targets" in README for the manifest format. -# Server-wide — same value for all apps on this server -SHOWS_PATH: ${APPS_SHOWS_PATH} -``` +There's no per-app override mechanism (a bash-fallback `${APPNAME_VAR:-${VAR}}` pattern existed here before and was removed - nothing in the catalog ever used it). If an app genuinely needs a value another app also uses but with a different value, give it its own distinctly-named variable instead - not a namespaced variant of the same name. + +Name a variable by what it *is*, never by its format (`SESSION_KEY`, not `KEY_HEX_32`) - a format-shaped name invites treating same-format secrets as interchangeable across apps when they aren't. This matters concretely for keys: a **session-signing key** (Better Auth, Django's `SECRET_KEY` - only ever used to sign/verify sessions and CSRF tokens) is safe to genuinely share across apps, since compromising it just forces every app's active sessions to re-authenticate - so it gets one shared name (`SESSION_KEY`) and one shared vault mapping. An **encryption key** (Laravel's `APP_KEY`, Semaphore's access-key encryption - used to encrypt stored data) must never be shared, since compromising or rotating it can make one app's already-stored ciphertext unrecoverable, and that risk shouldn't leak to a second app sharing the same key. Give each app's encryption key the same semantic app-side name (`ENCRYPTION_KEY`) so its role is still obvious from the compose file, but map it to a distinct, app-prefixed GitHub Secret per app (`TWOFAUTH_ENCRYPTION_KEY`, `SEMAPHORE_ENCRYPTION_KEY`) so the values themselves are never shared. + +Common shared variables a target's vaults map into one or more apps' `.env`: + +- `DOMAIN` - Base domain for all services +- `CERTIFICATE_RESOLVER` - SSL resolver (Cloudflare DNS or HTTP challenge) +- `DATABASE_PASSWORD` - Shared database password +- `SESSION_KEY` - Shared session-signing key (safe to share - see above) +- `TIMEZONE` - System timezone +- `TELEGRAM_TOKEN`, `TELEGRAM_CHAT` - Shared Telegram bot for app-originated alerts + +### Vault-Sourced vs. Internal Environment -Naming convention: +An app's `environment:` mix two different kinds of values: ones that must come from its vault (secrets, domain, feature flags - anything a deploy actually configures) and ones that are fixed or purely derived from `${APP_NAME}` (service hostnames, ports, internal db/user names - values that never change across deploys). Keeping them in one flat `x-environment` block makes it impossible to tell, at a glance, what an app actually needs from its vault. + +When an app has both kinds, split into two anchors instead of one, declared in this order: + +```yaml +x-vault-env: &vault-env + BASE_URL: https://${APP_NAME}.${DOMAIN} + BETTER_AUTH_SECRET: ${SESSION_KEY} +x-internal-env: &internal-env + NODE_ENV: production + POSTGRES_HOST: postgres +services: + backend: + environment: + <<: [*vault-env, *internal-env] +``` -- `{APPNAME}_{VAR}` — app-specific variable where `{APPNAME}` is the uppercased app directory with hyphens replaced by underscores (e.g. `JELLYFIN_SHOWS_PATH`, `BESZEL_AGENT_PUBLIC_KEY`, `TWOFAUTH_APPS_DOMAIN`) -- `APPS_{VAR}` — server-wide variable shared across apps (e.g. `APPS_DOMAIN`, `APPS_TIMEZONE`) +Classification rule: a variable goes in `x-vault-env` if its value references *any* vault-sourced variable, even mixed with `${APP_NAME}` or literal text (e.g. `https://${APP_NAME}.${DOMAIN}` is vault-env, because it can't resolve to anything meaningful without `DOMAIN`). It goes in `x-internal-env` if its value is a hardcoded literal or derived only from `${APP_NAME}` - it would be exactly the same regardless of which vault fed the deploy. -The compose file is the source of truth for which overrides are allowed. Not every variable needs an app-specific override — only declare one when you actually want to allow it. +If an app's env is entirely one kind, use a single anchor named for that kind (`x-vault-env` or `x-internal-env`) instead of the generic `x-environment` - don't split into two just to leave one empty. No section comments above the anchors - the names are meant to be self-explanatory. -Common `APPS_*` variables a target's vaults map into one or more apps' `.env`: +`docker compose`'s multi-anchor merge key (`<<: [*a, *b]`) is what makes this work; verified it resolves identically to a single flat block via `docker compose config`. -- `APPS_DOMAIN` - Base domain for all services -- `APPS_CERTIFICATE_RESOLVER` - SSL resolver (Cloudflare DNS or HTTP challenge) -- `APPS_DATABASE_PASSWORD` - Shared database password -- `APPS_KEY_HEX_{16,32,64}` - Encryption keys for various apps -- `APPS_TIMEZONE` - System timezone +The same split applies to non-`environment` fields too, whenever an app has vault-configurable values there - e.g. `apps/beszel-agent/docker-compose.yml`'s `devices:` list is vault-sourced (`DISK_1_DEVICE`/`DISK_2_DEVICE`, each defaulting to `/dev/null` when unset), so it's declared as `x-vault-devices: &vault-devices` and referenced with `devices: *vault-devices`. A YAML anchor isn't limited to a map - it can hold a list just as well. Name the anchor for the compose field it targets (`x-vault-{field}`) so it's still obvious at a glance which fields the app's vault interface actually touches. ### Traefik Integration @@ -145,12 +163,12 @@ All apps use Traefik labels pattern: ```yaml traefik.enable=true -traefik.http.routers.${APP_NAME}.rule=Host(`${APP_NAME}.${APPS_DOMAIN}`) -traefik.http.routers.${APP_NAME}.tls.certresolver=${APPS_CERTIFICATE_RESOLVER} +traefik.http.routers.${APP_NAME}.rule=Host(`${APP_NAME}.${DOMAIN}`) +traefik.http.routers.${APP_NAME}.tls.certresolver=${CERTIFICATE_RESOLVER} traefik.http.services.${APP_NAME}.loadbalancer.server.port=8080 ``` -Apps are accessible at `{app-name}.{APPS_DOMAIN}` with automatic SSL. +Apps are accessible at `{app-name}.{DOMAIN}` with automatic SSL. ## Operations @@ -230,7 +248,7 @@ x-image: &image # 3. X-ENVIRONMENT (only if environment variables exist) x-environment: &environment VAR1: ${VALUE1} - DATABASE_URL: postgresql://postgres:${APPS_DATABASE_PASSWORD}@postgres:5432/${APP_NAME} + DATABASE_URL: postgresql://postgres:${DATABASE_PASSWORD}@postgres:5432/${APP_NAME} # 4. X-VOLUMES (if multiple services share volumes) x-volumes: &volumes @@ -258,7 +276,7 @@ services: command: ["start", "--config", "/config.yml"] # 4. USER (if required) - user: "${APPS_UID}:${APPS_GID}" + user: "${PUID}:${PGID}" # not UID/GID - those are shell-reserved # 5. ENVIRONMENT (mandatory, via anchor) environment: *environment @@ -293,11 +311,11 @@ services: ### Key ordering principles: 1. **Include order**: networks.yml → database/cache templates (postgres/redis/mongodb/etc., pick a version) → others -2. **X-fields order**: x-image → x-environment → x-volumes (only if needed) +2. **X-fields order**: x-image → all `x-vault-*` anchors together → all `x-internal-*` anchors together → x-volumes (only if needed). Vault-sourced anchors are grouped first regardless of which compose field they target (e.g. `x-vault-env` then `x-vault-devices`, both before `x-internal-env`) - see "Vault-Sourced vs. Internal Environment" above 3. **X-image for shared images** - if multiple services use the same image, use `x-image: &image` 4. **X-volumes for shared volumes** - if 2+ volumes repeat across services, extract them to `x-volumes: &volumes` and merge with unique ones 5. **Image before extends** - declare what image is used, then extend common config -6. **Environment via anchor** - always use `x-environment: &environment` pattern +6. **Environment via anchor** - always declare env vars in an anchor, referenced with `environment: *name` (or merged via `<<: [*vault-env, *internal-env]` when split) - never inline a service's `environment:` map directly 7. **Networks from extends** - `main`, `main-http`, and `api` profiles include `traefik` and `internal`; never add `databases` (it's only for DB admin tools) 8. **Depends_on as simple list** - use array format without `condition:`, healthchecks are in common.yml 9. **Depends_on order**: postgres → redis → mongo → app services @@ -324,7 +342,7 @@ include: - ../networks.yml - ../postgres-18.yml x-environment: &environment - DATABASE_URL: postgresql://postgres:${APPS_DATABASE_PASSWORD}@postgres:5432/${APP_NAME} + DATABASE_URL: postgresql://postgres:${DATABASE_PASSWORD}@postgres:5432/${APP_NAME} ENABLE_FEATURE: true PORT: 8080 services: @@ -334,7 +352,7 @@ services: file: ../common.yml service: main command: ["worker", "--concurrency", "10"] - user: "${APPS_UID}:${APPS_GID}" + user: "${PUID}:${PGID}" # not UID/GID - those are shell-reserved environment: *environment volumes: - ../../apps-data/${APP_NAME}/data:/data diff --git a/README.md b/README.md index fd0eeff..1710c4e 100644 --- a/README.md +++ b/README.md @@ -97,16 +97,9 @@ This catalog is itself published as its own release asset (`flightdeck-apps.zip` ### Environment Variables -Every app's env comes from its own vault(s), declared in that target's manifest (see "Vaults And Targets" below). A vault declares the exact final variable names an app receives, mapped to GitHub Secret/Variable names - there is no server-side prefix filtering or shared root env file. Two apps' vaults can share a source secret (e.g. both mapping `APPS_DOMAIN`) without conflict, since each app ends up with its own separate `.env`. +Every app's env comes from its own vault(s), declared in that target's manifest (see "Vaults And Targets" below). A vault declares the exact final variable names an app receives, mapped to GitHub Secret/Variable names - there is no server-side prefix filtering or shared root env file. Two apps' vaults can share a source secret (e.g. both mapping `DOMAIN`) without conflict, since each app ends up with its own separate `.env`. -**Per-app overrides** are declared directly in each app's `docker-compose.yml` using bash fallback syntax: - -```yaml -# App-specific override, falls back to server-wide value -SOME_PATH: ${MYAPP_SOME_PATH:-${APPS_SOME_PATH}} -``` - -To use the override for a given deploy, that app's own vault sets `MYAPP_SOME_PATH` in its `env:` mapping; to fall back to the shared value, the vault just omits it and relies on `APPS_SOME_PATH` alone. App prefixes are the uppercased app directory with hyphens replaced by underscores. See `AGENTS.md` for the full naming convention. +Variable names inside a compose file are always bare, never prefixed with the app's own name - each compose file is already scoped to one app. Whether a name is "shared" or app-specific only matters on the vault side (whether more than one app's vault maps it). See `AGENTS.md` for the full naming convention. ### Network Architecture @@ -214,7 +207,7 @@ asset: mainframe-rybbit.sops.env keys: - mainframe env: - APPS_DOMAIN: MAINFRAME_DOMAIN + DOMAIN: MAINFRAME_DOMAIN ``` `targets/mainframe.yml`: @@ -243,7 +236,7 @@ credentials: sops_age_key: MAINFRAME_AGE_PRIVATE_KEY ``` -Credential fields contain GitHub Variable/Secret names, never credential values. `app_refs` and `hosts` are YAML arrays; `apps` is a mapping from app name to that app's own `env_refs` array. Each host uses the SSH `user@host` format. `app_refs` must list at least one app bundle — flightdeck's own `apps/` catalog is just another entry, not implicit. Each app in `apps` must list at least one `env_refs` entry; `deploy/deploy.py` decrypts and concatenates all of an app's sources into that app's own `.env` on the runner, failing loud on any key collision — but only within that one app's own sources. Two different apps' vaults sharing a key (e.g. both declaring `APPS_DOMAIN`) is expected, since each app gets a separate `.env`. `credentials.secrets.sops_age_key` names the GitHub Secret holding this target's *private* age key — the one used to decrypt its vaults, matching the public key in `keys/.pub` used to encrypt them. +Credential fields contain GitHub Variable/Secret names, never credential values. `app_refs` and `hosts` are YAML arrays; `apps` is a mapping from app name to that app's own `env_refs` array. Each host uses the SSH `user@host` format. `app_refs` must list at least one app bundle — flightdeck's own `apps/` catalog is just another entry, not implicit. Each app in `apps` must list at least one `env_refs` entry; `deploy/deploy.py` decrypts and concatenates all of an app's sources into that app's own `.env` on the runner, failing loud on any key collision — but only within that one app's own sources. Two different apps' vaults sharing a key (e.g. both declaring `DOMAIN`) is expected, since each app gets a separate `.env`. `credentials.secrets.sops_age_key` names the GitHub Secret holding this target's *private* age key — the one used to decrypt its vaults, matching the public key in `keys/.pub` used to encrypt them. `load-yaml-matrix` reads every file in `vaults/` or `targets/` into a matrix — it does not validate the manifest shape. Each manifest's fields are the responsibility of whatever consumes them: `encrypt-env` re-parses and validates its own manifest from `manifest`, and the workflows calling `deploy-shared.yml` apply `path`/`keep-releases` defaults and pull `credentials.secrets`/`credentials.variables` values directly from the matrix item. diff --git a/apps/beszel-agent/docker-compose.yml b/apps/beszel-agent/docker-compose.yml index 5fe191f..865d6c9 100644 --- a/apps/beszel-agent/docker-compose.yml +++ b/apps/beszel-agent/docker-compose.yml @@ -1,7 +1,10 @@ -x-environment: &environment +x-vault-env: &vault-env + KEY: ${PUBLIC_KEY} +x-vault-devices: &vault-devices + - ${DISK_1_DEVICE:-/dev/null}:${DISK_1_DEVICE:-/dev/null} + - ${DISK_2_DEVICE:-/dev/null}:${DISK_2_DEVICE:-/dev/null} +x-internal-env: &internal-env PORT: 45876 - HEALTHCHECK_CMD: "nc -z 127.0.0.1 $${PORT} || exit 1" - KEY: ${BESZEL_AGENT_PUBLIC_KEY} services: beszel-agent: image: "henrygd/beszel-agent:alpine" @@ -11,13 +14,12 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - /home:/extra-filesystems/home:ro - environment: *environment + environment: + <<: [*vault-env, *internal-env] cap_add: - SYS_RAWIO # required for S.M.A.R.T. data - SYS_ADMIN # required for NVMe S.M.A.R.T. data - devices: - - ${BESZEL_AGENT_DISK_1_DEVICE:-/dev/null}:${BESZEL_AGENT_DISK_1_DEVICE:-/dev/null} - - ${BESZEL_AGENT_DISK_2_DEVICE:-/dev/null}:${BESZEL_AGENT_DISK_2_DEVICE:-/dev/null} + devices: *vault-devices healthcheck: test: ["CMD", "/agent", "health"] start_period: 30s diff --git a/apps/clickhouse-26.5.yml b/apps/clickhouse-26.5.yml index ead2d2f..99d7d2f 100644 --- a/apps/clickhouse-26.5.yml +++ b/apps/clickhouse-26.5.yml @@ -5,7 +5,7 @@ services: environment: CLICKHOUSE_DB: ${APP_NAME} CLICKHOUSE_USER: ${APP_NAME} - CLICKHOUSE_PASSWORD: ${APPS_DATABASE_PASSWORD} + CLICKHOUSE_PASSWORD: ${DATABASE_PASSWORD} CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1 volumes: - ../apps-data/${APP_NAME}/clickhouse/data:/var/lib/clickhouse diff --git a/apps/codecov/codecov.yml.tpl b/apps/codecov/codecov.yml.tpl index ed59516..92de082 100644 --- a/apps/codecov/codecov.yml.tpl +++ b/apps/codecov/codecov.yml.tpl @@ -1,37 +1,37 @@ setup: # Replace with the http location of your Codecov # https://docs.codecov.io/docs/configuration#section-codecov-url - codecov_url: https://${APP_NAME}.${CODECOV_APPS_DOMAIN:-${APPS_DOMAIN}} + codecov_url: https://${APP_NAME}.${DOMAIN} # codecov_api_url: # this defaults to and is designed to work out of the box like this # api_allowed_hosts: [] # this defaults to and is designed to work out of the box like this # Replace with your Codecov Enterprise License key. This is required for the containers to function. # https://docs.codecov.io/docs/configuration#section-enterprise-license - enterprise_license: ${CODECOV_LICENSE} + enterprise_license: ${LICENSE} admins: # https://docs.codecov.com/docs/configuration#instance-wide-admins - service: github - username: ${CODECOV_ADMIN_GITHUB_USERNAME} + username: ${ADMIN_GITHUB_USERNAME} http: - cookie_secret: ${APPS_KEY_HEX_32} # Replace it with a random string - cookies_domain: ${APP_NAME}.${CODECOV_APPS_DOMAIN:-${APPS_DOMAIN}} + cookie_secret: ${SESSION_KEY} # Replace it with a random string + cookies_domain: ${APP_NAME}.${DOMAIN} timeseries: enabled: true guest_access: "off" github: - client_id: ${CODECOV_GITHUB_CLIENT_ID} - client_secret: ${CODECOV_GITHUB_CLIENT_SECRET} - webhook_secret: ${CODECOV_GITHUB_WEBHOOK_SECRET} + client_id: ${GITHUB_CLIENT_ID} + client_secret: ${GITHUB_CLIENT_SECRET} + webhook_secret: ${GITHUB_WEBHOOK_SECRET} integration: - id: ${CODECOV_GITHUB_APP_ID} + id: ${GITHUB_APP_ID} pem: /config/key.pem services: redis_url: "redis://redis:6379" - database_url: "postgres://${APP_NAME}:${APPS_DATABASE_PASSWORD}@postgres:5432/${APP_NAME}" - timeseries_database_url: "postgres://${APP_NAME}:${APPS_DATABASE_PASSWORD}@timescale:5432/${APP_NAME}" + database_url: "postgres://${APP_NAME}:${DATABASE_PASSWORD}@postgres:5432/${APP_NAME}" + timeseries_database_url: "postgres://${APP_NAME}:${DATABASE_PASSWORD}@timescale:5432/${APP_NAME}" minio: - host: ${APPS_S3_HOST} - bucket: ${CODECOV_S3_BUCKET} - region: ${APPS_S3_REGION} + host: ${S3_HOST} + bucket: ${S3_BUCKET} + region: ${S3_REGION} verify_ssl: true port: 443 - access_key_id: ${APPS_S3_ACCESS_KEY_ID} - secret_access_key: ${APPS_S3_SECRET_ACCESS_KEY} + access_key_id: ${S3_ACCESS_KEY_ID} + secret_access_key: ${S3_SECRET_ACCESS_KEY} diff --git a/apps/codecov/docker-compose.yml b/apps/codecov/docker-compose.yml index 6455b74..60b9aa4 100644 --- a/apps/codecov/docker-compose.yml +++ b/apps/codecov/docker-compose.yml @@ -3,10 +3,11 @@ include: - ../redis-7.yml - ../postgres-17.yml - ../timescale-17.yml -x-environment: &environment - CODECOV_BASE_HOST: ${APP_NAME}.${CODECOV_APPS_DOMAIN:-${APPS_DOMAIN}} - CODECOV_API_HOST: ${APP_NAME}.${CODECOV_APPS_DOMAIN:-${APPS_DOMAIN}} - CODECOV_IA_HOST: ${APP_NAME}.${CODECOV_APPS_DOMAIN:-${APPS_DOMAIN}} +x-vault-env: &vault-env + CODECOV_BASE_HOST: ${APP_NAME}.${DOMAIN} + CODECOV_API_HOST: ${APP_NAME}.${DOMAIN} + CODECOV_IA_HOST: ${APP_NAME}.${DOMAIN} +x-internal-env: &internal-env CODECOV_SCHEME: https RUN_ENV: ENTERPRISE x-volumes: &volumes @@ -21,7 +22,7 @@ services: - 8080 labels: - "traefik.http.services.${APP_NAME}.loadbalancer.server.port=8080" - - "traefik.http.routers.${APP_NAME}.rule=Host(`${APP_NAME}.${CODECOV_APPS_DOMAIN:-${APPS_DOMAIN}}`)" + - "traefik.http.routers.${APP_NAME}.rule=Host(`${APP_NAME}.${DOMAIN}`)" depends_on: - api - frontend @@ -31,7 +32,8 @@ services: extends: file: ../common.yml service: side - environment: *environment + environment: + <<: [*vault-env, *internal-env] volumes: *volumes expose: - "8080" @@ -54,7 +56,8 @@ services: - redis - postgres - timescale - environment: *environment + environment: + <<: [*vault-env, *internal-env] volumes: - ./codecov.yml:/config/codecov.yml - ../../apps-data/${APP_NAME}/archive:/archive diff --git a/apps/common.yml b/apps/common.yml index 0e2b830..1c80104 100644 --- a/apps/common.yml +++ b/apps/common.yml @@ -3,26 +3,26 @@ x-labels: &labels - "traefik.docker.network=traefik" - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" - "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=false" - - "traefik.http.routers.${APP_NAME}-http.rule=Host(`${APP_NAME}.${APPS_DOMAIN}`)" + - "traefik.http.routers.${APP_NAME}-http.rule=Host(`${APP_NAME}.${DOMAIN}`)" - "traefik.http.routers.${APP_NAME}-http.entrypoints=http" - "traefik.http.routers.${APP_NAME}-http.middlewares=redirect-to-https" - - "traefik.http.routers.${APP_NAME}.rule=Host(`${APP_NAME}.${APPS_DOMAIN}`)" + - "traefik.http.routers.${APP_NAME}.rule=Host(`${APP_NAME}.${DOMAIN}`)" - "traefik.http.routers.${APP_NAME}.entrypoints=https" - - "traefik.http.routers.${APP_NAME}.tls.certresolver=${APPS_CERTIFICATE_RESOLVER}" + - "traefik.http.routers.${APP_NAME}.tls.certresolver=${CERTIFICATE_RESOLVER}" x-labels-http: &labels-http - "traefik.enable=true" - "traefik.docker.network=traefik" - - "traefik.http.routers.${APP_NAME}.rule=Host(`${APP_NAME}.${APPS_DOMAIN}`)" + - "traefik.http.routers.${APP_NAME}.rule=Host(`${APP_NAME}.${DOMAIN}`)" - "traefik.http.routers.${APP_NAME}.entrypoints=https" - - "traefik.http.routers.${APP_NAME}.tls.certresolver=${APPS_CERTIFICATE_RESOLVER}" - - "traefik.http.routers.${APP_NAME}-plain.rule=Host(`${APP_NAME}.${APPS_DOMAIN}`)" + - "traefik.http.routers.${APP_NAME}.tls.certresolver=${CERTIFICATE_RESOLVER}" + - "traefik.http.routers.${APP_NAME}-plain.rule=Host(`${APP_NAME}.${DOMAIN}`)" - "traefik.http.routers.${APP_NAME}-plain.entrypoints=http" x-labels-api: &labels-api - traefik.enable=true - traefik.docker.network=traefik - - traefik.http.routers.${APP_NAME}-api.rule=Host(`${APP_NAME}.${APPS_DOMAIN}`) && PathPrefix(`/api`) + - traefik.http.routers.${APP_NAME}-api.rule=Host(`${APP_NAME}.${DOMAIN}`) && PathPrefix(`/api`) - traefik.http.routers.${APP_NAME}-api.entrypoints=https - - traefik.http.routers.${APP_NAME}-api.tls.certresolver=${APPS_CERTIFICATE_RESOLVER} + - traefik.http.routers.${APP_NAME}-api.tls.certresolver=${CERTIFICATE_RESOLVER} x-env-file: &env-file - ./${APP_NAME}/.env x-restart: &restart unless-stopped diff --git a/apps/gatus/docker-compose.yml b/apps/gatus/docker-compose.yml index 0cd939a..bfe0d2f 100644 --- a/apps/gatus/docker-compose.yml +++ b/apps/gatus/docker-compose.yml @@ -1,14 +1,15 @@ include: - ../networks.yml - ../postgres-17.yml -x-environment: &environment +x-vault-env: &vault-env + TZ: ${TIMEZONE} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + TELEGRAM_TOKEN: ${TELEGRAM_TOKEN} + TELEGRAM_ID: ${TELEGRAM_CHAT} +x-internal-env: &internal-env GATUS_CONFIG_PATH: /config/yml - TZ: ${APPS_TIMEZONE} POSTGRES_USER: ${APP_NAME} POSTGRES_DB: ${APP_NAME} - POSTGRES_PASSWORD: ${APPS_DATABASE_PASSWORD} - TELEGRAM_TOKEN: ${GATUS_TELEGRAM_TOKEN:-${APPS_TELEGRAM_TOKEN}} - TELEGRAM_ID: ${GATUS_TELEGRAM_CHAT:-${APPS_TELEGRAM_CHAT}} services: gatus: image: twinproduction/gatus @@ -19,7 +20,8 @@ services: - 8080 labels: - "traefik.http.services.${APP_NAME}.loadbalancer.server.port=8080" - environment: *environment + environment: + <<: [*vault-env, *internal-env] volumes: - ../../apps-data/${APP_NAME}/config:/config - ./config/global.yml:/config/yml/global.yml diff --git a/apps/glitchtip/docker-compose.yml b/apps/glitchtip/docker-compose.yml index d1770c9..d4fff11 100644 --- a/apps/glitchtip/docker-compose.yml +++ b/apps/glitchtip/docker-compose.yml @@ -3,14 +3,15 @@ include: - ../postgres-18.yml - ../redis-8.yml x-image: &image glitchtip/glitchtip -x-environment: &environment - DATABASE_URL: postgresql://${APP_NAME}:${APPS_DATABASE_PASSWORD}@postgres:5432/${APP_NAME} +x-vault-env: &vault-env + DATABASE_URL: postgresql://${APP_NAME}:${DATABASE_PASSWORD}@postgres:5432/${APP_NAME} + SECRET_KEY: ${SESSION_KEY} + EMAIL_URL: ${EMAIL_URL:-consolemail://} + GLITCHTIP_DOMAIN: https://${APP_NAME}.${DOMAIN} + DEFAULT_FROM_EMAIL: ${DEFAULT_FROM_EMAIL:-noreply@${DOMAIN}} +x-internal-env: &internal-env REDIS_URL: redis://redis:6379 - SECRET_KEY: ${APPS_KEY_HEX_64} PORT: 8000 - EMAIL_URL: ${GLITCHTIP_EMAIL_URL:-consolemail://} - GLITCHTIP_DOMAIN: https://${APP_NAME}.${APPS_DOMAIN} - DEFAULT_FROM_EMAIL: ${GLITCHTIP_DEFAULT_FROM_EMAIL:-noreply@${APPS_DOMAIN}} CELERY_WORKER_AUTOSCALE: "1,3" services: web: @@ -23,7 +24,8 @@ services: labels: - "traefik.http.services.${APP_NAME}.loadbalancer.server.port=8000" command: ["sh", "-c", "./bin/run-migrate.sh && ./bin/start.sh"] - environment: *environment + environment: + <<: [*vault-env, *internal-env] volumes: - ../../apps-data/${APP_NAME}/uploads:/code/uploads depends_on: @@ -35,7 +37,8 @@ services: file: ../common.yml service: side command: ["./bin/run-celery-with-beat.sh"] - environment: *environment + environment: + <<: [*vault-env, *internal-env] volumes: - ../../apps-data/${APP_NAME}/uploads:/code/uploads depends_on: diff --git a/apps/homepage/docker-compose.yml b/apps/homepage/docker-compose.yml index 518b374..fddfcc7 100644 --- a/apps/homepage/docker-compose.yml +++ b/apps/homepage/docker-compose.yml @@ -1,7 +1,7 @@ include: - ../networks.yml -x-environment: &environment - HOMEPAGE_ALLOWED_HOSTS: ${APP_NAME}.${APPS_DOMAIN} +x-vault-env: &vault-env + HOMEPAGE_ALLOWED_HOSTS: ${APP_NAME}.${DOMAIN} services: homepage: image: ghcr.io/gethomepage/homepage @@ -12,6 +12,6 @@ services: - 3000 labels: - "traefik.http.services.${APP_NAME}.loadbalancer.server.port=3000" - environment: *environment + environment: *vault-env volumes: - ../../apps-data/${APP_NAME}/config:/app/config diff --git a/apps/mongodb-8.yml b/apps/mongodb-8.yml index c332a17..69b1c6e 100644 --- a/apps/mongodb-8.yml +++ b/apps/mongodb-8.yml @@ -3,9 +3,9 @@ services: image: bitnami/mongodb restart: unless-stopped environment: - MONGODB_ROOT_PASSWORD: ${APPS_DATABASE_PASSWORD} + MONGODB_ROOT_PASSWORD: ${DATABASE_PASSWORD} MONGODB_USERNAME: ${APP_NAME} - MONGODB_PASSWORD: ${APPS_DATABASE_PASSWORD} + MONGODB_PASSWORD: ${DATABASE_PASSWORD} MONGODB_DATABASE: ${APP_NAME} volumes: - ../apps-data/${APP_NAME}/mongodb:/bitnami/mongodb diff --git a/apps/mysql-8.yml b/apps/mysql-8.yml index 656ac4d..6c8e75e 100644 --- a/apps/mysql-8.yml +++ b/apps/mysql-8.yml @@ -5,8 +5,8 @@ services: environment: MYSQL_DATABASE: ${APP_NAME} MYSQL_USER: ${APP_NAME} - MYSQL_PASSWORD: ${APPS_DATABASE_PASSWORD} - MYSQL_ROOT_PASSWORD: ${APPS_DATABASE_PASSWORD} + MYSQL_PASSWORD: ${DATABASE_PASSWORD} + MYSQL_ROOT_PASSWORD: ${DATABASE_PASSWORD} volumes: - ../apps-data/${APP_NAME}/mysql:/var/lib/mysql healthcheck: diff --git a/apps/paradedb-17.yml b/apps/paradedb-17.yml index 08698c6..544a17d 100644 --- a/apps/paradedb-17.yml +++ b/apps/paradedb-17.yml @@ -5,7 +5,7 @@ services: command: ["postgres", "-c", "log_min_messages=ERROR", "-c", "client_min_messages=ERROR"] environment: POSTGRES_DB: ${APP_NAME} - POSTGRES_PASSWORD: ${APPS_DATABASE_PASSWORD} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} POSTGRES_USER: ${APP_NAME} volumes: - ../apps-data/${APP_NAME}/paradedb:/var/lib/postgresql/data diff --git a/apps/pgvector-17.yml b/apps/pgvector-17.yml index 417b13c..7629fd0 100644 --- a/apps/pgvector-17.yml +++ b/apps/pgvector-17.yml @@ -5,7 +5,7 @@ services: command: ["postgres", "-c", "log_min_messages=ERROR", "-c", "client_min_messages=ERROR"] environment: POSTGRES_DB: ${APP_NAME} - POSTGRES_PASSWORD: ${APPS_DATABASE_PASSWORD} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} POSTGRES_USER: ${APP_NAME} volumes: - ../apps-data/${APP_NAME}/pgvector:/var/lib/postgresql/data diff --git a/apps/postgres-17.yml b/apps/postgres-17.yml index f97e4d1..2ea9b0c 100644 --- a/apps/postgres-17.yml +++ b/apps/postgres-17.yml @@ -5,7 +5,7 @@ services: command: ["postgres", "-c", "log_min_messages=ERROR", "-c", "client_min_messages=ERROR"] environment: POSTGRES_DB: ${APP_NAME} - POSTGRES_PASSWORD: ${APPS_DATABASE_PASSWORD} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} POSTGRES_USER: ${APP_NAME} volumes: - ../apps-data/${APP_NAME}/postgres:/var/lib/postgresql/data diff --git a/apps/postgres-18.yml b/apps/postgres-18.yml index dfb8434..e8988de 100644 --- a/apps/postgres-18.yml +++ b/apps/postgres-18.yml @@ -5,7 +5,7 @@ services: command: ["postgres", "-c", "log_min_messages=ERROR", "-c", "client_min_messages=ERROR"] environment: POSTGRES_DB: ${APP_NAME} - POSTGRES_PASSWORD: ${APPS_DATABASE_PASSWORD} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} POSTGRES_USER: ${APP_NAME} volumes: - ../apps-data/${APP_NAME}/postgres:/var/lib/postgresql diff --git a/apps/rybbit/docker-compose.yml b/apps/rybbit/docker-compose.yml index 2ef7a29..05d51d4 100644 --- a/apps/rybbit/docker-compose.yml +++ b/apps/rybbit/docker-compose.yml @@ -2,22 +2,23 @@ include: - ../networks.yml - ../postgres-18.yml - ../clickhouse-26.5.yml -x-environment: &environment +x-vault-env: &vault-env + BASE_URL: https://${APP_NAME}.${DOMAIN} + BETTER_AUTH_SECRET: ${SESSION_KEY} + DISABLE_SIGNUP: ${DISABLE_SIGNUP:-false} + DISABLE_TELEMETRY: ${DISABLE_TELEMETRY:-true} + MAPBOX_TOKEN: ${MAPBOX_TOKEN:-} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + CLICKHOUSE_PASSWORD: ${DATABASE_PASSWORD} +x-internal-env: &internal-env NODE_ENV: production - BASE_URL: https://${APP_NAME}.${APPS_DOMAIN} - BETTER_AUTH_SECRET: ${APPS_KEY_HEX_32} - DISABLE_SIGNUP: ${APPS_DISABLE_SIGNUP:-false} - DISABLE_TELEMETRY: ${APPS_DISABLE_TELEMETRY:-true} - MAPBOX_TOKEN: ${APPS_MAPBOX_TOKEN:-} POSTGRES_HOST: postgres POSTGRES_PORT: 5432 POSTGRES_DB: ${APP_NAME} POSTGRES_USER: ${APP_NAME} - POSTGRES_PASSWORD: ${APPS_DATABASE_PASSWORD} CLICKHOUSE_HOST: http://clickhouse:8123 CLICKHOUSE_DB: ${APP_NAME} CLICKHOUSE_USER: ${APP_NAME} - CLICKHOUSE_PASSWORD: ${APPS_DATABASE_PASSWORD} services: client: image: ghcr.io/rybbit-io/rybbit-client @@ -30,8 +31,8 @@ services: - "traefik.http.services.${APP_NAME}.loadbalancer.server.port=3002" environment: NODE_ENV: production - NEXT_PUBLIC_BACKEND_URL: https://${APP_NAME}.${APPS_DOMAIN} - NEXT_PUBLIC_DISABLE_SIGNUP: ${APPS_DISABLE_SIGNUP:-false} + NEXT_PUBLIC_BACKEND_URL: https://${APP_NAME}.${DOMAIN} + NEXT_PUBLIC_DISABLE_SIGNUP: ${DISABLE_SIGNUP:-false} depends_on: - backend backend: @@ -43,7 +44,8 @@ services: - 3001 labels: - "traefik.http.services.${APP_NAME}-api.loadbalancer.server.port=3001" - environment: *environment + environment: + <<: [*vault-env, *internal-env] depends_on: clickhouse: condition: service_healthy diff --git a/apps/semaphore/docker-compose.yml b/apps/semaphore/docker-compose.yml index 32b6416..de0e682 100644 --- a/apps/semaphore/docker-compose.yml +++ b/apps/semaphore/docker-compose.yml @@ -1,20 +1,21 @@ include: - ../networks.yml - ../postgres-17.yml -x-environment: &environment - SEMAPHORE_ACCESS_KEY_ENCRYPTION: ${APPS_KEY_HEX_16} +x-vault-env: &vault-env + SEMAPHORE_ACCESS_KEY_ENCRYPTION: ${ENCRYPTION_KEY} + SEMAPHORE_DB_PASS: ${DATABASE_PASSWORD} + SEMAPHORE_ADMIN_PASSWORD: ${ADMIN_DEFAULT_PASS} + SEMAPHORE_ADMIN_EMAIL: ${ADMIN_MAIL} + SEMAPHORE_TELEGRAM_TOKEN: ${TELEGRAM_TOKEN} + SEMAPHORE_TELEGRAM_CHAT: ${TELEGRAM_CHAT} +x-internal-env: &internal-env SEMAPHORE_DB_DIALECT: postgres SEMAPHORE_DB_HOST: postgres SEMAPHORE_DB_NAME: ${APP_NAME} SEMAPHORE_DB_USER: ${APP_NAME} - SEMAPHORE_DB_PASS: ${APPS_DATABASE_PASSWORD} SEMAPHORE_ADMIN: admin - SEMAPHORE_ADMIN_PASSWORD: ${APPS_ADMIN_DEFAULT_PASS} SEMAPHORE_ADMIN_NAME: admin - SEMAPHORE_ADMIN_EMAIL: ${APPS_ADMIN_MAIL} SEMAPHORE_TELEGRAM_ALERT: true - SEMAPHORE_TELEGRAM_TOKEN: ${SEMAPHORE_TELEGRAM_TOKEN:-${APPS_TELEGRAM_TOKEN}} - SEMAPHORE_TELEGRAM_CHAT: ${SEMAPHORE_TELEGRAM_CHAT:-${APPS_TELEGRAM_CHAT}} ANSIBLE_HOST_KEY_CHECKING: false services: semaphore: @@ -26,6 +27,7 @@ services: - 3000 labels: - "traefik.http.services.${APP_NAME}.loadbalancer.server.port=3000" - environment: *environment + environment: + <<: [*vault-env, *internal-env] depends_on: - postgres diff --git a/apps/timescale-17.yml b/apps/timescale-17.yml index abddbae..48e90fe 100644 --- a/apps/timescale-17.yml +++ b/apps/timescale-17.yml @@ -5,7 +5,7 @@ services: environment: POSTGRES_DB: ${APP_NAME} POSTGRES_USER: ${APP_NAME} - POSTGRES_PASSWORD: ${APPS_DATABASE_PASSWORD} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} volumes: - ../apps-data/${APP_NAME}/timescale:/var/lib/postgresql/data healthcheck: diff --git a/apps/traefik/docker-compose.yml b/apps/traefik/docker-compose.yml index 6e03b4a..6de97c9 100644 --- a/apps/traefik/docker-compose.yml +++ b/apps/traefik/docker-compose.yml @@ -1,9 +1,9 @@ -x-environment: &environment - CF_DNS_API_TOKEN: ${APPS_CLOUDFLARE_DNS_API_TOKEN} +x-vault-env: &vault-env + CF_DNS_API_TOKEN: ${CLOUDFLARE_DNS_API_TOKEN} services: traefik: image: traefik - environment: *environment + environment: *vault-env ports: - "${HTTP_PORT}:80" - "${HTTPS_PORT}:443" diff --git a/apps/traefik/traefik.yml.tpl b/apps/traefik/traefik.yml.tpl index 2b94bb4..b822446 100644 --- a/apps/traefik/traefik.yml.tpl +++ b/apps/traefik/traefik.yml.tpl @@ -30,13 +30,13 @@ providers: certificatesResolvers: acmeHttpChallengeResolver: acme: - email: ${APPS_ADMIN_MAIL} + email: ${ADMIN_MAIL} storage: acme.json httpChallenge: entryPoint: http acmeCloudflareDnsChallengeResolver: acme: - email: ${APPS_ADMIN_MAIL} + email: ${ADMIN_MAIL} storage: acme.json dnsChallenge: provider: cloudflare diff --git a/apps/twofauth/docker-compose.yml b/apps/twofauth/docker-compose.yml index ea9f5c3..3cf8e9e 100644 --- a/apps/twofauth/docker-compose.yml +++ b/apps/twofauth/docker-compose.yml @@ -1,13 +1,19 @@ include: - ../networks.yml -x-environment: &environment +x-vault-env: &vault-env + SITE_OWNER: ${ADMIN_MAIL} + APP_KEY: ${ENCRYPTION_KEY} + APP_URL: https://${APP_NAME}.${DOMAIN} + MAIL_HOST: ${SMTP_HOSTNAME} + MAIL_PORT: ${SMTP_PORT} + MAIL_USERNAME: ${SMTP_USERNAME} + MAIL_PASSWORD: ${SMTP_PASSWORD} + MAIL_FROM_ADDRESS: ${SMTP_FROM} +x-internal-env: &internal-env APP_NAME: 2FAuth APP_ENV: local APP_TIMEZONE: UTC APP_DEBUG: false - SITE_OWNER: ${APPS_ADMIN_MAIL} - APP_KEY: ${APPS_KEY_HEX_16} - APP_URL: https://${APP_NAME}.${TWOFAUTH_APPS_DOMAIN:-${APPS_DOMAIN}} IS_DEMO_APP: false LOG_CHANNEL: daily LOG_LEVEL: error @@ -15,13 +21,8 @@ x-environment: &environment CACHE_DRIVER: file SESSION_DRIVER: file MAIL_MAILER: smtp - MAIL_HOST: ${APPS_SMTP_HOSTNAME} - MAIL_PORT: ${APPS_SMTP_PORT} - MAIL_USERNAME: ${APPS_SMTP_USERNAME} - MAIL_PASSWORD: ${APPS_SMTP_PASSWORD} MAIL_ENCRYPTION: true MAIL_FROM_NAME: ${APP_NAME} - MAIL_FROM_ADDRESS: ${APPS_SMTP_FROM} MAIL_VERIFY_SSL_PEER: false THROTTLE_API: 60 LOGIN_THROTTLE: 5 @@ -45,7 +46,8 @@ services: - 8000 labels: - "traefik.http.services.${APP_NAME}.loadbalancer.server.port=8000" - - "traefik.http.routers.${APP_NAME}.rule=Host(`${APP_NAME}.${TWOFAUTH_APPS_DOMAIN:-${APPS_DOMAIN}}`)" - environment: *environment + - "traefik.http.routers.${APP_NAME}.rule=Host(`${APP_NAME}.${DOMAIN}`)" + environment: + <<: [*vault-env, *internal-env] volumes: - ../../apps-data/${APP_NAME}/2fauth:/2fauth diff --git a/deploy/collisions.py b/deploy/collisions.py index aac1847..d9d57e6 100644 --- a/deploy/collisions.py +++ b/deploy/collisions.py @@ -2,7 +2,7 @@ """Detect duplicate env keys across an app's env_refs sources, from ciphertext. SOPS's dotenv output format only encrypts values, not key names -(`APPS_DOMAIN=ENC[...]`), so this needs no decryption at all - it runs in +(`DOMAIN=ENC[...]`), so this needs no decryption at all - it runs in CI, before anything is pushed to the target host, on files whose private key CI never has access to in the first place. """ diff --git a/deploy/tests/test_collisions.py b/deploy/tests/test_collisions.py index aa3575c..599cb6d 100644 --- a/deploy/tests/test_collisions.py +++ b/deploy/tests/test_collisions.py @@ -12,7 +12,7 @@ TRAEFIK_ENV = """\ HTTP_PORT=ENC[AES256_GCM,data:Ab==,iv:xx==,tag:yy==,type:str] HTTPS_PORT=ENC[AES256_GCM,data:Cd==,iv:xx==,tag:yy==,type:str] -APPS_DOMAIN=ENC[AES256_GCM,data:Ef==,iv:xx==,tag:yy==,type:str] +DOMAIN=ENC[AES256_GCM,data:Ef==,iv:xx==,tag:yy==,type:str] sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE----- sops_lastmodified=2026-08-19T00:00:00Z sops_mac=ENC[AES256_GCM,data:Gh==,iv:xx==,tag:yy==,type:str] @@ -20,14 +20,14 @@ """ RYBBIT_ENV = """\ -APPS_DATABASE_PASSWORD=ENC[AES256_GCM,data:Ij==,iv:xx==,tag:yy==,type:str] -APPS_KEY_HEX_32=ENC[AES256_GCM,data:Kl==,iv:xx==,tag:yy==,type:str] +DATABASE_PASSWORD=ENC[AES256_GCM,data:Ij==,iv:xx==,tag:yy==,type:str] +SESSION_KEY=ENC[AES256_GCM,data:Kl==,iv:xx==,tag:yy==,type:str] sops_lastmodified=2026-08-19T00:00:00Z sops_version=3.13.1 """ COLLIDING_ENV = """\ -APPS_DOMAIN=ENC[AES256_GCM,data:Mn==,iv:xx==,tag:yy==,type:str] +DOMAIN=ENC[AES256_GCM,data:Mn==,iv:xx==,tag:yy==,type:str] sops_version=3.13.1 """ @@ -59,7 +59,7 @@ def test_ignores_sops_metadata_keys(self): keys = collisions.extract_keys(traefik) | collisions.extract_keys(rybbit) self.assertFalse(any(key.startswith("sops_") for key in keys)) self.assertIn("HTTP_PORT", keys) - self.assertIn("APPS_KEY_HEX_32", keys) + self.assertIn("SESSION_KEY", keys) if __name__ == "__main__": diff --git a/deploy/tests/test_deploy.py b/deploy/tests/test_deploy.py index 0b7f187..5fefbcb 100644 --- a/deploy/tests/test_deploy.py +++ b/deploy/tests/test_deploy.py @@ -150,9 +150,9 @@ def test_renders_templates_found_for_app_next_to_the_template(self): release_dir = Path(directory) app_dir = release_dir / "apps" / "traefik" app_dir.mkdir(parents=True) - (app_dir / "traefik.yml.tpl").write_text("email: ${APPS_ADMIN_MAIL}\n") + (app_dir / "traefik.yml.tpl").write_text("email: ${ADMIN_MAIL}\n") - deploy.render_app_configs(release_dir, "traefik", {"APPS_ADMIN_MAIL": "a@example.com"}) + deploy.render_app_configs(release_dir, "traefik", {"ADMIN_MAIL": "a@example.com"}) rendered_path = app_dir / "traefik.yml" self.assertEqual(rendered_path.read_text(), "email: a@example.com\n") @@ -179,20 +179,20 @@ def _release_dir_with_app(self, work_dir, app, template=None): def test_writes_decrypted_env_and_renders_configs(self): with tempfile.TemporaryDirectory() as directory: work_dir = Path(directory) - release_dir = self._release_dir_with_app(work_dir, "traefik", template="email: ${APPS_ADMIN_MAIL}\n") + release_dir = self._release_dir_with_app(work_dir, "traefik", template="email: ${ADMIN_MAIL}\n") ciphertext = work_dir / "a.sops.env" - ciphertext.write_text("APPS_ADMIN_MAIL=ENC[...]\n") + ciphertext.write_text("ADMIN_MAIL=ENC[...]\n") config = {"apps": {"traefik": {"env_refs": ["owner/repo@latest:a.sops.env"]}}} with ( patch.object(deploy, "download_ref", return_value=ciphertext), - patch.object(deploy, "decrypt_env", return_value="APPS_ADMIN_MAIL=a@example.com\n"), + patch.object(deploy, "decrypt_env", return_value="ADMIN_MAIL=a@example.com\n"), ): deploy.resolve_app_envs(config, work_dir / "work", release_dir, work_dir / "key.txt") env_path = release_dir / "apps" / "traefik" / ".env" - self.assertEqual(env_path.read_text(), "APPS_ADMIN_MAIL=a@example.com\n") + self.assertEqual(env_path.read_text(), "ADMIN_MAIL=a@example.com\n") self.assertEqual(oct(env_path.stat().st_mode)[-3:], "600") rendered_path = release_dir / "apps" / "traefik" / "traefik.yml" @@ -203,9 +203,9 @@ def test_raises_on_collision_within_one_apps_own_env_refs(self): work_dir = Path(directory) release_dir = self._release_dir_with_app(work_dir, "traefik") first_env = work_dir / "a.sops.env" - first_env.write_text("APPS_DOMAIN=ENC[AES256_GCM,data:Ab==,iv:xx==,tag:yy==,type:str]\n") + first_env.write_text("DOMAIN=ENC[AES256_GCM,data:Ab==,iv:xx==,tag:yy==,type:str]\n") second_env = work_dir / "b.sops.env" - second_env.write_text("APPS_DOMAIN=ENC[AES256_GCM,data:Cd==,iv:xx==,tag:yy==,type:str]\n") + second_env.write_text("DOMAIN=ENC[AES256_GCM,data:Cd==,iv:xx==,tag:yy==,type:str]\n") def fake_download_ref(ref, out_dir, default_asset=None, run=None): return first_env if ref.endswith(":a.sops.env") else second_env @@ -229,15 +229,15 @@ def test_allows_same_key_across_different_apps(self): release_dir = self._release_dir_with_app(work_dir, "traefik") self._release_dir_with_app(work_dir, "rybbit") traefik_env = work_dir / "a.sops.env" - traefik_env.write_text("APPS_DOMAIN=ENC[AES256_GCM,data:Ab==,iv:xx==,tag:yy==,type:str]\n") + traefik_env.write_text("DOMAIN=ENC[AES256_GCM,data:Ab==,iv:xx==,tag:yy==,type:str]\n") rybbit_env = work_dir / "b.sops.env" - rybbit_env.write_text("APPS_DOMAIN=ENC[AES256_GCM,data:Cd==,iv:xx==,tag:yy==,type:str]\n") + rybbit_env.write_text("DOMAIN=ENC[AES256_GCM,data:Cd==,iv:xx==,tag:yy==,type:str]\n") def fake_download_ref(ref, out_dir, default_asset=None, run=None): return traefik_env if "traefik" in ref else rybbit_env def fake_decrypt_env(path, age_key_file, run=None): - return "APPS_DOMAIN=example.com\n" + return "DOMAIN=example.com\n" config = { "apps": { diff --git a/deploy/tests/test_render.py b/deploy/tests/test_render.py index 759660c..834ed5e 100644 --- a/deploy/tests/test_render.py +++ b/deploy/tests/test_render.py @@ -10,20 +10,20 @@ class RenderTemplateTest(unittest.TestCase): def test_substitutes_braced_var(self): - result = render.render_template("email: ${APPS_ADMIN_MAIL}", {"APPS_ADMIN_MAIL": "a@example.com"}) + result = render.render_template("email: ${ADMIN_MAIL}", {"ADMIN_MAIL": "a@example.com"}) self.assertEqual(result, "email: a@example.com") def test_substitutes_bare_var(self): - result = render.render_template("email: $APPS_ADMIN_MAIL", {"APPS_ADMIN_MAIL": "a@example.com"}) + result = render.render_template("email: $ADMIN_MAIL", {"ADMIN_MAIL": "a@example.com"}) self.assertEqual(result, "email: a@example.com") def test_missing_var_becomes_empty_string(self): - result = render.render_template("email: ${APPS_ADMIN_MAIL}", {}) + result = render.render_template("email: ${ADMIN_MAIL}", {}) self.assertEqual(result, "email: ") def test_leaves_bash_default_syntax_untouched(self): - text = "email: ${APPS_ADMIN_MAIL:-fallback@example.com}" - result = render.render_template(text, {"APPS_ADMIN_MAIL": "a@example.com"}) + text = "email: ${ADMIN_MAIL:-fallback@example.com}" + result = render.render_template(text, {"ADMIN_MAIL": "a@example.com"}) self.assertEqual(result, text) def test_leaves_double_dollar_untouched(self): diff --git a/deploy/tests/test_vault.py b/deploy/tests/test_vault.py index 3df2d3d..a42215e 100644 --- a/deploy/tests/test_vault.py +++ b/deploy/tests/test_vault.py @@ -28,9 +28,9 @@ def fail(stderr="boom"): class DecryptEnvTest(unittest.TestCase): def test_returns_decrypted_stdout(self): calls = [] - run = fake_run(calls, [ok("APPS_DOMAIN=example.com\n")]) + run = fake_run(calls, [ok("DOMAIN=example.com\n")]) plaintext = vault.decrypt_env("traefik.sops.env", "/tmp/key.txt", run=run) - self.assertEqual(plaintext, "APPS_DOMAIN=example.com\n") + self.assertEqual(plaintext, "DOMAIN=example.com\n") def test_passes_sops_decrypt_dotenv_args(self): calls = [] @@ -49,20 +49,20 @@ def test_raises_on_failure(self): class ParseDotenvTest(unittest.TestCase): def test_parses_key_value_pairs(self): - values = vault.parse_dotenv("APPS_DOMAIN=example.com\nHTTP_PORT=80\n") - self.assertEqual(values, {"APPS_DOMAIN": "example.com", "HTTP_PORT": "80"}) + values = vault.parse_dotenv("DOMAIN=example.com\nHTTP_PORT=80\n") + self.assertEqual(values, {"DOMAIN": "example.com", "HTTP_PORT": "80"}) def test_splits_only_on_first_equals(self): - values = vault.parse_dotenv("APPS_HTPASSWD=user:pass=word\n") - self.assertEqual(values["APPS_HTPASSWD"], "user:pass=word") + values = vault.parse_dotenv("HTPASSWD=user:pass=word\n") + self.assertEqual(values["HTPASSWD"], "user:pass=word") def test_ignores_lines_without_equals(self): - values = vault.parse_dotenv("not a valid line\nAPPS_DOMAIN=example.com\n") - self.assertEqual(values, {"APPS_DOMAIN": "example.com"}) + values = vault.parse_dotenv("not a valid line\nDOMAIN=example.com\n") + self.assertEqual(values, {"DOMAIN": "example.com"}) def test_ignores_blank_lines(self): - values = vault.parse_dotenv("\n\nAPPS_DOMAIN=example.com\n") - self.assertEqual(values, {"APPS_DOMAIN": "example.com"}) + values = vault.parse_dotenv("\n\nDOMAIN=example.com\n") + self.assertEqual(values, {"DOMAIN": "example.com"}) if __name__ == "__main__": diff --git a/vaults/hawkeye-rybbit.yml b/vaults/hawkeye-rybbit.yml index 09e1ee4..c199b28 100644 --- a/vaults/hawkeye-rybbit.yml +++ b/vaults/hawkeye-rybbit.yml @@ -2,7 +2,7 @@ asset: hawkeye-rybbit.sops.env keys: - hawkeye env: - APPS_DOMAIN: RUBYKATZEN_COM_DOMAIN - APPS_CERTIFICATE_RESOLVER: RUBYKATZEN_COM_CERT_RESOLVER - APPS_DATABASE_PASSWORD: RUBYKATZEN_COM_DATABASE_PASSWORD - APPS_KEY_HEX_32: RUBYKATZEN_COM_KEY_HEX_32 + DOMAIN: DOMAIN + CERTIFICATE_RESOLVER: CERTIFICATE_RESOLVER + DATABASE_PASSWORD: DATABASE_PASSWORD + SESSION_KEY: SESSION_KEY diff --git a/vaults/hawkeye-traefik.yml b/vaults/hawkeye-traefik.yml index 25535ec..1c88642 100644 --- a/vaults/hawkeye-traefik.yml +++ b/vaults/hawkeye-traefik.yml @@ -2,7 +2,7 @@ asset: hawkeye-traefik.sops.env keys: - hawkeye env: - APPS_ADMIN_MAIL: RUBYKATZEN_COM_ADMIN_MAIL - APPS_CLOUDFLARE_DNS_API_TOKEN: RUBYKATZEN_COM_CLOUDFLARE_TOKEN - HTTP_PORT: RUBYKATZEN_COM_TRAEFIK_HTTP_PORT - HTTPS_PORT: RUBYKATZEN_COM_TRAEFIK_HTTPS_PORT + ADMIN_MAIL: ADMIN_MAIL + CLOUDFLARE_DNS_API_TOKEN: CLOUDFLARE_DNS_API_TOKEN + HTTP_PORT: TRAEFIK_HTTP_PORT + HTTPS_PORT: TRAEFIK_HTTPS_PORT