analytics
Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s
Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s
Signed-off-by: Melvin Strobl <lc3267@kit.edu>
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
.PHONY: dev-utils dev dev-local up down update install dev-backend dev-frontend \
|
.PHONY: dev-utils dev dev-local up down update install dev-backend dev-frontend \
|
||||||
generate-client seed-example test test-backend test-frontend soak lint lint-backend \
|
generate-client seed-example test test-backend test-frontend soak lint lint-backend \
|
||||||
lint-frontend clean help
|
lint-frontend umami clean help
|
||||||
|
|
||||||
COMPOSE_ROOT := $(CURDIR)
|
COMPOSE_ROOT := $(CURDIR)
|
||||||
# Explicit project name keeps this stack isolated from the sibling website
|
# Explicit project name keeps this stack isolated from the sibling website
|
||||||
@@ -39,6 +39,32 @@ dev-local: ## Start the integrated local stack (called by the root `make dev`)
|
|||||||
up: ## Start the production stack
|
up: ## Start the production stack
|
||||||
$(COMPOSE_PROD_RUN) up --build -d
|
$(COMPOSE_PROD_RUN) up --build -d
|
||||||
|
|
||||||
|
umami: ## Provision + start the optional Umami site-analytics service (idempotent)
|
||||||
|
# Guard: the dashboard is internet-facing on analytics.$(DOMAIN); refuse to
|
||||||
|
# start when its session-signing secret is unset or still the placeholder.
|
||||||
|
@secret=$$(grep -E '^UMAMI_APP_SECRET=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); \
|
||||||
|
if [ -z "$$secret" ] || [ "$$secret" = "changethis" ]; then \
|
||||||
|
echo "ERROR: UMAMI_APP_SECRET is empty or 'changethis' in .env — run the workspace root's scripts/setup.sh (or set it: openssl rand -hex 32) before 'make umami'." >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
# --no-recreate: reuse an already-running db instead of recreating it under
|
||||||
|
# the prod compose set, which would blip every service that depends on it.
|
||||||
|
$(COMPOSE_PROD) up -d --wait --no-recreate db
|
||||||
|
# Least-privilege role owning only the umami database, so the third-party
|
||||||
|
# image never holds the shared superuser credentials.
|
||||||
|
@pguser=$$(grep -E '^POSTGRES_USER=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); pguser=$${pguser:-postgres}; \
|
||||||
|
role=$$(grep -E '^UMAMI_DB_USER=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); role=$${role:-umami}; \
|
||||||
|
pw=$$(grep -E '^UMAMI_DB_PASSWORD=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); \
|
||||||
|
db=$$(grep -E '^UMAMI_DB=' $(COMPOSE_ROOT)/.env 2>/dev/null | head -1 | cut -d= -f2-); db=$${db:-umami}; \
|
||||||
|
if [ -z "$$pw" ] || [ "$$pw" = "changethis" ]; then \
|
||||||
|
echo "ERROR: UMAMI_DB_PASSWORD is empty or 'changethis' in .env — run the workspace root's scripts/setup.sh before 'make umami'." >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
$(COMPOSE_PROD) exec -T db psql -v ON_ERROR_STOP=1 -U "$$pguser" -d postgres \
|
||||||
|
-v role="$$role" -v pw="$$pw" -v db="$$db" < docker/provision-umami-db.sql
|
||||||
|
# --no-deps: only (re)create umami, never restart the shared db underneath it.
|
||||||
|
$(COMPOSE_PROD) --profile analytics up -d --no-deps umami
|
||||||
|
|
||||||
update: ## Pull, rebuild using the layer cache, and recreate changed containers
|
update: ## Pull, rebuild using the layer cache, and recreate changed containers
|
||||||
git pull
|
git pull
|
||||||
$(COMPOSE_PROD_RUN) build
|
$(COMPOSE_PROD_RUN) build
|
||||||
|
|||||||
@@ -238,6 +238,55 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
|
||||||
|
# ── Site analytics (optional) ──────────────────────────────────────
|
||||||
|
# Self-hosted, cookieless Umami on analytics.${DOMAIN}. Gated behind the
|
||||||
|
# `analytics` profile, so a plain `make up` never starts it; bring it up with
|
||||||
|
# `make umami`, which also provisions its database.
|
||||||
|
umami:
|
||||||
|
container_name: fluksio-umami
|
||||||
|
# Pinned by digest so a bump is a deliberate step: this is an internet-facing
|
||||||
|
# third-party dashboard, not a library.
|
||||||
|
image: ghcr.io/umami-software/umami:postgresql-latest@sha256:87312d334d009ee67ee0d2fba8fed01435547cc468e452243aef5133a9984d48
|
||||||
|
restart: always
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
profiles: ["analytics"]
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
- default
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
# Deliberately no `env_file: ../.env` -- this third-party image receives the
|
||||||
|
# three variables it needs and none of the app's secrets.
|
||||||
|
environment:
|
||||||
|
- DATABASE_TYPE=postgresql
|
||||||
|
# Least-privilege role owning only the umami database, never the shared
|
||||||
|
# Postgres superuser (provisioned by `make umami`).
|
||||||
|
- DATABASE_URL=postgresql://${UMAMI_DB_USER:-umami}:${UMAMI_DB_PASSWORD}@db:5432/${UMAMI_DB:-umami}
|
||||||
|
- APP_SECRET=${UMAMI_APP_SECRET:-}
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
# No healthcheck on purpose: the upstream image ships neither curl nor wget,
|
||||||
|
# so a probe would leave the container permanently `starting` and Traefik
|
||||||
|
# would never route it (the same trap the backend's python healthcheck
|
||||||
|
# documents).
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.docker.network=proxy
|
||||||
|
- traefik.constraint-label=proxy
|
||||||
|
|
||||||
|
- traefik.http.services.${STACK_NAME?Variable not set}-umami.loadbalancer.server.port=3000
|
||||||
|
|
||||||
|
- traefik.http.routers.${STACK_NAME?Variable not set}-umami-http.rule=Host(`analytics.${DOMAIN?Variable not set}`)
|
||||||
|
- traefik.http.routers.${STACK_NAME?Variable not set}-umami-http.entrypoints=http
|
||||||
|
- traefik.http.routers.${STACK_NAME?Variable not set}-umami-http.middlewares=https-redirect
|
||||||
|
|
||||||
|
- traefik.http.routers.${STACK_NAME?Variable not set}-umami-https.rule=Host(`analytics.${DOMAIN?Variable not set}`)
|
||||||
|
- traefik.http.routers.${STACK_NAME?Variable not set}-umami-https.entrypoints=https
|
||||||
|
- traefik.http.routers.${STACK_NAME?Variable not set}-umami-https.tls=true
|
||||||
|
- traefik.http.routers.${STACK_NAME?Variable not set}-umami-https.tls.certresolver=le
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
app-db-data:
|
app-db-data:
|
||||||
app-redis-data:
|
app-redis-data:
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- Least-privilege Postgres role + database for the optional Umami analytics
|
||||||
|
-- service. Umami must not connect as the shared superuser, which can read the
|
||||||
|
-- `app` database holding every user, flow and credential. This role owns only
|
||||||
|
-- its own database, so it has full rights there and none on `app`.
|
||||||
|
--
|
||||||
|
-- Invoked by `make umami` (piped into psql in the db container). Idempotent.
|
||||||
|
-- Identifiers and the password arrive as psql variables (-v role=… -v pw=…
|
||||||
|
-- -v db=…) and are quoted via format()'s %I / %L, so they are injection-safe.
|
||||||
|
|
||||||
|
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'role', :'pw')
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'role')
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
SELECT format('ALTER ROLE %I LOGIN PASSWORD %L', :'role', :'pw')
|
||||||
|
WHERE EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'role')
|
||||||
|
\gexec
|
||||||
|
|
||||||
|
SELECT format('CREATE DATABASE %I OWNER %I', :'db', :'role')
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = :'db')
|
||||||
|
\gexec
|
||||||
Reference in New Issue
Block a user