diff --git a/.env.example b/.env.example index ba9bc13..9f5f458 100644 --- a/.env.example +++ b/.env.example @@ -1,29 +1,122 @@ -########################################################################## -# Plumber configuration file # -# Documentation: https://getplumber.io/docs/installation/docker-compose/ # -########################################################################## - -# Required configuration -DOMAIN_NAME="" -JOBS_GITLAB_URL="" -ORGANIZATION="" -GITLAB_OAUTH2_CLIENT_ID="" -GITLAB_OAUTH2_CLIENT_SECRET="" - -# Auto-generated secrets (populated by install script) -SECRET_KEY="" -JOBS_DB_PASSWORD="" -JOBS_REDIS_PASSWORD="" - -# Deployment profile (set by install script) -# Options: letsencrypt or custom-certs, combined with internal-db or omit for external -COMPOSE_PROFILES="letsencrypt,internal-db" -CERT_RESOLVER="le" - -# Optional: Override database defaults (only needed when NOT using internal-db profile) -# JOBS_DB_HOST="your-db-host" -# JOBS_DB_PORT="5432" -# JOBS_DB_USER="jobs" -# JOBS_DB_NAME="jobs" -# JOBS_DB_SSLMODE="disable" -# JOBS_DB_TIMEZONE="Europe/Paris" +# Plumber v2 configuration (compose.yml, root of this repo). +# +# Copy this file to .env, fill in every value under "Required", then +# `docker compose up -d`. See the root README.md for the full quick start +# and the first-run bootstrap step. v1's own .env.example lives unchanged in +# legacy-v1/ and is unrelated to this file. + +# The release workflow rewrites this line in place (`sed -i +# "s/^PLATFORM_VERSION=.*/PLATFORM_VERSION=$V/" .env.example`) - keep it the +# very first line, keep the variable name exact. This is also the image tag +# for both docker.io/getplumber/platform-backend and platform-frontend. +PLATFORM_VERSION=v2.0.0 + +############################################################################## +# Required +############################################################################## + +# --- Database --------------------------------------------------------------- +# Credentials for the bundled Postgres container (compose.yml's own +# "postgres" service) - the backend connects to it as PLUMBER_DB_HOST=postgres, +# hardcoded in compose.yml since it is a sibling service, not operator config. +PLUMBER_DB_USER=plumber +PLUMBER_DB_NAME=plumber +# Generate: openssl rand -hex 16 +PLUMBER_DB_PASSWORD= + +# --- Public origin / OIDC ---------------------------------------------------- +# The externally reachable base URL of THIS install, no trailing slash (this +# is also what you put in front of a reverse proxy - see compose.yml's +# frontend service comment for why one is needed for the browser). +PLUMBER_BASE_URL=https://plumber.example.com + +# The `aud` claim every CI OIDC token must carry to be accepted as a push. +# Must be BYTE-IDENTICAL to PLUMBER_BASE_URL above (this is what the +# `id_tokens:` block in a project's .gitlab-ci.yml is configured to request +# as its audience) - two different values here is a guaranteed +# "every push rejected" misconfiguration. +PLUMBER_OIDC_AUDIENCE=https://plumber.example.com + +# Comma-separated list of GitLab instance base URLs this install trusts to +# issue CI OIDC tokens (e.g. https://gitlab.com or your self-hosted +# instance's URL). Leaving this empty means ingestion rejects every push - +# there is no useful default. +PLUMBER_OIDC_ALLOWED_ISSUERS=https://gitlab.com + +# --- Secrets ------------------------------------------------------------------ +# Seals every org token, OAuth client secret, and user OAuth token this +# install stores at rest (AES-256-GCM). MUST be exactly 64 hex characters +# (32 bytes) - a shorter value fails the backend at startup. +# Generate: openssl rand -hex 32 +# +# There is no in-place rotation: swapping this value does not re-seal +# anything already stored under the old key, and every secret sealed under +# the old key becomes undecryptable (fails loudly, never silently). Back +# this value up somewhere safe - losing it means re-entering every org +# token, OAuth app secret, and having every user log in again. +PLUMBER_TOKEN_ENCRYPTION_KEY= + +############################################################################## +# Optional - ports +############################################################################## + +# Host-side ports compose publishes the two apps on. Change these if +# 8080/3000 are already taken on this host. +# BACKEND_PORT=8080 +# FRONTEND_PORT=3000 + +############################################################################## +# Optional hardening knobs (all commented; uncomment to change the default) +############################################################################## + +# Redis: PLUMBER_REDIS_ADDR (compose.yml, hardcoded to redis:6379 - the +# bundled Redis container has no auth/TLS since it never leaves the compose +# network) is the default. If you point at an external/managed Redis +# instead, edit compose.yml to set PLUMBER_REDIS_URL (a redis:// or +# rediss:// URL with credentials, mutually exclusive with PLUMBER_REDIS_ADDR) +# rather than adding both here. + +# Postgres TLS - only meaningful if you edit compose.yml to point +# PLUMBER_DB_HOST at a real network hop instead of the bundled postgres +# service. Values: disable (default) | allow | prefer | require | +# verify-ca | verify-full. +# PLUMBER_DB_SSLMODE=disable +# PLUMBER_DB_SSLROOTCERT=/path/to/pg-ca.pem +# PLUMBER_DB_SSLCERT=/path/to/client-cert.pem +# PLUMBER_DB_SSLKEY=/path/to/client-key.pem + +# Refuse a loopback/RFC1918/link-local base_url or SMTP relay address by +# mistake. Both default to false because on-prem/private GitLab and mail +# relays are the common, legitimate case for this product; the cloud +# metadata address (169.254.169.254 and friends) is always refused +# regardless of these flags. +# PLUMBER_BASE_URL_REJECT_PRIVATE=false +# PLUMBER_REPORT_SMTP_REJECT_PRIVATE_RELAY=false + +# Behind a reverse proxy/ingress (recommended - see compose.yml's frontend +# note): trust the nearest untrusted X-Forwarded-For hop for the inbound +# rate limiter's per-IP key, instead of trusting only the direct TCP peer +# (which, behind a proxy, is always the proxy itself - every caller then +# shares ONE fleet-wide rate-limit budget). Only enable this once a real +# proxy is actually in front; enabling it without one lets any caller spoof +# its own rate-limit key via the header. +# PLUMBER_TRUST_FORWARDED_FOR=false +# PLUMBER_TRUSTED_PROXY_CIDRS= + +# Air-gapped install: stop the daily "is a newer version available" dial-out +# entirely (default dials https://raw.githubusercontent.com/getplumber/platform/main/latest.json). +# PLUMBER_UPDATE_CHECK_URL=off + +# Mark session cookies Secure - flip this on once this install is actually +# served over HTTPS (through your own reverse proxy). +# PLUMBER_COOKIE_SECURE=false + +# How many days of full-granularity history (runs, project snapshots, sync +# attempts) this install keeps before the weekly retention sweep downsamples +# older data. Floor is 2 (48h); default 30. +# PLUMBER_RETENTION_DAYS=30 + +# Provider (GitLab) CA bundle: set this if your GitLab instance's TLS +# certificate is signed by an internal/private CA the backend does not +# already trust. PEM file path, mounted into the backend container. +# PLUMBER_PROVIDER_CA_BUNDLE=/path/to/gitlab-ca.pem diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de94a67..e46e24c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,13 +36,15 @@ jobs: - name: Install chart-testing uses: helm/chart-testing-action@afea100a513515fbd68b0e72a7bb0ae34cb62aec # v2.3.1 - name: Add dependency chart repositories - run: ./scripts/add_helm_repo.sh + # v1's own scripts moved to legacy-v1/ 2026-08-25 (root takeover by v2) - path updated, + # nothing else about this script changed. + run: ./legacy-v1/scripts/add_helm_repo.sh - name: List changed charts id: list-changed env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} run: | - changed=$(ct list-changed --target-branch "$DEFAULT_BRANCH") + changed=$(ct list-changed --target-branch "$DEFAULT_BRANCH" --chart-dirs legacy-v1/charts) charts=$(echo "$changed" | tr '\n' ' ' | xargs) if [[ -n "$changed" ]]; then echo "changed=true" >> $GITHUB_OUTPUT @@ -51,7 +53,7 @@ jobs: - name: Lint charts env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - run: ct lint --target-branch "$DEFAULT_BRANCH" + run: ct lint --target-branch "$DEFAULT_BRANCH" --chart-dirs legacy-v1/charts unit-tests: name: Unit tests runs-on: ubuntu-latest @@ -59,7 +61,7 @@ jobs: - uses: actions/checkout@v3 - uses: d3adb5/helm-unittest-action@850bc76597579183998069830d5fa8c3ef0ea34a # 2.5.0 with: - charts: charts/plumber + charts: legacy-v1/charts/plumber flags: --color -o test-results.xml --output-type JUnit helm-version: v3.10.1 # helm-unittest v1.1.0 (2026-05-08) switched plugin.yaml to the diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..34630d8 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,68 @@ +# Step 7 of the release-process spec (getplumber/monorepo, +# .github/workflows/release.yml): that workflow opens a release PR against +# THIS repo (branch release-$V) carrying an updated .env.example +# (PLATFORM_VERSION=$V), latest.json ({"latest":"$V"}), and +# releases/$V.md. Thomas reviews and merges that PR by hand - merging it is +# the release approval. This workflow reacts to that merge landing on main: +# it never opens or approves anything itself, it only turns an already +# merged version bump into a real git tag + GitHub Release. +# +# Deliberately narrow trigger (paths: latest.json) so this never fires on +# an unrelated push to main. +name: publish release + +on: + push: + branches: [main] + paths: [latest.json] + +concurrency: {group: publish-release, cancel-in-progress: false} + +permissions: + contents: write + +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: {fetch-depth: 0} + + - name: resolve version + id: ver + run: | + set -euo pipefail + V=$(jq -r '.latest' latest.json) + if [ -z "$V" ] || [ "$V" = "null" ]; then + echo "::error::latest.json has no .latest field" + exit 1 + fi + if ! [[ "$V" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::latest.json's .latest ('$V') is not a vX.Y.Z version" + exit 1 + fi + NOTES="releases/$V.md" + if [ ! -f "$NOTES" ]; then + echo "::error::$NOTES not found - the release PR that bumped latest.json to $V must also add it" + exit 1 + fi + echo "version=$V" >> "$GITHUB_OUTPUT" + echo "notes=$NOTES" >> "$GITHUB_OUTPUT" + + - name: tag + release (idempotent) + env: + GH_TOKEN: ${{ github.token }} + V: ${{ steps.ver.outputs.version }} + NOTES: ${{ steps.ver.outputs.notes }} + run: | + set -euo pipefail + if git rev-parse -q --verify "refs/tags/$V" >/dev/null; then + echo "tag $V already exists - nothing to do (re-run or a no-op latest.json change)" + exit 0 + fi + git config user.name "plumber-release-bot" + git config user.email "release@getplumber.io" + git tag "$V" + git push origin "$V" + gh release create "$V" --title "$V" --notes-file "$NOTES" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c858ad3..fa60550 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,9 +26,13 @@ jobs: with: version: 3.10.1 - name: Add dependency chart repositories - run: ./scripts/add_helm_repo.sh + # v1's own scripts moved to legacy-v1/ 2026-08-25 (root takeover by v2) - path updated, + # nothing else about this script changed. + run: ./legacy-v1/scripts/add_helm_repo.sh - name: Run chart-releaser uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0 + with: + charts_dir: legacy-v1/charts env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" CR_RELEASE_NAME_TEMPLATE: "v{{ .Version }}" diff --git a/README.md b/README.md index 4ff45a2..c7f16df 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,98 @@ -# Self-Managed Plumber +# Plumber Platform (self-managed) -[![Plumber Score](https://score.getplumber.io/github.com/getplumber/platform.svg)](https://score.getplumber.io/github.com/getplumber/platform) [![CI](https://github.com/getplumber/platform/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/getplumber/platform/actions/workflows/ci.yml) -[![Release](https://github.com/getplumber/platform/actions/workflows/release.yml/badge.svg?branch=main)](https://github.com/getplumber/platform/actions/workflows/release.yml) -This project contains resources to setup a self-managed instance of [Plumber](https://getplumber.io/). +This repository contains everything needed to self-host [Plumber](https://getplumber.io/), the +CI/CD security & compliance control plane: [analysis stays in the open-source +CLI](https://github.com/getplumber/plumber), pushed here over native CI OIDC. This is the **v2** +line (compose-first, images published as `docker.io/getplumber/platform-backend` and +`docker.io/getplumber/platform-frontend`). Looking for the previous Helm/Compose/podman line? It +still lives, unchanged, in [`legacy-v1/`](legacy-v1/) - see `legacy-v1/README.md`. -## Installation +## Quick start (Docker Compose) -- 🐳 **Docker Compose** — [Documentation](https://getplumber.io/docs/installation/docker-compose/) +Requires Docker with the Compose plugin (`docker compose version`). - ```bash - curl -fsSL https://raw.githubusercontent.com/getplumber/platform/main/install.sh | bash - ``` +1. **Configure.** -- ☸️ **Kubernetes with Helm** — [Documentation](https://getplumber.io/docs/installation/kubernetes/) + ```bash + cp .env.example .env + ``` -## Contributions + Fill in every variable under "Required" in `.env` - the public URL, the GitLab instance(s) + allowed to push analysis results, and two secrets (`.env.example` gives you the exact + `openssl` command for each one). Leave the rest at their defaults for a first install; every + optional hardening knob is documented, commented out, in `.env.example` and `compose.yml`. + +2. **Start the stack.** + + ```bash + docker compose up -d + ``` + + This starts Postgres, Redis, the backend (runs its own database migrations on boot), and the + frontend. Give it a minute the first time - `docker compose ps` should show the backend and + frontend as `healthy` once migrations have applied and both apps are answering. + +3. **Bootstrap the first GitLab instance.** + + A fresh install has no configured GitLab connection yet, so there is nothing to log in + against. Run the bootstrap command once, inside the backend container, to configure it: + + ```bash + docker compose exec -e PLUMBER_BOOTSTRAP_CLIENT_SECRET= \ + backend plumber-bootstrap \ + -base-url https://gitlab.example.com \ + -client-id \ + -scope instance + ``` + + - `-base-url` and `-client-id` are required; create a GitLab OAuth application first (redirect + URI `/auth/callback`) and pass its client id/secret here. + - `-e PLUMBER_BOOTSTRAP_CLIENT_SECRET=...` keeps the secret out of the container's own process + list (`-client-secret ` also works but is both shell-history- and `ps`-visible inside + the container - prefer the env form). The same applies to `-token`/ + `PLUMBER_BOOTSTRAP_TOKEN` below. + - `-scope instance` connects every project on that GitLab instance; use `-scope group + -root-group ` instead to scope to one root group. Omit `-scope` entirely to leave the + connection scope unset for now (configure it later through the settings UI as an Admin). + - Optionally add `-e PLUMBER_BOOTSTRAP_TOKEN=` to validate and store an org token in + the same run (sealed before storage, never printed) - with `-scope group` this also resolves + the root group immediately. Without a token, the connection is stored but cannot sync until + an Admin adds one later through the settings UI. + - The command prints a login hint on success (it never prints a secret or token). It is + self-limiting: it refuses to touch an already-configured instance, so it is always safe to + re-run against a fresh install and impossible to run twice by accident against a live one. + +4. **Log in.** Visit `PLUMBER_BASE_URL` (via your reverse proxy - see the note in `compose.yml`'s + `frontend` service about why one is needed) and sign in with GitLab. The account behind the + very first login becomes this org's Admin automatically. -You are welcome to help us improve this repository! +## Upgrading + +1. Bump `PLATFORM_VERSION` in `.env` to the new version. +2. `docker compose pull && docker compose up -d`. + +Compose's default behavior (stop the old container, then start the replacement) already gives +single-version operation - nothing else to configure. Every migration this backend ships is +additive-only, so an in-place upgrade never requires a maintenance window for the schema itself; +sequential upgrades (one version at a time) are the supported, best-tested path. Rolling back is +just reverting `PLATFORM_VERSION` and re-running step 2 - never run a database downgrade against a +live install. + +## Kubernetes / Helm + +A v2 Helm chart is coming soon. Until then, `legacy-v1/charts/plumber` is the closest available +reference (it deploys v1, not v2 - the container images, environment variables, and data model are +different; do not point v1's chart at v2 images). + +## Legacy v1 + +The previous install line (Helm chart, Docker Compose with bundled Traefik, podman, `install.sh`) +still lives, unchanged, in [`legacy-v1/`](legacy-v1/). See `legacy-v1/README.md` for what moved and +why. + +## Contributions -🎮 Open an Issue or create Pull Requests from your fork \ No newline at end of file +You are welcome to help us improve this repository! Open an Issue or create a Pull Request from +your fork. diff --git a/compose.yml b/compose.yml index d2fe909..684eeeb 100644 --- a/compose.yml +++ b/compose.yml @@ -1,218 +1,153 @@ -name: "plumber" - -x-backend-common: &backend-common - image: docker.io/getplumber/backend:${BACKEND_IMAGE_TAG} - env_file: .env - # The backend runs as the rootless user uid:gid 65532:65532 (distroless nonroot) - # and therefore listens on a non-privileged port (>=1024). - environment: - - JOBS_LISTEN_ADDR=0.0.0.0 - - JOBS_LISTEN_PORT=3000 - - JOBS_CORS_ORIGIN=https://${DOMAIN_NAME} - - JOBS_FRONTEND_URL=https://${DOMAIN_NAME} - - JOBS_SESSION_TTL=168h - - JOBS_DB_HOST=${JOBS_DB_HOST:-postgres} - - JOBS_DB_PORT=${JOBS_DB_PORT:-5432} - - JOBS_DB_USER=${JOBS_DB_USER:-jobs} - - JOBS_DB_NAME=${JOBS_DB_NAME:-jobs} - - JOBS_DB_SSLMODE=${JOBS_DB_SSLMODE:-disable} - - JOBS_DB_TIMEZONE=${JOBS_DB_TIMEZONE:-Europe/Paris} - - JOBS_API_DOMAIN=https://${DOMAIN_NAME}/api - - LOG_LEVEL=info - - LOG_FORMATTER=text - - JOBS_REDIS_HOST=redis - - JOBS_REDIS_PORT=6379 - - JOBS_REDIS_DB=0 - - JOBS_REDIS_USER=default - - JOBS_REDIS_SET_NAMESPACES_TTL=30s - - GITLEAKS_PATH=/opt/gitleaks - - FRONTEND_DOMAIN=${DOMAIN_NAME} - - API_DOMAIN=${DOMAIN_NAME} - - API_PATH=/api - - API_URL=${DOMAIN_NAME}/api - restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - required: false - redis: - condition: service_healthy - volumes: - - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ - networks: - - intranet +# Plumber v2 (compose-first, release-process spec, root takeover 2026-08-25). +# +# What this file is: a minimal, working production stack for a single-node +# self-managed install - backend, frontend, Postgres, Redis. No `build:` +# sections (both apps run from the published images), no bundled reverse +# proxy / TLS terminator (see the note on the frontend service below - front +# this stack with your own reverse proxy for a real deployment). +# +# v1 (Helm chart, podman, the old Traefik-in-compose install) still lives, +# unchanged, in legacy-v1/ - see legacy-v1/README.md. This file is the NEW +# v2 line only; the two are not compatible and do not share a database. +# +# Copy .env.example to .env, fill it in (see that file for what every +# variable means and how to generate the secrets), then `docker compose up +# -d`. See the root README.md for the full quick start and the first-run +# bootstrap step. services: - - ############ - # Plumber # - ############ - - frontend: - image: docker.io/getplumber/frontend:${FRONTEND_IMAGE_TAG} - env_file: .env - environment: - - FRONTEND_DOMAIN=${DOMAIN_NAME} - - API_DOMAIN=${DOMAIN_NAME} - - API_PATH=/api - - API_URL=${DOMAIN_NAME}/api + postgres: + image: postgres:18-alpine restart: unless-stopped + environment: + POSTGRES_USER: ${PLUMBER_DB_USER} + POSTGRES_PASSWORD: ${PLUMBER_DB_PASSWORD} + POSTGRES_DB: ${PLUMBER_DB_NAME} volumes: - - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ - expose: - - "3000" - labels: - - "traefik.http.routers.front.rule=Host(`${DOMAIN_NAME}`)" - - "traefik.http.routers.front.entrypoints=websecure" - - "traefik.http.routers.front.tls=true" - - "traefik.http.routers.front.tls.certresolver=${CERT_RESOLVER:-}" - networks: - - intranet - - backend: - <<: *backend-common - expose: - - "3000" - - "9090" - labels: - - "traefik.http.routers.api.rule=Host(`${DOMAIN_NAME}`)&&PathPrefix(`/api`)" - - "traefik.http.routers.api.entrypoints=websecure" - - "traefik.http.routers.api.tls=true" - - "traefik.http.routers.api.tls.certresolver=${CERT_RESOLVER:-}" - - "traefik.http.services.api.loadbalancer.server.port=3000" - - worker: - <<: *backend-common - command: ["--worker"] - deploy: - mode: replicated - replicas: 5 - expose: - - "9090" - labels: - - "traefik.enable=false" - - - ##################### - # External services # - ##################### - - redis: - image: redis:8.4 - pull_policy: always - restart: unless-stopped - env_file: - - .env - command: - - redis-server - - --requirepass ${JOBS_REDIS_PASSWORD} + - postgres_data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "redis-cli -a \"$$JOBS_REDIS_PASSWORD\" ping"] + test: ["CMD-SHELL", "pg_isready -U ${PLUMBER_DB_USER} -d ${PLUMBER_DB_NAME}"] interval: 5s - timeout: 3s - retries: 20 - start_period: 5s - expose: - - "6379" - labels: - - "traefik.enable=false" - networks: - - intranet + timeout: 5s + retries: 10 - postgres: - profiles: ["internal-db"] - image: postgres:18 - pull_policy: always + redis: + image: redis:7-alpine restart: unless-stopped volumes: - - postgres-data:/var/lib/postgresql - env_file: .env - environment: - - POSTGRES_USER=${JOBS_DB_USER:-jobs} - - POSTGRES_PASSWORD=${JOBS_DB_PASSWORD} - - POSTGRES_DB=${JOBS_DB_NAME:-jobs} - - PGDATA=/var/lib/postgresql/18/data - labels: - - "traefik.enable=false" + - redis_data:/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB -h 127.0.0.1 -p 5432"] + test: ["CMD", "redis-cli", "ping"] interval: 5s - timeout: 3s - retries: 20 - start_period: 10s - expose: - - "5432" - networks: - - intranet - - ################# - # Reverse proxy # - ################# + timeout: 5s + retries: 10 - traefik-le: - profiles: ["letsencrypt"] - image: traefik:3.6 - pull_policy: always + backend: + image: docker.io/getplumber/platform-backend:${PLATFORM_VERSION} restart: unless-stopped depends_on: - - frontend - - backend + postgres: {condition: service_healthy} + redis: {condition: service_healthy} ports: - - "80:80" - - "443:443" - command: - - --log.level=DEBUG - - --accesslog=true - - --entrypoints.web.address=:80 - - --entrypoints.web.http.redirections.entryPoint.to=websecure - - --entrypoints.web.http.redirections.entryPoint.scheme=https - - --entrypoints.web.http.redirections.entrypoint.permanent=true - - --entrypoints.websecure.address=:443 - - --providers.docker=true - - --certificatesresolvers.le.acme.caserver=https://acme-v02.api.letsencrypt.org/directory - - --certificatesresolvers.le.acme.storage=/acme/acme.json - - --certificatesresolvers.le.acme.tlschallenge=true - labels: - - "traefik.enable=false" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - traefik-acme:/acme - networks: - - intranet + - "${BACKEND_PORT:-8080}:8080" + environment: + # Compose-internal hostnames (postgres/redis are sibling services on + # this same compose network) - not operator-configurable through + # .env; edit this file directly if you point at an external DB/Redis + # instead of the bundled ones above. + PLUMBER_DB_HOST: postgres + PLUMBER_DB_PORT: "5432" + PLUMBER_REDIS_ADDR: redis:6379 + + # Required - see .env.example for what each one means and how to + # generate it. + PLUMBER_DB_USER: ${PLUMBER_DB_USER} + PLUMBER_DB_PASSWORD: ${PLUMBER_DB_PASSWORD} + PLUMBER_DB_NAME: ${PLUMBER_DB_NAME} + PLUMBER_BASE_URL: ${PLUMBER_BASE_URL} + PLUMBER_OIDC_AUDIENCE: ${PLUMBER_OIDC_AUDIENCE} + PLUMBER_OIDC_ALLOWED_ISSUERS: ${PLUMBER_OIDC_ALLOWED_ISSUERS} + PLUMBER_TOKEN_ENCRYPTION_KEY: ${PLUMBER_TOKEN_ENCRYPTION_KEY} - traefik-custom-certs: - profiles: ["custom-certs"] - image: traefik:3.6 - pull_policy: always + # --- Optional hardening knobs, all commented (defaults shown match + # the backend's own built-in defaults). Uncomment and set in .env, + # not here, then reference with ${...} the same way as above. + # + # Postgres TLS (only meaningful if PLUMBER_DB_HOST points at a real + # network hop, not the compose-private postgres service above): + # PLUMBER_DB_SSLMODE: ${PLUMBER_DB_SSLMODE:-disable} # disable|allow|prefer|require|verify-ca|verify-full + # PLUMBER_DB_SSLROOTCERT: ${PLUMBER_DB_SSLROOTCERT:-} # private CA PEM path (verify-ca/verify-full) + # PLUMBER_DB_SSLCERT: ${PLUMBER_DB_SSLCERT:-} # client cert PEM path (mutual TLS) + # PLUMBER_DB_SSLKEY: ${PLUMBER_DB_SSLKEY:-} # client key PEM path (mutual TLS) + # + # Refuse a loopback/private/link-local base_url or SMTP relay by + # mistake (off by default: on-prem/private GitLab is the common, + # legitimate case): + # PLUMBER_BASE_URL_REJECT_PRIVATE: ${PLUMBER_BASE_URL_REJECT_PRIVATE:-false} + # PLUMBER_REPORT_SMTP_REJECT_PRIVATE_RELAY: ${PLUMBER_REPORT_SMTP_REJECT_PRIVATE_RELAY:-false} + # + # Behind a reverse proxy/ingress: trust its X-Forwarded-For hop for + # the inbound rate limiter's per-IP key (off by default - enabling + # this without a real proxy in front lets any caller spoof its own + # rate-limit key): + # PLUMBER_TRUST_FORWARDED_FOR: ${PLUMBER_TRUST_FORWARDED_FOR:-false} + # PLUMBER_TRUSTED_PROXY_CIDRS: ${PLUMBER_TRUSTED_PROXY_CIDRS:-} + # + # Air-gapped install: stop the daily update-check dial-out entirely: + # PLUMBER_UPDATE_CHECK_URL: ${PLUMBER_UPDATE_CHECK_URL:-} + # + # Once this stack is served over HTTPS (through your own reverse + # proxy - see the frontend service note below), mark session cookies + # Secure: + # PLUMBER_COOKIE_SECURE: ${PLUMBER_COOKIE_SECURE:-false} + # + # Runs/snapshots/sync-attempt retention window in days (floor 2): + # PLUMBER_RETENTION_DAYS: ${PLUMBER_RETENTION_DAYS:-30} + # + # Trust an internal CA fronting your GitLab instance (PEM path, + # mounted below via an added `volumes:` entry on this service): + # PLUMBER_PROVIDER_CA_BUNDLE: ${PLUMBER_PROVIDER_CA_BUNDLE:-} + # volumes: + # - /path/to/gitlab-ca.pem:/etc/plumber/gitlab-ca.pem:ro # pair with PLUMBER_PROVIDER_CA_BUNDLE above + healthcheck: + # /readyz (db-backed readiness), not /health - see the frontend's own + # dependency on this healthcheck below. + test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/readyz"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 20s + + frontend: + image: docker.io/getplumber/platform-frontend:${PLATFORM_VERSION} restart: unless-stopped depends_on: - - frontend - - backend + backend: {condition: service_healthy} ports: - - "80:80" - - "443:443" - command: - - --log.level=DEBUG - - --accesslog=true - - --entrypoints.web.address=:80 - - --entrypoints.web.http.redirections.entryPoint.to=websecure - - --entrypoints.web.http.redirections.entryPoint.scheme=https - - --entrypoints.web.http.redirections.entrypoint.permanent=true - - --entrypoints.websecure.address=:443 - - --providers.docker=true - - --providers.file.filename=/etc/traefik/certs.yml - labels: - - "traefik.enable=false" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./.docker/traefik/certs:/certs - - ./.docker/traefik/certs.yml:/etc/traefik/certs.yml - networks: - - intranet - -networks: - intranet: + - "${FRONTEND_PORT:-3000}:3000" + environment: + # Server-side calls (page rendering) reach the backend directly over + # the compose network - this always works, with or without a reverse + # proxy in front. + API_INTERNAL_URL: http://backend:8080/api + # IMPORTANT: the BROWSER talks to the API at the same origin, under + # /api (no client-side config, no NEXT_PUBLIC_* - see + # platform/frontend's own env.ts). This compose file exposes backend + # and frontend on two SEPARATE host ports, which is fine for local use + # or if you reach each service directly, but does NOT give the browser + # one shared origin. For a real deployment, put a reverse proxy in + # front of both that routes /api/* to backend:8080 and everything else + # to frontend:3000 on one public hostname (that hostname is + # PLUMBER_BASE_URL) - and terminate TLS there. This file deliberately + # does not bundle one (kept minimal, BYO reverse proxy); v1's Traefik + # setup in legacy-v1/ is a worked reference if you want one. + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 20s volumes: - postgres-data: - traefik-acme: + postgres_data: + redis_data: diff --git a/.docker/ca-certificates/.gitkeep b/legacy-v1/.docker/ca-certificates/.gitkeep similarity index 100% rename from .docker/ca-certificates/.gitkeep rename to legacy-v1/.docker/ca-certificates/.gitkeep diff --git a/.docker/traefik/certs.yml b/legacy-v1/.docker/traefik/certs.yml similarity index 100% rename from .docker/traefik/certs.yml rename to legacy-v1/.docker/traefik/certs.yml diff --git a/.docker/traefik/certs/.gitkeep b/legacy-v1/.docker/traefik/certs/.gitkeep similarity index 100% rename from .docker/traefik/certs/.gitkeep rename to legacy-v1/.docker/traefik/certs/.gitkeep diff --git a/legacy-v1/.env.example b/legacy-v1/.env.example new file mode 100644 index 0000000..ba9bc13 --- /dev/null +++ b/legacy-v1/.env.example @@ -0,0 +1,29 @@ +########################################################################## +# Plumber configuration file # +# Documentation: https://getplumber.io/docs/installation/docker-compose/ # +########################################################################## + +# Required configuration +DOMAIN_NAME="" +JOBS_GITLAB_URL="" +ORGANIZATION="" +GITLAB_OAUTH2_CLIENT_ID="" +GITLAB_OAUTH2_CLIENT_SECRET="" + +# Auto-generated secrets (populated by install script) +SECRET_KEY="" +JOBS_DB_PASSWORD="" +JOBS_REDIS_PASSWORD="" + +# Deployment profile (set by install script) +# Options: letsencrypt or custom-certs, combined with internal-db or omit for external +COMPOSE_PROFILES="letsencrypt,internal-db" +CERT_RESOLVER="le" + +# Optional: Override database defaults (only needed when NOT using internal-db profile) +# JOBS_DB_HOST="your-db-host" +# JOBS_DB_PORT="5432" +# JOBS_DB_USER="jobs" +# JOBS_DB_NAME="jobs" +# JOBS_DB_SSLMODE="disable" +# JOBS_DB_TIMEZONE="Europe/Paris" diff --git a/.env.local.example b/legacy-v1/.env.local.example similarity index 100% rename from .env.local.example rename to legacy-v1/.env.local.example diff --git a/legacy-v1/README.md b/legacy-v1/README.md new file mode 100644 index 0000000..f80f781 --- /dev/null +++ b/legacy-v1/README.md @@ -0,0 +1,47 @@ +# Plumber v1 (legacy) + +The v1 self-managed line lives here, **unchanged**. Everything under this directory (`compose.yml`, +`compose.local.yml`, `.env.example`, `.env.local.example`, `configmap.yml.example`, +`configmap.local.yml.example`, `podman.yml.example`, `podman.local.yml.example`, `.docker/`, +`charts/`, `install.sh`, `scripts/`, `versions.env`) is exactly what used to live at the repository +root, moved here as a unit on 2026-08-25 when v2 took over the root layout (compose-first, see the +root `README.md`). Nothing inside this directory was edited as part of that move: every script's +own relative references (`compose.yml`, `versions.env`, `scripts/*.sh`) still resolve, because the +whole set moved together. + +## Why v1 still lives here + +v1 (the Helm/Compose/podman install described here) is still supported for existing installs. It is +not being deprecated by this move, only relocated so the repository root can carry the new v2 line +(a different product generation, incompatible configuration, `docker.io/getplumber/platform-*` +images instead of v1's own). Run v1 exactly as documented below, from inside this directory. + +## Installing v1 + +- **Docker Compose**: + + ```bash + curl -fsSL https://raw.githubusercontent.com/getplumber/platform/main/legacy-v1/install.sh | bash + ``` + +- **Kubernetes with Helm**: see `charts/plumber/README.md` in this directory. + +- **Podman**: see `podman.yml.example` / `podman.local.yml.example` in this directory, and + `scripts/backup_podman.sh` / `scripts/restore_podman.sh`. + +## A note on documentation links + +The public docs site (getplumber.io/docs/installation/...) linked v1's Compose/Helm instructions +at their old repository-root paths. **Those links now need the `legacy-v1/` path prefix** (e.g. a +raw-file link to `install.sh` becomes `.../platform/main/legacy-v1/install.sh`, a link into +`charts/plumber` becomes `.../platform/main/legacy-v1/charts/plumber`). This directory does not fix +the site itself; whoever owns getplumber.io's docs content needs to update those links separately. + +## Upgrading within v1 + +Nothing about v1's own upgrade story changed: `scripts/update.sh` still reads `versions.env` from +this directory and syncs image tags into your `.env`, exactly as before the move. + +## v2 + +For the new install line, see the repository root `README.md`. diff --git a/charts/plumber/CONTIBUTING.md b/legacy-v1/charts/plumber/CONTIBUTING.md similarity index 100% rename from charts/plumber/CONTIBUTING.md rename to legacy-v1/charts/plumber/CONTIBUTING.md diff --git a/charts/plumber/Chart.yaml b/legacy-v1/charts/plumber/Chart.yaml similarity index 100% rename from charts/plumber/Chart.yaml rename to legacy-v1/charts/plumber/Chart.yaml diff --git a/charts/plumber/README.md b/legacy-v1/charts/plumber/README.md similarity index 100% rename from charts/plumber/README.md rename to legacy-v1/charts/plumber/README.md diff --git a/charts/plumber/templates/NOTES.txt b/legacy-v1/charts/plumber/templates/NOTES.txt similarity index 100% rename from charts/plumber/templates/NOTES.txt rename to legacy-v1/charts/plumber/templates/NOTES.txt diff --git a/charts/plumber/templates/_helpers.tpl b/legacy-v1/charts/plumber/templates/_helpers.tpl similarity index 100% rename from charts/plumber/templates/_helpers.tpl rename to legacy-v1/charts/plumber/templates/_helpers.tpl diff --git a/charts/plumber/templates/configmap.yaml b/legacy-v1/charts/plumber/templates/configmap.yaml similarity index 100% rename from charts/plumber/templates/configmap.yaml rename to legacy-v1/charts/plumber/templates/configmap.yaml diff --git a/charts/plumber/templates/deployment.yaml b/legacy-v1/charts/plumber/templates/deployment.yaml similarity index 100% rename from charts/plumber/templates/deployment.yaml rename to legacy-v1/charts/plumber/templates/deployment.yaml diff --git a/charts/plumber/templates/ingress.yaml b/legacy-v1/charts/plumber/templates/ingress.yaml similarity index 100% rename from charts/plumber/templates/ingress.yaml rename to legacy-v1/charts/plumber/templates/ingress.yaml diff --git a/charts/plumber/templates/redis-deployment.yaml b/legacy-v1/charts/plumber/templates/redis-deployment.yaml similarity index 100% rename from charts/plumber/templates/redis-deployment.yaml rename to legacy-v1/charts/plumber/templates/redis-deployment.yaml diff --git a/charts/plumber/templates/redis-service.yaml b/legacy-v1/charts/plumber/templates/redis-service.yaml similarity index 100% rename from charts/plumber/templates/redis-service.yaml rename to legacy-v1/charts/plumber/templates/redis-service.yaml diff --git a/charts/plumber/templates/service.yaml b/legacy-v1/charts/plumber/templates/service.yaml similarity index 100% rename from charts/plumber/templates/service.yaml rename to legacy-v1/charts/plumber/templates/service.yaml diff --git a/charts/plumber/templates/statefulset-psql.yaml b/legacy-v1/charts/plumber/templates/statefulset-psql.yaml similarity index 100% rename from charts/plumber/templates/statefulset-psql.yaml rename to legacy-v1/charts/plumber/templates/statefulset-psql.yaml diff --git a/charts/plumber/tests/configmap_test.yaml b/legacy-v1/charts/plumber/tests/configmap_test.yaml similarity index 100% rename from charts/plumber/tests/configmap_test.yaml rename to legacy-v1/charts/plumber/tests/configmap_test.yaml diff --git a/charts/plumber/tests/deployment_test.yaml b/legacy-v1/charts/plumber/tests/deployment_test.yaml similarity index 100% rename from charts/plumber/tests/deployment_test.yaml rename to legacy-v1/charts/plumber/tests/deployment_test.yaml diff --git a/charts/plumber/values.yaml b/legacy-v1/charts/plumber/values.yaml similarity index 100% rename from charts/plumber/values.yaml rename to legacy-v1/charts/plumber/values.yaml diff --git a/compose.local.yml b/legacy-v1/compose.local.yml similarity index 100% rename from compose.local.yml rename to legacy-v1/compose.local.yml diff --git a/legacy-v1/compose.yml b/legacy-v1/compose.yml new file mode 100644 index 0000000..d2fe909 --- /dev/null +++ b/legacy-v1/compose.yml @@ -0,0 +1,218 @@ +name: "plumber" + +x-backend-common: &backend-common + image: docker.io/getplumber/backend:${BACKEND_IMAGE_TAG} + env_file: .env + # The backend runs as the rootless user uid:gid 65532:65532 (distroless nonroot) + # and therefore listens on a non-privileged port (>=1024). + environment: + - JOBS_LISTEN_ADDR=0.0.0.0 + - JOBS_LISTEN_PORT=3000 + - JOBS_CORS_ORIGIN=https://${DOMAIN_NAME} + - JOBS_FRONTEND_URL=https://${DOMAIN_NAME} + - JOBS_SESSION_TTL=168h + - JOBS_DB_HOST=${JOBS_DB_HOST:-postgres} + - JOBS_DB_PORT=${JOBS_DB_PORT:-5432} + - JOBS_DB_USER=${JOBS_DB_USER:-jobs} + - JOBS_DB_NAME=${JOBS_DB_NAME:-jobs} + - JOBS_DB_SSLMODE=${JOBS_DB_SSLMODE:-disable} + - JOBS_DB_TIMEZONE=${JOBS_DB_TIMEZONE:-Europe/Paris} + - JOBS_API_DOMAIN=https://${DOMAIN_NAME}/api + - LOG_LEVEL=info + - LOG_FORMATTER=text + - JOBS_REDIS_HOST=redis + - JOBS_REDIS_PORT=6379 + - JOBS_REDIS_DB=0 + - JOBS_REDIS_USER=default + - JOBS_REDIS_SET_NAMESPACES_TTL=30s + - GITLEAKS_PATH=/opt/gitleaks + - FRONTEND_DOMAIN=${DOMAIN_NAME} + - API_DOMAIN=${DOMAIN_NAME} + - API_PATH=/api + - API_URL=${DOMAIN_NAME}/api + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + required: false + redis: + condition: service_healthy + volumes: + - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ + networks: + - intranet + +services: + + ############ + # Plumber # + ############ + + frontend: + image: docker.io/getplumber/frontend:${FRONTEND_IMAGE_TAG} + env_file: .env + environment: + - FRONTEND_DOMAIN=${DOMAIN_NAME} + - API_DOMAIN=${DOMAIN_NAME} + - API_PATH=/api + - API_URL=${DOMAIN_NAME}/api + restart: unless-stopped + volumes: + - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ + expose: + - "3000" + labels: + - "traefik.http.routers.front.rule=Host(`${DOMAIN_NAME}`)" + - "traefik.http.routers.front.entrypoints=websecure" + - "traefik.http.routers.front.tls=true" + - "traefik.http.routers.front.tls.certresolver=${CERT_RESOLVER:-}" + networks: + - intranet + + backend: + <<: *backend-common + expose: + - "3000" + - "9090" + labels: + - "traefik.http.routers.api.rule=Host(`${DOMAIN_NAME}`)&&PathPrefix(`/api`)" + - "traefik.http.routers.api.entrypoints=websecure" + - "traefik.http.routers.api.tls=true" + - "traefik.http.routers.api.tls.certresolver=${CERT_RESOLVER:-}" + - "traefik.http.services.api.loadbalancer.server.port=3000" + + worker: + <<: *backend-common + command: ["--worker"] + deploy: + mode: replicated + replicas: 5 + expose: + - "9090" + labels: + - "traefik.enable=false" + + + ##################### + # External services # + ##################### + + redis: + image: redis:8.4 + pull_policy: always + restart: unless-stopped + env_file: + - .env + command: + - redis-server + - --requirepass ${JOBS_REDIS_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "redis-cli -a \"$$JOBS_REDIS_PASSWORD\" ping"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 5s + expose: + - "6379" + labels: + - "traefik.enable=false" + networks: + - intranet + + postgres: + profiles: ["internal-db"] + image: postgres:18 + pull_policy: always + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql + env_file: .env + environment: + - POSTGRES_USER=${JOBS_DB_USER:-jobs} + - POSTGRES_PASSWORD=${JOBS_DB_PASSWORD} + - POSTGRES_DB=${JOBS_DB_NAME:-jobs} + - PGDATA=/var/lib/postgresql/18/data + labels: + - "traefik.enable=false" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB -h 127.0.0.1 -p 5432"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 10s + expose: + - "5432" + networks: + - intranet + + ################# + # Reverse proxy # + ################# + + traefik-le: + profiles: ["letsencrypt"] + image: traefik:3.6 + pull_policy: always + restart: unless-stopped + depends_on: + - frontend + - backend + ports: + - "80:80" + - "443:443" + command: + - --log.level=DEBUG + - --accesslog=true + - --entrypoints.web.address=:80 + - --entrypoints.web.http.redirections.entryPoint.to=websecure + - --entrypoints.web.http.redirections.entryPoint.scheme=https + - --entrypoints.web.http.redirections.entrypoint.permanent=true + - --entrypoints.websecure.address=:443 + - --providers.docker=true + - --certificatesresolvers.le.acme.caserver=https://acme-v02.api.letsencrypt.org/directory + - --certificatesresolvers.le.acme.storage=/acme/acme.json + - --certificatesresolvers.le.acme.tlschallenge=true + labels: + - "traefik.enable=false" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - traefik-acme:/acme + networks: + - intranet + + traefik-custom-certs: + profiles: ["custom-certs"] + image: traefik:3.6 + pull_policy: always + restart: unless-stopped + depends_on: + - frontend + - backend + ports: + - "80:80" + - "443:443" + command: + - --log.level=DEBUG + - --accesslog=true + - --entrypoints.web.address=:80 + - --entrypoints.web.http.redirections.entryPoint.to=websecure + - --entrypoints.web.http.redirections.entryPoint.scheme=https + - --entrypoints.web.http.redirections.entrypoint.permanent=true + - --entrypoints.websecure.address=:443 + - --providers.docker=true + - --providers.file.filename=/etc/traefik/certs.yml + labels: + - "traefik.enable=false" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./.docker/traefik/certs:/certs + - ./.docker/traefik/certs.yml:/etc/traefik/certs.yml + networks: + - intranet + +networks: + intranet: + +volumes: + postgres-data: + traefik-acme: diff --git a/configmap.local.yml.example b/legacy-v1/configmap.local.yml.example similarity index 100% rename from configmap.local.yml.example rename to legacy-v1/configmap.local.yml.example diff --git a/configmap.yml.example b/legacy-v1/configmap.yml.example similarity index 100% rename from configmap.yml.example rename to legacy-v1/configmap.yml.example diff --git a/install.sh b/legacy-v1/install.sh similarity index 100% rename from install.sh rename to legacy-v1/install.sh diff --git a/podman.local.yml.example b/legacy-v1/podman.local.yml.example similarity index 100% rename from podman.local.yml.example rename to legacy-v1/podman.local.yml.example diff --git a/podman.yml.example b/legacy-v1/podman.yml.example similarity index 100% rename from podman.yml.example rename to legacy-v1/podman.yml.example diff --git a/scripts/add_helm_repo.sh b/legacy-v1/scripts/add_helm_repo.sh similarity index 100% rename from scripts/add_helm_repo.sh rename to legacy-v1/scripts/add_helm_repo.sh diff --git a/scripts/backup.sh b/legacy-v1/scripts/backup.sh similarity index 100% rename from scripts/backup.sh rename to legacy-v1/scripts/backup.sh diff --git a/scripts/backup_podman.sh b/legacy-v1/scripts/backup_podman.sh similarity index 100% rename from scripts/backup_podman.sh rename to legacy-v1/scripts/backup_podman.sh diff --git a/scripts/preflight.sh b/legacy-v1/scripts/preflight.sh similarity index 100% rename from scripts/preflight.sh rename to legacy-v1/scripts/preflight.sh diff --git a/scripts/restore.sh b/legacy-v1/scripts/restore.sh similarity index 100% rename from scripts/restore.sh rename to legacy-v1/scripts/restore.sh diff --git a/scripts/restore_podman.sh b/legacy-v1/scripts/restore_podman.sh similarity index 100% rename from scripts/restore_podman.sh rename to legacy-v1/scripts/restore_podman.sh diff --git a/scripts/update.sh b/legacy-v1/scripts/update.sh similarity index 100% rename from scripts/update.sh rename to legacy-v1/scripts/update.sh diff --git a/versions.env b/legacy-v1/versions.env similarity index 100% rename from versions.env rename to legacy-v1/versions.env diff --git a/releases/.gitkeep b/releases/.gitkeep new file mode 100644 index 0000000..e69de29