mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
feat: adding e2e tests for launching agent.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
PROJECT_URL=http://localhost:8887
|
||||
|
||||
E2E_NOTIFICATION_SMTP_HOST="smtp.lunaris-mail.test"
|
||||
E2E_NOTIFICATION_SMTP_PORT="587"
|
||||
E2E_NOTIFICATION_SMTP_USER="e2e_user_7f9a2c"
|
||||
E2E_NOTIFICATION_SMTP_PASSWORD="uR8vK3mZpQ92xTnB"
|
||||
E2E_NOTIFICATION_SMTP_FROM="e2e-sender@lunaris-mail.test"
|
||||
E2E_NOTIFICATION_SMTP_TO="e2e-recipient@lunaris-mail.test"
|
||||
E2E_NOTIFICATION_SLACK_WEBHOOK="https://hooks.slack.com/services/T00000000/B00000000/AbCdEfGhIjKlMnOpQrStUvWx"
|
||||
E2E_NOTIFICATION_DISCORD_WEBHOOK="https://discord.com/api/webhooks/123456789012345678/qtY8xKpLmN4vBzR6sWdE9uHaC3fGjT2"
|
||||
E2E_NOTIFICATION_TELEGRAM_BOT_TOKEN="7352198460:AAHkLm9vPqR2sTuVwXyZ1234567890abcd"
|
||||
E2E_NOTIFICATION_TELEGRAM_CHAT_ID="-1002847391826"
|
||||
E2E_NOTIFICATION_TELEGRAM_TOPIC_ID="42"
|
||||
E2E_NOTIFICATION_GOTIFY_SERVER_URL="https://gotify-e2e.lunaris.test"
|
||||
E2E_NOTIFICATION_GOTIFY_APP_TOKEN="gfy_8a92f3c1d7e64b0aa51c"
|
||||
E2E_NOTIFICATION_NTFY_TOPIC="e2e-alerts-7f9a2c"
|
||||
E2E_NOTIFICATION_NTFY_SERVER_URL="https://ntfy.sh"
|
||||
E2E_NOTIFICATION_NTFY_TOKEN="tk_e2e_9c7f4a1b6d2e8f0"
|
||||
E2E_NOTIFICATION_NTFY_USERNAME="ntfy_e2e_user"
|
||||
E2E_NOTIFICATION_NTFY_PASSWORD="NtFyP4ss_8rQ2zLm"
|
||||
E2E_NOTIFICATION_WEBHOOK_URL="https://webhook-e2e.lunaris.test/notify"
|
||||
E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER="X-E2E-Signature"
|
||||
E2E_NOTIFICATION_WEBHOOK_SECRET="whsec_2f8a9c4e71b6d3a0"
|
||||
|
||||
E2E_STORAGE_AWS_S3_ENDPOINT_URL="https://s3.eu-west-3.amazonaws.com"
|
||||
E2E_STORAGE_AWS_S3_REGION="eu-west-3"
|
||||
E2E_STORAGE_AWS_S3_ACCESS_KEY="AKIAE2E7F9A2C4D6B8QX"
|
||||
E2E_STORAGE_AWS_S3_SECRET_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
E2E_STORAGE_AWS_S3_BUCKET_NAME="e2e-bucket-7f9a2c"
|
||||
E2E_STORAGE_AWS_S3_PORT="443"
|
||||
|
||||
E2E_STORAGE_R2_ENDPOINT_URL="https://7f9a2c4d6b8q.r2.cloudflarestorage.com"
|
||||
E2E_STORAGE_R2_REGION="auto"
|
||||
E2E_STORAGE_R2_ACCESS_KEY="R2E2E7F9A2C4D6B8QX"
|
||||
E2E_STORAGE_R2_SECRET_KEY="r2sec_8vK3mZpQ92xTnBuR4sWdE9uHaC3fGjT2"
|
||||
E2E_STORAGE_R2_BUCKET_NAME="r2-e2e-bucket-7f9a2c"
|
||||
E2E_STORAGE_R2_PORT="443"
|
||||
|
||||
E2E_STORAGE_GOOGLE_DRIVE_CLIENT_ID="735219846012-e2e7f9a2c4d6b8q.apps.googleusercontent.com"
|
||||
E2E_STORAGE_GOOGLE_DRIVE_CLIENT_SECRET="GOCSPX-e2e_8vK3mZpQ92xTnBuR4sWd"
|
||||
E2E_STORAGE_GOOGLE_DRIVE_FOLDER_ID="1E2e7F9a2C4d6B8qXzLmNoPqRsTuVwXyZ"
|
||||
+1
-3
@@ -45,7 +45,6 @@ AUTH_OIDC_ROLE_MAP=""
|
||||
AUTH_ALLOW_LINKING=true
|
||||
AUTH_ALLOW_UNLINKING=true
|
||||
|
||||
|
||||
# Social OAuth2 Authentification
|
||||
AUTH_SOCIAL_ID=""
|
||||
AUTH_SOCIAL_TITLE=""
|
||||
@@ -65,5 +64,4 @@ AUTH_PASSKEY_ENABLED=true
|
||||
# Retention
|
||||
RETENTION_CRON="* * * * *"
|
||||
|
||||
TRUSTED_DOMAINS="http://localhost:8887, http://localhost:3055, http://localhost:3056"
|
||||
|
||||
TRUSTED_DOMAINS="http://localhost:8887, http://localhost:3055, http://localhost:3056, http://localhost:3057"
|
||||
|
||||
+57
-10
@@ -33,19 +33,64 @@ jobs:
|
||||
load: true
|
||||
tags: portabase/portabase:test
|
||||
|
||||
- name: Run app container
|
||||
- name: Create shared Docker network
|
||||
run: |
|
||||
docker run -d --name myapp \
|
||||
-p 8887:80 \
|
||||
-e NODE_ENV=production \
|
||||
-e PROJECT_URL=http://localhost:8887 \
|
||||
-e PROJECT_SECRET=80af5e4c875dfded3a6ba511c6ed8b0160cad723f848ee0851403ed6141f6ba1 \
|
||||
portabase/portabase:test
|
||||
docker network inspect portabase >/dev/null 2>&1 || \
|
||||
docker network create portabase
|
||||
|
||||
- name: Seed OIDC services
|
||||
run: make seed-auth
|
||||
|
||||
- name: Wait for OIDC providers
|
||||
run: |
|
||||
for i in {1..60}; do
|
||||
if curl -fsS http://localhost:3056/realms/master/.well-known/openid-configuration >/dev/null; then
|
||||
echo "Keycloak is ready"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 60 ]; then
|
||||
echo "Timeout: Keycloak never became ready"
|
||||
docker compose -f docker-compose.oidc.yml logs
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for Keycloak... ($i/60)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
for i in {1..60}; do
|
||||
if curl -fsS http://localhost:3055/healthz >/dev/null; then
|
||||
echo "Pocket ID is ready"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 60 ]; then
|
||||
echo "Timeout: Pocket ID never became ready"
|
||||
docker compose -f docker-compose.oidc.yml logs
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for Pocket ID... ($i/60)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
for i in {1..60}; do
|
||||
if curl -fsS http://localhost:3057/application/o/portabase/.well-known/openid-configuration >/dev/null; then
|
||||
echo "Authentik is ready"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 60 ]; then
|
||||
echo "Timeout: Authentik never became ready"
|
||||
docker compose -f docker-compose.oidc.yml logs
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for Authentik... ($i/60)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Start E2E app stack
|
||||
run: docker compose -f docker-compose.prod.yml up -d --no-build
|
||||
|
||||
- name: Wait for app port to be listening
|
||||
run: |
|
||||
for i in {1..40}; do
|
||||
|
||||
if curl -fsS http://localhost:8887/ >/dev/null; then
|
||||
echo "App is responding on port 8887"
|
||||
exit 0
|
||||
@@ -54,7 +99,7 @@ jobs:
|
||||
sleep 2
|
||||
done
|
||||
echo "Timeout: app never became ready"
|
||||
docker logs myapp
|
||||
docker compose -f docker-compose.prod.yml logs
|
||||
exit 1
|
||||
|
||||
- name: Install Playwright browsers
|
||||
@@ -114,4 +159,6 @@ jobs:
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: docker stop myapp && docker rm myapp || true
|
||||
run: |
|
||||
docker compose -f docker-compose.prod.yml down --volumes || true
|
||||
docker compose -f docker-compose.oidc.yml down --volumes || true
|
||||
|
||||
@@ -5,58 +5,73 @@ up:
|
||||
|
||||
seed-keycloak:
|
||||
@[ $$(ls -1 seeds/keycloak/*.json 2>/dev/null | wc -l) -gt 0 ] || (echo "No realm export found in seeds/keycloak. Add an export from Keycloak first."; exit 1)
|
||||
@docker compose -f docker-compose.func.yml stop keycloak >/dev/null 2>&1 || true
|
||||
@docker compose -f docker-compose.func.yml rm -f -s keycloak >/dev/null 2>&1 || true
|
||||
@docker volume rm portabase-dev-func_keycloak-data >/dev/null 2>&1 || true
|
||||
@docker compose -f docker-compose.func.yml up -d keycloak
|
||||
@docker compose -f docker-compose.oidc.yml stop keycloak >/dev/null 2>&1 || true
|
||||
@docker compose -f docker-compose.oidc.yml rm -f -s keycloak >/dev/null 2>&1 || true
|
||||
@docker volume rm portabase-oidc_keycloak-data >/dev/null 2>&1 || true
|
||||
@docker compose -f docker-compose.oidc.yml up -d keycloak
|
||||
@echo "Keycloak seed import triggered from seeds/keycloak/*.json"
|
||||
|
||||
seed-pocket:
|
||||
@[ -f seeds/pocket-id/portabase.zip ] || (echo "No export found in seeds/pocket-id. Add an export from Pocket ID first."; exit 1)
|
||||
@docker compose -f docker-compose.func.yml stop pocket-id >/dev/null 2>&1 || true
|
||||
@docker compose -f docker-compose.func.yml rm -f -s pocket-id >/dev/null 2>&1 || true
|
||||
@docker volume rm portabase-dev-func_pocket-id-data >/dev/null 2>&1 || true
|
||||
@docker compose -f docker-compose.func.yml run --rm -v $$(pwd)/seeds/pocket-id/portabase.zip:/tmp/portabase.zip pocket-id ./pocket-id import --yes --path /tmp/portabase.zip >/dev/null
|
||||
@docker compose -f docker-compose.func.yml up -d pocket-id
|
||||
@docker compose -f docker-compose.oidc.yml stop pocket-id >/dev/null 2>&1 || true
|
||||
@docker compose -f docker-compose.oidc.yml rm -f -s pocket-id >/dev/null 2>&1 || true
|
||||
@docker volume rm portabase-oidc_pocket-id-data >/dev/null 2>&1 || true
|
||||
@docker compose -f docker-compose.oidc.yml run --rm -v $$(pwd)/seeds/pocket-id/portabase.zip:/tmp/portabase.zip pocket-id ./pocket-id import --yes --path /tmp/portabase.zip >/dev/null
|
||||
@docker compose -f docker-compose.oidc.yml up -d pocket-id
|
||||
@sleep 2
|
||||
@docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id one-time-access-token admin
|
||||
@docker compose -f docker-compose.oidc.yml exec pocket-id ./pocket-id one-time-access-token admin
|
||||
@echo "Pocket ID data restored from seeds/pocket-id/portabase.zip"
|
||||
|
||||
seed-auth: seed-keycloak seed-pocket
|
||||
seed-authentik:
|
||||
@docker compose -f docker-compose.oidc.yml stop authentik-postgresql authentik-server authentik-worker >/dev/null 2>&1 || true
|
||||
@docker compose -f docker-compose.oidc.yml rm -f -s authentik-postgresql authentik-server authentik-worker >/dev/null 2>&1 || true
|
||||
@docker volume rm portabase-oidc_authentik-data >/dev/null 2>&1 || true
|
||||
@docker compose -f docker-compose.oidc.yml up -d authentik-postgresql authentik-server authentik-worker
|
||||
@docker compose -f docker-compose.oidc.yml run --rm authentik-bootstrap
|
||||
@echo "Authentik seed/bootstrap completed"
|
||||
|
||||
seed-auth: seed-keycloak seed-pocket seed-authentik
|
||||
|
||||
pocket-token:
|
||||
@docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id one-time-access-token admin
|
||||
@docker compose -f docker-compose.oidc.yml exec pocket-id ./pocket-id one-time-access-token admin
|
||||
|
||||
export-pocket:
|
||||
@echo "Exporting Pocket ID data to seeds/pocket-id/portabase.zip..."
|
||||
@mkdir -p ./seeds/pocket-id
|
||||
@docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id export --path /tmp/portabase-export.zip
|
||||
@docker compose -f docker-compose.func.yml cp pocket-id:/tmp/portabase-export.zip ./seeds/pocket-id/portabase.zip
|
||||
@docker compose -f docker-compose.func.yml exec pocket-id rm /tmp/portabase-export.zip
|
||||
@docker compose -f docker-compose.oidc.yml exec pocket-id ./pocket-id export --path /tmp/portabase-export.zip
|
||||
@docker compose -f docker-compose.oidc.yml cp pocket-id:/tmp/portabase-export.zip ./seeds/pocket-id/portabase.zip
|
||||
@docker compose -f docker-compose.oidc.yml exec pocket-id rm /tmp/portabase-export.zip
|
||||
@echo "Pocket ID data exported to seeds/pocket-id/portabase.zip"
|
||||
|
||||
export-keycloak:
|
||||
@echo "Exporting Keycloak configuration and users to seeds/keycloak/*.json..."
|
||||
@mkdir -p ./seeds/keycloak
|
||||
@rm -f ./seeds/keycloak/*.json
|
||||
@docker compose -f docker-compose.func.yml stop keycloak >/dev/null 2>&1
|
||||
@docker compose -f docker-compose.oidc.yml stop keycloak >/dev/null 2>&1
|
||||
@docker rm -f kc-exporter >/dev/null 2>&1 || true
|
||||
@docker compose -f docker-compose.func.yml run --name kc-exporter keycloak export --dir /tmp/kc-export --users realm_file
|
||||
@docker compose -f docker-compose.oidc.yml run --name kc-exporter keycloak export --dir /tmp/kc-export --users realm_file
|
||||
@docker cp kc-exporter:/tmp/kc-export/. ./seeds/keycloak/
|
||||
@docker rm -f kc-exporter >/dev/null 2>&1
|
||||
@docker compose -f docker-compose.func.yml start keycloak >/dev/null 2>&1
|
||||
@docker compose -f docker-compose.oidc.yml start keycloak >/dev/null 2>&1
|
||||
@echo "Keycloak configuration and users exported to seeds/keycloak/*.json"
|
||||
|
||||
end-to-end:
|
||||
e2e-auto:
|
||||
@echo "Starting E2E testing..."
|
||||
@docker compose -f docker-compose.e2e.yml up -d
|
||||
@CI=true PROJECT_URL=http://localhost:8887 pnpm playwright test --project=chromium || (docker compose -f docker-compose.e2e.yml down --volumes && exit 1)
|
||||
@docker compose -f docker-compose.e2e.yml down --volumes
|
||||
@docker network inspect portabase >/dev/null 2>&1 || docker network create portabase
|
||||
@$(MAKE) seed-auth
|
||||
@docker compose -f docker-compose.prod.yml up -d
|
||||
@CI=true PROJECT_URL=http://localhost:8887 pnpm playwright test --project=chromium || (docker compose -f docker-compose.prod.yml down --volumes && docker compose -f docker-compose.oidc.yml down --volumes && exit 1)
|
||||
@docker compose -f docker-compose.prod.yml down --volumes
|
||||
@docker compose -f docker-compose.oidc.yml down --volumes
|
||||
@echo "Finished E2E testing successfully."
|
||||
|
||||
e2e-manual:
|
||||
@echo "Starting E2E testing..."
|
||||
@docker compose -f docker-compose.e2e.yml up -d
|
||||
@pnpm playwright test --ui || (docker compose -f docker-compose.e2e.yml down --volumes && exit 1)
|
||||
@docker compose -f docker-compose.e2e.yml down --volumes
|
||||
@echo "Finished E2E testing successfully."
|
||||
@docker network inspect portabase >/dev/null 2>&1 || docker network create portabase
|
||||
@docker compose -f e2e/helpers/agent/docker-compose.database.yml up -d
|
||||
@$(MAKE) seed-auth
|
||||
@docker compose -f docker-compose.prod.yml up -d
|
||||
@pnpm playwright test --ui || (docker compose -f docker-compose.prod.yml down --volumes && docker compose -f docker-compose.oidc.yml down --volumes && exit 1)
|
||||
@docker compose -f docker-compose.prod.yml down --volumes
|
||||
@docker compose -f docker-compose.oidc.yml down --volumes
|
||||
@echo "Finished E2E testing successfully."
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/dockerfile/Dockerfile
|
||||
target: prod
|
||||
ports:
|
||||
- '8887:80'
|
||||
environment:
|
||||
TZ: "Europe/Paris"
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- portabase-e2e-data:/data
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: postgres:18-alpine
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- postgres-e2e-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=devdb
|
||||
- POSTGRES_USER=devuser
|
||||
- POSTGRES_PASSWORD=changeme
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
portabase-e2e-data:
|
||||
postgres-e2e-data:
|
||||
@@ -1,35 +0,0 @@
|
||||
name: portabase-dev-func
|
||||
|
||||
services:
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:latest
|
||||
command: start-dev --import-realm
|
||||
# environment:
|
||||
# KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||
# KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||
ports:
|
||||
- "3056:8080"
|
||||
volumes:
|
||||
- keycloak-data:/opt/keycloak/data
|
||||
- ./seeds/keycloak:/opt/keycloak/data/import:ro
|
||||
- ./export:/tmp/export
|
||||
pocket-id:
|
||||
image: ghcr.io/pocket-id/pocket-id
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- APP_URL=http://localhost:3055
|
||||
- ENCRYPTION_KEY=QwHyjbZvSsDUAcjpdmSPsuYxaH6vET6OeBaeLwXccCb43L6Om3W1AoU5pKIJTzYr
|
||||
ports:
|
||||
- 3055:1411
|
||||
volumes:
|
||||
- pocket-id-data:/app/data
|
||||
- ./seeds/pocket-id:/seed:ro
|
||||
healthcheck:
|
||||
test: "curl -f http://localhost:1411/healthz"
|
||||
interval: 1m30s
|
||||
timeout: 5s
|
||||
retries: 2
|
||||
start_period: 10s
|
||||
volumes:
|
||||
keycloak-data:
|
||||
pocket-id-data:
|
||||
@@ -0,0 +1,138 @@
|
||||
name: portabase-oidc
|
||||
|
||||
services:
|
||||
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:latest
|
||||
restart: unless-stopped
|
||||
command: start-dev --import-realm
|
||||
environment:
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME: bootstrap-admin
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||
KC_HEALTH_ENABLED: "true"
|
||||
ports:
|
||||
- "3056:8080"
|
||||
volumes:
|
||||
- keycloak-data:/opt/keycloak/data
|
||||
- ./seeds/keycloak:/opt/keycloak/data/import:ro
|
||||
- ./export:/tmp/export
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "bash -ec '</dev/tcp/127.0.0.1/8080'" ]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
pocket-id:
|
||||
image: ghcr.io/pocket-id/pocket-id
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- APP_URL=http://localhost:3055
|
||||
- ENCRYPTION_KEY=QwHyjbZvSsDUAcjpdmSPsuYxaH6vET6OeBaeLwXccCb43L6Om3W1AoU5pKIJTzYr
|
||||
ports:
|
||||
- "3055:1411"
|
||||
volumes:
|
||||
- pocket-id-data:/app/data
|
||||
- ./seeds/pocket-id:/seed:ro
|
||||
healthcheck:
|
||||
test: "curl -f http://localhost:1411/healthz"
|
||||
interval: 1m30s
|
||||
timeout: 5s
|
||||
retries: 2
|
||||
start_period: 10s
|
||||
|
||||
authentik-server:
|
||||
image: ghcr.io/goauthentik/server:2026.2.3
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
AUTHENTIK_POSTGRESQL__HOST: authentik-postgresql
|
||||
AUTHENTIK_POSTGRESQL__USER: authentik
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: authentik
|
||||
AUTHENTIK_POSTGRESQL__NAME: authentik
|
||||
AUTHENTIK_SECRET_KEY: changemechangemechangemechangeme
|
||||
ports:
|
||||
- "3057:9000"
|
||||
depends_on:
|
||||
authentik-postgresql:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:9000/-/health/live/')\"" ]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
authentik-postgresql:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: authentik
|
||||
POSTGRES_USER: authentik
|
||||
POSTGRES_PASSWORD: authentik
|
||||
volumes:
|
||||
- authentik-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U authentik -d authentik" ]
|
||||
start_period: 20s
|
||||
interval: 30s
|
||||
retries: 5
|
||||
timeout: 5s
|
||||
|
||||
authentik-worker:
|
||||
image: ghcr.io/goauthentik/server:2026.2.3
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
user: root
|
||||
environment:
|
||||
AUTHENTIK_POSTGRESQL__HOST: authentik-postgresql
|
||||
AUTHENTIK_POSTGRESQL__USER: authentik
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: authentik
|
||||
AUTHENTIK_POSTGRESQL__NAME: authentik
|
||||
AUTHENTIK_SECRET_KEY: changemechangemechangemechangeme
|
||||
AUTHENTIK_BOOTSTRAP_PASSWORD: portabase-bootstrap-password
|
||||
AUTHENTIK_BOOTSTRAP_TOKEN: portabase-bootstrap-token
|
||||
AUTHENTIK_BOOTSTRAP_EMAIL: akadmin@portabase.local
|
||||
volumes:
|
||||
- ./seeds/authentik/blueprints/portabase-oidc.yaml:/blueprints/portabase-oidc.yaml:ro
|
||||
shm_size: 512mb
|
||||
depends_on:
|
||||
authentik-postgresql:
|
||||
condition: service_healthy
|
||||
|
||||
authentik-bootstrap:
|
||||
image: ghcr.io/goauthentik/server:2026.2.3
|
||||
entrypoint: [ "/bin/sh", "/seeds/bootstrap.sh" ]
|
||||
environment:
|
||||
AUTHENTIK_BASE_URL: http://authentik-server:9000
|
||||
AUTHENTIK_API_TOKEN: portabase-bootstrap-token
|
||||
AUTHENTIK_DISCOVERY_PATH: /application/o/portabase/.well-known/openid-configuration
|
||||
AUTHENTIK_SECRET_KEY: changemechangemechangemechangeme
|
||||
AUTHENTIK_POSTGRESQL__HOST: authentik-postgresql
|
||||
AUTHENTIK_POSTGRESQL__USER: authentik
|
||||
AUTHENTIK_POSTGRESQL__PASSWORD: authentik
|
||||
AUTHENTIK_POSTGRESQL__NAME: authentik
|
||||
AUTHENTIK_BOOTSTRAP_EMAIL: akadmin@portabase.local
|
||||
AUTHENTIK_TEST_USER_USERNAME: admin@example.com
|
||||
AUTHENTIK_TEST_USER_EMAIL: admin@example.com
|
||||
AUTHENTIK_TEST_USER_NAME: Portabase Admin
|
||||
AUTHENTIK_TEST_USER_PASSWORD: testPASS123456!
|
||||
AUTHENTIK_TEST_GROUP: admin
|
||||
AUTHENTIK_PROVIDER_NAME: portabase
|
||||
AUTHENTIK_APPLICATION_NAME: Portabase
|
||||
AUTHENTIK_APPLICATION_SLUG: portabase
|
||||
AUTHENTIK_CLIENT_ID: portabase
|
||||
AUTHENTIK_CLIENT_SECRET: portabase-e2e-secret
|
||||
AUTHENTIK_REDIRECT_URI: http://localhost:8887/api/auth/sso/callback/authentik
|
||||
volumes:
|
||||
- ./seeds/authentik:/seeds:ro
|
||||
depends_on:
|
||||
authentik-server:
|
||||
condition: service_healthy
|
||||
authentik-worker:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
keycloak-data:
|
||||
pocket-id-data:
|
||||
authentik-data:
|
||||
+15
-14
@@ -1,35 +1,36 @@
|
||||
name: portabase-prod
|
||||
|
||||
services:
|
||||
|
||||
app:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: docker/dockerfile/Dockerfile
|
||||
# target: prod
|
||||
image: portabase/portabase:1
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/dockerfile/Dockerfile
|
||||
target: prod
|
||||
ports:
|
||||
- '8887:80'
|
||||
environment:
|
||||
TZ: "Europe/Paris"
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- portabase-data:/data
|
||||
- portabase-prod-data:/data
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
container_name: portabase-app-prod
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost/api/health"]
|
||||
test: [ "CMD-SHELL", "curl -f http://localhost/api/health" ]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
extra_hosts:
|
||||
- "localhost:host-gateway"
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
image: postgres:17-alpine
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
- portabase-prod-postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=devdb
|
||||
- POSTGRES_USER=devuser
|
||||
@@ -41,5 +42,5 @@ services:
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
portabase-data:
|
||||
postgres-data:
|
||||
portabase-prod-data:
|
||||
portabase-prod-postgres-data:
|
||||
+2
-2
@@ -6,7 +6,7 @@ services:
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
- portabase-dev-postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=devdb
|
||||
- POSTGRES_USER=devuser
|
||||
@@ -42,4 +42,4 @@ services:
|
||||
- "1025:1025"
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
portabase-dev-postgres-data:
|
||||
|
||||
+14
-51
@@ -1,10 +1,5 @@
|
||||
import {expect, test} from "@playwright/test";
|
||||
import {execSync} from "child_process";
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import {createAgentWithDockerDatabases} from "./helpers/agent-cli";
|
||||
import {create, edit, get, remove} from "./helpers/agent";
|
||||
import {create, edit, get, launch, remove} from "./helpers/agent";
|
||||
import {LOCAL_STORAGE_PATH} from "./helpers/session";
|
||||
|
||||
const agent = {
|
||||
@@ -64,53 +59,21 @@ test.describe.serial(() => {
|
||||
await expect(page.getByText(agent.bName)).toHaveCount(0);
|
||||
});
|
||||
|
||||
// test("Launch the updated agent", async ({page}) => {
|
||||
// await page.goto("/dashboard/agents");
|
||||
// await expect(page.getByRole("heading", {name: "Agents"})).toBeVisible();
|
||||
// await get(page, agent.aName).click();
|
||||
//
|
||||
// await expect(page).toHaveURL(/\/dashboard\/agents\/.+/);
|
||||
// await expect(page.getByText(agent.aName, {exact: true})).toBeVisible();
|
||||
// await expect(page.getByText("Registration & Setup")).toBeVisible();
|
||||
//
|
||||
// const commandInput = page.locator("input[readonly]").first();
|
||||
// await page.locator("input[readonly]").first().locator("xpath=following-sibling::button[1]").click();
|
||||
// const command = await commandInput.inputValue();
|
||||
//
|
||||
// agentWorkspace = fs.mkdtempSync(path.join(os.tmpdir(), "portabase-agent-"));
|
||||
// await createAgentWithDockerDatabases(command, agentWorkspace);
|
||||
// execSync(`portabase start "${agent.aName}"`, {
|
||||
// cwd: agentWorkspace,
|
||||
// stdio: "pipe",
|
||||
// timeout: 120_000,
|
||||
// });
|
||||
//
|
||||
// await expect(page.getByText("Never connected.")).toHaveCount(0, {timeout: 120_000});
|
||||
// await expect(page.getByText("Action Required")).toHaveCount(0);
|
||||
// });
|
||||
test("Launch the updated agent", async ({page}) => {
|
||||
await page.goto("/dashboard/agents");
|
||||
await expect(page.getByRole("heading", {name: "Agents"})).toBeVisible();
|
||||
await get(page, agent.aName).click();
|
||||
|
||||
test.afterAll(async () => {
|
||||
if (agentWorkspace) {
|
||||
try {
|
||||
execSync(`portabase stop "${agent.aName}"`, {
|
||||
cwd: agentWorkspace,
|
||||
stdio: "pipe",
|
||||
timeout: 30_000,
|
||||
});
|
||||
} catch {
|
||||
}
|
||||
await expect(page).toHaveURL(/\/dashboard\/agents\/.+/);
|
||||
await expect(page.getByText(agent.aUpdatedName, {exact: true})).toBeVisible();
|
||||
await expect(page.getByText("Registration & Setup")).toBeVisible();
|
||||
|
||||
try {
|
||||
execSync(`portabase uninstall --force "${agent.aName}"`, {
|
||||
cwd: agentWorkspace,
|
||||
stdio: "pipe",
|
||||
timeout: 30_000,
|
||||
});
|
||||
} catch {
|
||||
}
|
||||
const commandInput = page.locator("input[readonly]").last();
|
||||
await page.locator("input[readonly]").first().locator("xpath=following-sibling::button[1]").click();
|
||||
const edgeKey = await commandInput.inputValue();
|
||||
|
||||
fs.rmSync(agentWorkspace, {recursive: true, force: true});
|
||||
agentWorkspace = null;
|
||||
}
|
||||
await launch(edgeKey)
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Managed Databases" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import {Page} from "@playwright/test";
|
||||
import {execSync} from "node:child_process";
|
||||
|
||||
|
||||
/**
|
||||
@@ -64,3 +65,16 @@ export async function remove(page: Page, name: string) {
|
||||
await page.getByRole("button", {name: /Delete Agent/i}).click();
|
||||
await page.getByRole("button", {name: "Delete", exact: true}).click();
|
||||
}
|
||||
|
||||
|
||||
export async function launch(edgeKey: string) {
|
||||
const output = execSync(
|
||||
`EDGE_KEY=${edgeKey} docker compose -f e2e/helpers/agent/docker-compose.agent-a.yml up -d`,
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
},
|
||||
);
|
||||
return output;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"databases": [
|
||||
{
|
||||
"name": "PostgreSQL 18",
|
||||
"database": "backup_e2e",
|
||||
"type": "postgresql",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 5432,
|
||||
"host": "postgresql-18",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef001"
|
||||
},
|
||||
{
|
||||
"name": "PostgreSQL 17",
|
||||
"database": "backup_e2e",
|
||||
"type": "postgresql",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 5432,
|
||||
"host": "postgresql-17",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef002"
|
||||
},
|
||||
{
|
||||
"name": "PostgreSQL 16",
|
||||
"database": "backup_e2e",
|
||||
"type": "postgresql",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 5432,
|
||||
"host": "postgresql-16",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef003"
|
||||
},
|
||||
{
|
||||
"name": "PostgreSQL 15",
|
||||
"database": "backup_e2e",
|
||||
"type": "postgresql",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 5432,
|
||||
"host": "postgresql-15",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef004"
|
||||
},
|
||||
{
|
||||
"name": "PostgreSQL 14",
|
||||
"database": "backup_e2e",
|
||||
"type": "postgresql",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 5432,
|
||||
"host": "postgresql-14",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef005"
|
||||
},
|
||||
{
|
||||
"name": "PostgreSQL 13",
|
||||
"database": "backup_e2e",
|
||||
"type": "postgresql",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 5432,
|
||||
"host": "postgresql-13",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef006"
|
||||
},
|
||||
{
|
||||
"name": "PostgreSQL 12",
|
||||
"database": "backup_e2e",
|
||||
"type": "postgresql",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 5432,
|
||||
"host": "postgresql-12",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef007"
|
||||
},
|
||||
{
|
||||
"name": "MySQL 8.4",
|
||||
"database": "backup_e2e",
|
||||
"type": "mysql",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 3306,
|
||||
"host": "mysql-8.4",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef008"
|
||||
},
|
||||
{
|
||||
"name": "MySQL 8.0",
|
||||
"database": "backup_e2e",
|
||||
"type": "mysql",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 3306,
|
||||
"host": "mysql-8.0",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef009"
|
||||
},
|
||||
{
|
||||
"name": "MariaDB 11.4",
|
||||
"database": "backup_e2e",
|
||||
"type": "mariadb",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 3306,
|
||||
"host": "mariadb-11.4",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef010"
|
||||
},
|
||||
{
|
||||
"name": "MariaDB 11.3",
|
||||
"database": "backup_e2e",
|
||||
"type": "mariadb",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 3306,
|
||||
"host": "mariadb-11.3",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef011"
|
||||
},
|
||||
{
|
||||
"name": "MariaDB 11.2",
|
||||
"database": "backup_e2e",
|
||||
"type": "mariadb",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 3306,
|
||||
"host": "mariadb-11.2",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef012"
|
||||
},
|
||||
{
|
||||
"name": "MariaDB 11.1",
|
||||
"database": "backup_e2e",
|
||||
"type": "mariadb",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 3306,
|
||||
"host": "mariadb-11.1",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef013"
|
||||
},
|
||||
{
|
||||
"name": "MariaDB 11.0",
|
||||
"database": "backup_e2e",
|
||||
"type": "mariadb",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 3306,
|
||||
"host": "mariadb-11.0",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef014"
|
||||
},
|
||||
{
|
||||
"name": "MariaDB 10.11",
|
||||
"database": "backup_e2e",
|
||||
"type": "mariadb",
|
||||
"username": "backup",
|
||||
"password": "backup",
|
||||
"port": 3306,
|
||||
"host": "mariadb-10.11",
|
||||
"generated_id": "9f8a5b18-0f45-4b4c-a821-93d7e46ef015"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
[[databases]]
|
||||
name = "MongoDB 8.0 Auth"
|
||||
database = "backup_e2e"
|
||||
type = "mongodb"
|
||||
username = "backup"
|
||||
password = "backup"
|
||||
port = 27017
|
||||
host = "mongodb-8.0-auth"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef101"
|
||||
|
||||
[[databases]]
|
||||
name = "MongoDB 8.0 No Auth"
|
||||
database = "backup_e2e"
|
||||
type = "mongodb"
|
||||
username = ""
|
||||
password = ""
|
||||
port = 27017
|
||||
host = "mongodb-8.0-no-auth"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef102"
|
||||
|
||||
[[databases]]
|
||||
name = "MongoDB 7.0 Auth"
|
||||
database = "backup_e2e"
|
||||
type = "mongodb"
|
||||
username = "backup"
|
||||
password = "backup"
|
||||
port = 27017
|
||||
host = "mongodb-7.0-auth"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef103"
|
||||
|
||||
[[databases]]
|
||||
name = "MongoDB 7.0 No Auth"
|
||||
database = "backup_e2e"
|
||||
type = "mongodb"
|
||||
username = ""
|
||||
password = ""
|
||||
port = 27017
|
||||
host = "mongodb-7.0-no-auth"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef104"
|
||||
|
||||
[[databases]]
|
||||
name = "MongoDB 6.0 Auth"
|
||||
database = "backup_e2e"
|
||||
type = "mongodb"
|
||||
username = "backup"
|
||||
password = "backup"
|
||||
port = 27017
|
||||
host = "mongodb-6.0-auth"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef105"
|
||||
|
||||
[[databases]]
|
||||
name = "MongoDB 6.0 No Auth"
|
||||
database = "backup_e2e"
|
||||
type = "mongodb"
|
||||
username = ""
|
||||
password = ""
|
||||
port = 27017
|
||||
host = "mongodb-6.0-no-auth"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef106"
|
||||
|
||||
[[databases]]
|
||||
name = "Redis 8"
|
||||
database = "0"
|
||||
type = "redis"
|
||||
username = ""
|
||||
password = ""
|
||||
port = 6379
|
||||
host = "redis-8"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef107"
|
||||
|
||||
[[databases]]
|
||||
name = "Redis 7"
|
||||
database = "0"
|
||||
type = "redis"
|
||||
username = ""
|
||||
password = ""
|
||||
port = 6379
|
||||
host = "redis-7"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef108"
|
||||
|
||||
[[databases]]
|
||||
name = "Valkey 8"
|
||||
database = "0"
|
||||
type = "valkey"
|
||||
username = ""
|
||||
password = ""
|
||||
port = 6379
|
||||
host = "valkey-8"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef109"
|
||||
|
||||
[[databases]]
|
||||
name = "Valkey 7"
|
||||
database = "0"
|
||||
type = "valkey"
|
||||
username = ""
|
||||
password = ""
|
||||
port = 6379
|
||||
host = "valkey-7"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef110"
|
||||
|
||||
[[databases]]
|
||||
name = "MSSQL 2022"
|
||||
database = "master"
|
||||
type = "mssql"
|
||||
username = "sa"
|
||||
password = "BackupRootPassw0rd!"
|
||||
port = 1433
|
||||
host = "mssql-2022"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef111"
|
||||
|
||||
[[databases]]
|
||||
name = "MSSQL 2019"
|
||||
database = "master"
|
||||
type = "mssql"
|
||||
username = "sa"
|
||||
password = "BackupRootPassw0rd!"
|
||||
port = 1433
|
||||
host = "mssql-2019"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef112"
|
||||
|
||||
[[databases]]
|
||||
name = "SQLite 3.46"
|
||||
database = "/data/backup_e2e.db"
|
||||
type = "sqlite"
|
||||
username = ""
|
||||
password = ""
|
||||
port = 0
|
||||
host = "sqlite-3.46"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef113"
|
||||
|
||||
[[databases]]
|
||||
name = "SQLite 3.45"
|
||||
database = "/data/backup_e2e.db"
|
||||
type = "sqlite"
|
||||
username = ""
|
||||
password = ""
|
||||
port = 0
|
||||
host = "sqlite-3.45"
|
||||
generated_id = "9f8a5b18-0f45-4b4c-a821-93d7e46ef114"
|
||||
@@ -0,0 +1,24 @@
|
||||
name: portabase-agent-a
|
||||
|
||||
services:
|
||||
|
||||
app:
|
||||
container_name: portabase-agent
|
||||
image: portabase/agent:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./databases.json:/config/config.json
|
||||
environment:
|
||||
LOG: info
|
||||
TZ: "UTC"
|
||||
POLLING: 5
|
||||
EDGE_KEY: "${EDGE_KEY}"
|
||||
networks:
|
||||
- portabase
|
||||
extra_hosts:
|
||||
- "localhost:host-gateway"
|
||||
|
||||
networks:
|
||||
portabase:
|
||||
name: portabase
|
||||
external: true
|
||||
@@ -0,0 +1,23 @@
|
||||
name: portabase-agent-b
|
||||
|
||||
services:
|
||||
|
||||
app:
|
||||
container_name: portabase-agent
|
||||
image: portabase/agent:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./databases.json:/config/config.json
|
||||
environment:
|
||||
LOG: info
|
||||
TZ: "UTC"
|
||||
POLLING: 5
|
||||
DATA_PATH: "/data"
|
||||
EDGE_KEY: "${EDGE_KEY}"
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
networks:
|
||||
portabase:
|
||||
name: portabase
|
||||
external: true
|
||||
@@ -0,0 +1,390 @@
|
||||
name: database
|
||||
|
||||
x-postgresql-common: &postgresql-common
|
||||
environment:
|
||||
POSTGRES_DB: backup_e2e
|
||||
POSTGRES_USER: backup
|
||||
POSTGRES_PASSWORD: backup
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U backup -d backup_e2e"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
x-mysql-common: &mysql-common
|
||||
environment:
|
||||
MYSQL_DATABASE: backup_e2e
|
||||
MYSQL_USER: backup
|
||||
MYSQL_PASSWORD: backup
|
||||
MYSQL_ROOT_PASSWORD: backup-root
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 20s
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
x-mariadb-common: &mariadb-common
|
||||
environment:
|
||||
MARIADB_DATABASE: backup_e2e
|
||||
MARIADB_USER: backup
|
||||
MARIADB_PASSWORD: backup
|
||||
MARIADB_ROOT_PASSWORD: backup-root
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -uroot -p$$MARIADB_ROOT_PASSWORD"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 20s
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
x-mongodb-auth-common: &mongodb-auth-common
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: backup
|
||||
MONGO_INITDB_ROOT_PASSWORD: backup
|
||||
MONGO_INITDB_DATABASE: backup_e2e
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"mongosh --quiet --username $$MONGO_INITDB_ROOT_USERNAME --password $$MONGO_INITDB_ROOT_PASSWORD --authenticationDatabase admin --eval \"db.runCommand({ ping: 1 }).ok\" | grep 1",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 20s
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
x-mongodb-no-auth-common: &mongodb-no-auth-common
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"mongosh --quiet --eval \"db.runCommand({ ping: 1 }).ok\" | grep 1",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 20s
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
x-redis-common: &redis-common
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 1
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
x-valkey-common: &valkey-common
|
||||
command: ["valkey-server", "--appendonly", "yes"]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "valkey-cli ping | grep PONG"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
x-mssql-common: &mssql-common
|
||||
platform: linux/amd64
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
MSSQL_SA_PASSWORD: "BackupRootPassw0rd!"
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"(test -x /opt/mssql-tools18/bin/sqlcmd && /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P $$MSSQL_SA_PASSWORD -Q \"SELECT 1\") || (test -x /opt/mssql-tools/bin/sqlcmd && /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P $$MSSQL_SA_PASSWORD -Q \"SELECT 1\")",
|
||||
]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 20
|
||||
start_period: 40s
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
x-sqlite-common: &sqlite-common
|
||||
command: ["/bin/sh", "-lc", "mkdir -p /data && sqlite3 /data/backup_e2e.db 'SELECT 1;' >/dev/null && tail -f /dev/null"]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "sqlite3 /data/backup_e2e.db 'SELECT 1;' | grep 1"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 1
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
services:
|
||||
|
||||
postgresql-18:
|
||||
<<: *postgresql-common
|
||||
image: postgres:18-alpine
|
||||
expose:
|
||||
- "5432"
|
||||
volumes:
|
||||
- postgresql-18-backup-e2e-data:/var/lib/postgresql/data
|
||||
|
||||
postgresql-17:
|
||||
<<: *postgresql-common
|
||||
image: postgres:17-alpine
|
||||
expose:
|
||||
- "5432"
|
||||
volumes:
|
||||
- postgresql-17-backup-e2e-data:/var/lib/postgresql/data
|
||||
|
||||
postgresql-16:
|
||||
<<: *postgresql-common
|
||||
image: postgres:16-alpine
|
||||
expose:
|
||||
- "5432"
|
||||
volumes:
|
||||
- postgresql-16-backup-e2e-data:/var/lib/postgresql/data
|
||||
|
||||
postgresql-15:
|
||||
<<: *postgresql-common
|
||||
image: postgres:15-alpine
|
||||
expose:
|
||||
- "5432"
|
||||
volumes:
|
||||
- postgresql-15-backup-e2e-data:/var/lib/postgresql/data
|
||||
|
||||
postgresql-14:
|
||||
<<: *postgresql-common
|
||||
image: postgres:14-alpine
|
||||
expose:
|
||||
- "5432"
|
||||
volumes:
|
||||
- postgresql-14-backup-e2e-data:/var/lib/postgresql/data
|
||||
|
||||
postgresql-13:
|
||||
<<: *postgresql-common
|
||||
image: postgres:13-alpine
|
||||
expose:
|
||||
- "5432"
|
||||
volumes:
|
||||
- postgresql-13-backup-e2e-data:/var/lib/postgresql/data
|
||||
|
||||
postgresql-12:
|
||||
<<: *postgresql-common
|
||||
image: postgres:12-alpine
|
||||
expose:
|
||||
- "5432"
|
||||
volumes:
|
||||
- postgresql-12-backup-e2e-data:/var/lib/postgresql/data
|
||||
|
||||
mysql-8.4:
|
||||
<<: *mysql-common
|
||||
image: mysql:8.4
|
||||
expose:
|
||||
- "3306"
|
||||
volumes:
|
||||
- mysql-8.4-backup-e2e-data:/var/lib/mysql
|
||||
|
||||
mysql-8.0:
|
||||
<<: *mysql-common
|
||||
image: mysql:8.0
|
||||
expose:
|
||||
- "3306"
|
||||
volumes:
|
||||
- mysql-8.0-backup-e2e-data:/var/lib/mysql
|
||||
|
||||
mariadb-11.4:
|
||||
<<: *mariadb-common
|
||||
image: mariadb:11.4
|
||||
expose:
|
||||
- "3306"
|
||||
volumes:
|
||||
- mariadb-11.4-backup-e2e-data:/var/lib/mysql
|
||||
|
||||
mariadb-11.3:
|
||||
<<: *mariadb-common
|
||||
image: mariadb:11.3
|
||||
expose:
|
||||
- "3306"
|
||||
volumes:
|
||||
- mariadb-11.3-backup-e2e-data:/var/lib/mysql
|
||||
|
||||
mariadb-11.2:
|
||||
<<: *mariadb-common
|
||||
image: mariadb:11.2
|
||||
expose:
|
||||
- "3306"
|
||||
volumes:
|
||||
- mariadb-11.2-backup-e2e-data:/var/lib/mysql
|
||||
|
||||
mariadb-11.1:
|
||||
<<: *mariadb-common
|
||||
image: mariadb:11.1
|
||||
expose:
|
||||
- "3306"
|
||||
volumes:
|
||||
- mariadb-11.1-backup-e2e-data:/var/lib/mysql
|
||||
|
||||
mariadb-11.0:
|
||||
<<: *mariadb-common
|
||||
image: mariadb:11.0
|
||||
expose:
|
||||
- "3306"
|
||||
volumes:
|
||||
- mariadb-11.0-backup-e2e-data:/var/lib/mysql
|
||||
|
||||
mariadb-10.11:
|
||||
<<: *mariadb-common
|
||||
image: mariadb:10.11
|
||||
expose:
|
||||
- "3306"
|
||||
volumes:
|
||||
- mariadb-10.11-backup-e2e-data:/var/lib/mysql
|
||||
|
||||
mongodb-8.0-auth:
|
||||
<<: *mongodb-auth-common
|
||||
image: mongo:8.0
|
||||
expose:
|
||||
- "27017"
|
||||
volumes:
|
||||
- mongodb-8.0-auth-backup-e2e-data:/data/db
|
||||
|
||||
mongodb-8.0-no-auth:
|
||||
<<: *mongodb-no-auth-common
|
||||
image: mongo:8.0
|
||||
expose:
|
||||
- "27017"
|
||||
volumes:
|
||||
- mongodb-8.0-no-auth-backup-e2e-data:/data/db
|
||||
|
||||
mongodb-7.0-auth:
|
||||
<<: *mongodb-auth-common
|
||||
image: mongo:7.0
|
||||
expose:
|
||||
- "27017"
|
||||
volumes:
|
||||
- mongodb-7.0-auth-backup-e2e-data:/data/db
|
||||
|
||||
mongodb-7.0-no-auth:
|
||||
<<: *mongodb-no-auth-common
|
||||
image: mongo:7.0
|
||||
expose:
|
||||
- "27017"
|
||||
volumes:
|
||||
- mongodb-7.0-no-auth-backup-e2e-data:/data/db
|
||||
|
||||
mongodb-6.0-auth:
|
||||
<<: *mongodb-auth-common
|
||||
image: mongo:6.0
|
||||
expose:
|
||||
- "27017"
|
||||
volumes:
|
||||
- mongodb-6.0-auth-backup-e2e-data:/data/db
|
||||
|
||||
mongodb-6.0-no-auth:
|
||||
<<: *mongodb-no-auth-common
|
||||
image: mongo:6.0
|
||||
expose:
|
||||
- "27017"
|
||||
volumes:
|
||||
- mongodb-6.0-no-auth-backup-e2e-data:/data/db
|
||||
|
||||
redis-8:
|
||||
<<: *redis-common
|
||||
image: redis:8
|
||||
expose:
|
||||
- "6379"
|
||||
volumes:
|
||||
- redis-8-backup-e2e-data:/data
|
||||
|
||||
redis-7:
|
||||
<<: *redis-common
|
||||
image: redis:7
|
||||
expose:
|
||||
- "6379"
|
||||
volumes:
|
||||
- redis-7-backup-e2e-data:/data
|
||||
|
||||
valkey-8:
|
||||
<<: *valkey-common
|
||||
image: valkey/valkey:8
|
||||
expose:
|
||||
- "6379"
|
||||
volumes:
|
||||
- valkey-8-backup-e2e-data:/data
|
||||
|
||||
valkey-7:
|
||||
<<: *valkey-common
|
||||
image: valkey/valkey:7
|
||||
expose:
|
||||
- "6379"
|
||||
volumes:
|
||||
- valkey-7-backup-e2e-data:/data
|
||||
|
||||
mssql-2022:
|
||||
<<: *mssql-common
|
||||
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||
expose:
|
||||
- "1433"
|
||||
volumes:
|
||||
- mssql-2022-backup-e2e-data:/var/opt/mssql
|
||||
|
||||
mssql-2019:
|
||||
<<: *mssql-common
|
||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||
expose:
|
||||
- "1433"
|
||||
volumes:
|
||||
- mssql-2019-backup-e2e-data:/var/opt/mssql
|
||||
|
||||
sqlite-3.46:
|
||||
<<: *sqlite-common
|
||||
image: keinos/sqlite3:3.46.1
|
||||
volumes:
|
||||
- sqlite-3.46-backup-e2e-data:/data
|
||||
|
||||
sqlite-3.45:
|
||||
<<: *sqlite-common
|
||||
image: keinos/sqlite3:3.45.3
|
||||
volumes:
|
||||
- sqlite-3.45-backup-e2e-data:/data
|
||||
|
||||
volumes:
|
||||
postgresql-18-backup-e2e-data:
|
||||
postgresql-17-backup-e2e-data:
|
||||
postgresql-16-backup-e2e-data:
|
||||
postgresql-15-backup-e2e-data:
|
||||
postgresql-14-backup-e2e-data:
|
||||
postgresql-13-backup-e2e-data:
|
||||
postgresql-12-backup-e2e-data:
|
||||
mysql-8.4-backup-e2e-data:
|
||||
mysql-8.0-backup-e2e-data:
|
||||
mariadb-11.4-backup-e2e-data:
|
||||
mariadb-11.3-backup-e2e-data:
|
||||
mariadb-11.2-backup-e2e-data:
|
||||
mariadb-11.1-backup-e2e-data:
|
||||
mariadb-11.0-backup-e2e-data:
|
||||
mariadb-10.11-backup-e2e-data:
|
||||
mongodb-8.0-auth-backup-e2e-data:
|
||||
mongodb-8.0-no-auth-backup-e2e-data:
|
||||
mongodb-7.0-auth-backup-e2e-data:
|
||||
mongodb-7.0-no-auth-backup-e2e-data:
|
||||
mongodb-6.0-auth-backup-e2e-data:
|
||||
mongodb-6.0-no-auth-backup-e2e-data:
|
||||
redis-8-backup-e2e-data:
|
||||
redis-7-backup-e2e-data:
|
||||
valkey-8-backup-e2e-data:
|
||||
valkey-7-backup-e2e-data:
|
||||
mssql-2022-backup-e2e-data:
|
||||
mssql-2019-backup-e2e-data:
|
||||
sqlite-3.46-backup-e2e-data:
|
||||
sqlite-3.45-backup-e2e-data:
|
||||
|
||||
networks:
|
||||
portabase:
|
||||
name: portabase
|
||||
external: true
|
||||
@@ -0,0 +1,165 @@
|
||||
import {expect, Page} from "@playwright/test";
|
||||
import {execSync} from "node:child_process";
|
||||
|
||||
|
||||
export const oidcProviders: Array<{ id: string, title: string }> = [
|
||||
{id: "authentik", title: "Authentik"},
|
||||
{id: "pocket-id", title: "Pocket ID"},
|
||||
{id: "keycloak", title: "Keycloak"},
|
||||
]
|
||||
|
||||
|
||||
export async function loginWithOidc(page: Page, providerId) {
|
||||
await page.goto("/login");
|
||||
|
||||
const provider = oidcProviders.find((entry) => entry.id === providerId);
|
||||
if (!provider) throw new Error(`Unknown OIDC provider: ${providerId}`)
|
||||
|
||||
await expect(page.getByRole("button", {name: provider.title})).toBeVisible();
|
||||
|
||||
const authorizationUrl = await getOidcAuthorizationUrl(page, providerId);
|
||||
await page.goto(authorizationUrl);
|
||||
|
||||
switch (providerId) {
|
||||
case "pocket-id":
|
||||
await loginWithPocketId(page)
|
||||
return
|
||||
case "authentik":
|
||||
await loginWithAuthentik(page)
|
||||
return
|
||||
case "keycloak":
|
||||
await loginWithKeycloak(page)
|
||||
return
|
||||
}
|
||||
|
||||
// if (providerId === "pocket-id") {
|
||||
// await loginWithPocketId(page);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// const authorizationUrl = await getOidcAuthorizationUrl(page, providerId);
|
||||
// await page.goto(authorizationUrl);
|
||||
//
|
||||
// if (providerId === "authentik") {
|
||||
// await loginWithAuthentik(page);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// await loginWithKeycloak(page);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const AUTHENTIK_USER = "admin@example.com";
|
||||
const AUTHENTIK_PASSWORD = "testPASS123456!";
|
||||
|
||||
const KEYCLOAK_USER = "admin";
|
||||
const KEYCLOAK_PASSWORD = "admin";
|
||||
|
||||
const oidcHostRewrites: Record<string, string> = {
|
||||
"http://authentik-server:9000": "http://localhost:3057",
|
||||
"http://keycloak:8080": "http://localhost:3056",
|
||||
"http://pocket-id:1411": "http://localhost:3055",
|
||||
};
|
||||
|
||||
function toBrowserReachableUrl(url: string) {
|
||||
let browserUrl = url;
|
||||
|
||||
for (const [internalOrigin, browserOrigin] of Object.entries(oidcHostRewrites)) {
|
||||
browserUrl = browserUrl.replace(internalOrigin, browserOrigin);
|
||||
}
|
||||
|
||||
return browserUrl;
|
||||
}
|
||||
|
||||
async function getOidcAuthorizationUrl(page: Page, providerId) {
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
const result = await page.evaluate(async ({providerId}) => {
|
||||
const response = await fetch("/api/auth/sign-in/sso", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
providerId,
|
||||
providerType: "oidc",
|
||||
callbackURL: "/dashboard",
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json().catch(() => ({}));
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
data,
|
||||
};
|
||||
}, {providerId});
|
||||
|
||||
if (result.ok && result.data?.url) {
|
||||
return toBrowserReachableUrl(result.data.url);
|
||||
}
|
||||
|
||||
if (result.status !== 429 || attempt === 2) {
|
||||
throw new Error(`Failed to initiate ${providerId} OIDC sign-in: ${JSON.stringify(result)}`);
|
||||
}
|
||||
|
||||
await page.waitForTimeout(2_000);
|
||||
}
|
||||
throw new Error(`Failed to initiate ${providerId} OIDC sign-in after retries.`);
|
||||
}
|
||||
|
||||
async function loginWithAuthentik(page: Page) {
|
||||
const usernameField = page.locator('input[name="uidField"], input[type="text"], input[type="email"]').first();
|
||||
await expect(usernameField).toBeVisible();
|
||||
await usernameField.fill(AUTHENTIK_USER);
|
||||
await usernameField.press("Enter").catch(() => undefined);
|
||||
if (await page.getByLabel(/password/i).count() === 0) {
|
||||
await page.getByRole("button", {name: /log in|sign in|continue/i}).click();
|
||||
}
|
||||
|
||||
const passwordField = page.locator('input[name="password"], input[type="password"]').first();
|
||||
await expect(passwordField).toBeVisible();
|
||||
await passwordField.fill(AUTHENTIK_PASSWORD);
|
||||
await page.getByRole("button", {name: /log in|sign in|continue/i}).click();
|
||||
}
|
||||
|
||||
async function loginWithKeycloak(page: Page) {
|
||||
await page.locator('input[name="username"], #username').fill(KEYCLOAK_USER);
|
||||
await page.locator('input[name="password"], #password').fill(KEYCLOAK_PASSWORD);
|
||||
await page.locator('input[type="submit"], #kc-login').click();
|
||||
}
|
||||
|
||||
function getPocketIdAccessUrl() {
|
||||
const output = execSync(
|
||||
"docker compose -f docker-compose.oidc.yml exec -T pocket-id ./pocket-id one-time-access-token admin",
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
},
|
||||
);
|
||||
|
||||
const match = output.match(/https?:\/\/\S+/);
|
||||
if (!match) {
|
||||
throw new Error(`Failed to extract Pocket ID access URL from output:\n${output}`);
|
||||
}
|
||||
|
||||
return match[0];
|
||||
}
|
||||
|
||||
async function loginWithPocketId(page: Page) {
|
||||
// const accessUrl = getPocketIdAccessUrl();
|
||||
// const authorizationUrl = await getOidcAuthorizationUrl(page, "pocket-id");
|
||||
|
||||
// await page.goto(authorizationUrl);
|
||||
// await page.goto(accessUrl);
|
||||
// await page.goto(authorizationUrl);
|
||||
await page.waitForURL(/\/dashboard\/home(?:\?.*)?$/);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import {expect, test} from "@playwright/test";
|
||||
import {logout} from "./helpers/auth";
|
||||
import {loginWithOidc, oidcProviders} from "./helpers/oidc";
|
||||
|
||||
|
||||
test.describe.serial("OIDC auth", () => {
|
||||
for (const provider of oidcProviders) {
|
||||
test(`OIDC login/logout with ${provider.title}`, async ({page}) => {
|
||||
|
||||
await loginWithOidc(page, provider.id);
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard\/home(?:\?.*)?$/);
|
||||
await expect(page.getByRole("link", {name: "Logo Portabase"})).toBeVisible();
|
||||
|
||||
await logout(page);
|
||||
|
||||
await expect(page).toHaveURL(/\/login(?:\?.*)?$/);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -6,7 +6,8 @@ import path from 'path';
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
dotenv.config({ path: path.resolve(__dirname, '.env.e2e') });
|
||||
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
@@ -43,10 +44,15 @@ export default defineConfig({
|
||||
testMatch: '**/auth.spec.ts',
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
{
|
||||
name: 'oidc',
|
||||
testMatch: '**/oidc.spec.ts',
|
||||
dependencies: ['auth'],
|
||||
},
|
||||
{
|
||||
name: 'access-management',
|
||||
testMatch: '**/access-management.spec.ts',
|
||||
dependencies: ['auth'],
|
||||
dependencies: ['oidc'],
|
||||
},
|
||||
{
|
||||
name: 'notification',
|
||||
@@ -63,11 +69,10 @@ export default defineConfig({
|
||||
testMatch: '**/agent.spec.ts',
|
||||
dependencies: ['access-management'],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'project',
|
||||
testMatch: '**/project.spec.ts',
|
||||
dependencies: ['access-management'],
|
||||
dependencies: ['agent'],
|
||||
},
|
||||
{
|
||||
name: 'cleanup',
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Authentik seed contract
|
||||
|
||||
This directory defines the deterministic Authentik seed used by the local OIDC stack in `docker-compose.oidc.yml`.
|
||||
|
||||
## Bootstrap contract
|
||||
|
||||
The compose stack must provide these first-start bootstrap values on the `authentik-worker` container:
|
||||
|
||||
- `AUTHENTIK_BOOTSTRAP_PASSWORD`
|
||||
- `AUTHENTIK_BOOTSTRAP_TOKEN`
|
||||
- `AUTHENTIK_BOOTSTRAP_EMAIL`
|
||||
|
||||
Per Authentik's automated install flow, those values are only read on the first startup of a fresh Authentik database volume.
|
||||
|
||||
## Mounted assets
|
||||
|
||||
- `blueprints/portabase-oidc.yaml`
|
||||
- mounted into `/blueprints/portabase-oidc.yaml`
|
||||
- auto-discovered by the Authentik worker
|
||||
- kept as an in-repo reference blueprint for the intended provider/application shape
|
||||
- `bootstrap.sh`
|
||||
- executed by the dedicated `authentik-bootstrap` compose service
|
||||
- waits for the server and required default Authentik objects
|
||||
- grants the bootstrap admin deterministic `authentik Admins` membership from inside Authentik
|
||||
- resolves the required default flow, scope-mapping, and signing-key objects from inside Authentik with `ak shell`
|
||||
- then reconciles the deterministic Portabase seed objects through Authentik's ORM before proving the public discovery URL over HTTP
|
||||
|
||||
## Seeded objects
|
||||
|
||||
The bootstrap flow is expected to reconcile, idempotently:
|
||||
|
||||
- an OAuth2/OIDC provider named `portabase`
|
||||
- an application named `Portabase` with slug `portabase`
|
||||
- a confidential client id `portabase`
|
||||
- a client secret `portabase-e2e-secret`
|
||||
- a strict redirect URI `http://localhost:8887/api/auth/sso/callback/authentik`
|
||||
- an internal test user:
|
||||
- username: `admin@example.com`
|
||||
- email: `admin@example.com`
|
||||
- password: `testPASS123456!`
|
||||
- an `admin` group containing the test user
|
||||
|
||||
## Expected runtime outputs
|
||||
|
||||
After a fresh `docker compose -f docker-compose.oidc.yml up`, the seeded Authentik instance should expose:
|
||||
|
||||
- `http://localhost:3057/-/health/live/`
|
||||
- `http://localhost:3057/application/o/portabase/.well-known/openid-configuration`
|
||||
|
||||
That discovery document is the canonical proof that the bootstrap completed successfully. The runtime source of truth is `bootstrap.sh`, which first normalizes the bootstrap admin's permissions inside Authentik and then reconciles the provider, application, user, and group directly inside Authentik even if blueprint auto-application semantics or bootstrap-token permissions change between Authentik releases.
|
||||
|
||||
## Portabase env contract
|
||||
|
||||
This seed is aligned with the current E2E environment:
|
||||
|
||||
- `AUTH_OIDC_AUTHENTIK_ID=authentik`
|
||||
- `AUTH_OIDC_AUTHENTIK_CLIENT=portabase`
|
||||
- `AUTH_OIDC_AUTHENTIK_SECRET=portabase-e2e-secret`
|
||||
- `AUTH_OIDC_AUTHENTIK_DISCOVERY_ENDPOINT=http://authentik-server:9000/application/o/portabase/.well-known/openid-configuration`
|
||||
- `AUTH_OIDC_AUTHENTIK_HOST=authentik-server:9000`
|
||||
- `AUTH_OIDC_AUTHENTIK_SCOPES=openid profile email`
|
||||
- `AUTH_OIDC_AUTHENTIK_ALLOWED_GROUP=admin`
|
||||
@@ -0,0 +1,44 @@
|
||||
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
||||
version: 1
|
||||
metadata:
|
||||
name: portabase-oidc
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "false"
|
||||
blueprints.goauthentik.io/description: "Reference Portabase OIDC shape; the deterministic runtime seed is applied by seeds/authentik/bootstrap.sh."
|
||||
entries:
|
||||
- model: authentik_providers_oauth2.oauth2provider
|
||||
state: present
|
||||
identifiers:
|
||||
name: portabase
|
||||
attrs:
|
||||
name: portabase
|
||||
authentication_flow: !Find [authentik_flows.flow, [slug, default-authentication-flow]]
|
||||
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
||||
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
|
||||
property_mappings:
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]]
|
||||
- !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
|
||||
client_type: confidential
|
||||
client_id: portabase
|
||||
client_secret: portabase-e2e-secret
|
||||
access_code_validity: minutes=1
|
||||
access_token_validity: hours=1
|
||||
refresh_token_validity: days=7
|
||||
include_claims_in_id_token: true
|
||||
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
|
||||
redirect_uris:
|
||||
- matching_mode: strict
|
||||
url: http://localhost:8887/api/auth/sso/callback/authentik
|
||||
sub_mode: user_email
|
||||
issuer_mode: global
|
||||
|
||||
- model: authentik_core.application
|
||||
state: present
|
||||
identifiers:
|
||||
slug: portabase
|
||||
attrs:
|
||||
name: Portabase
|
||||
slug: portabase
|
||||
policy_engine_mode: any
|
||||
provider: !Find [authentik_providers_oauth2.oauth2provider, [name, portabase]]
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
python - <<'PY'
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
BASE_URL = os.environ.get("AUTHENTIK_BASE_URL", "http://authentik-server:9000")
|
||||
LIVE_URL = os.environ.get("AUTHENTIK_LIVE_URL", f"{BASE_URL}/-/health/live/")
|
||||
ATTEMPTS = int(os.environ.get("AUTHENTIK_BOOTSTRAP_ATTEMPTS", "90"))
|
||||
SLEEP_SECONDS = int(os.environ.get("AUTHENTIK_BOOTSTRAP_SLEEP_SECONDS", "2"))
|
||||
|
||||
|
||||
def wait_for_live() -> None:
|
||||
for attempt in range(1, ATTEMPTS + 1):
|
||||
try:
|
||||
with urllib.request.urlopen(LIVE_URL, timeout=10) as response:
|
||||
if response.status == 200:
|
||||
print("Authentik live health is reachable")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
print(f"Waiting for Authentik live health ({attempt}/{ATTEMPTS})")
|
||||
time.sleep(SLEEP_SECONDS)
|
||||
raise SystemExit(f"Timed out waiting for Authentik live health at {LIVE_URL}")
|
||||
|
||||
|
||||
wait_for_live()
|
||||
PY
|
||||
|
||||
ak shell -c "import os; from authentik.core.models import Group, User; user = User.objects.get(email=os.environ['AUTHENTIK_BOOTSTRAP_EMAIL']); group = Group.objects.get(name='authentik Admins'); group.users.add(user); print(f'Granted {user.username} authentik Admins membership')" >/dev/null
|
||||
|
||||
BOOTSTRAP_ATTEMPTS="${AUTHENTIK_BOOTSTRAP_ATTEMPTS:-90}"
|
||||
BOOTSTRAP_SLEEP_SECONDS="${AUTHENTIK_BOOTSTRAP_SLEEP_SECONDS:-2}"
|
||||
|
||||
reconcile_seed_objects() {
|
||||
attempt=1
|
||||
while [ "$attempt" -le "$BOOTSTRAP_ATTEMPTS" ]; do
|
||||
if ak shell -c "import os; from authentik.core.models import Application, Group, User; from authentik.crypto.models import CertificateKeyPair; from authentik.flows.models import Flow; from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping; bootstrap_admin = User.objects.get(email=os.environ['AUTHENTIK_BOOTSTRAP_EMAIL']); authentik_admins = Group.objects.get(name='authentik Admins'); authentik_admins.users.add(bootstrap_admin); user, user_created = User.objects.get_or_create(username=os.environ['AUTHENTIK_TEST_USER_USERNAME'], defaults={'email': os.environ['AUTHENTIK_TEST_USER_EMAIL'], 'name': os.environ['AUTHENTIK_TEST_USER_NAME']}); user.email = os.environ['AUTHENTIK_TEST_USER_EMAIL']; user.name = os.environ['AUTHENTIK_TEST_USER_NAME']; user.is_active = True; user.save(); user.set_password(os.environ['AUTHENTIK_TEST_USER_PASSWORD']); user.save(); group, group_created = Group.objects.get_or_create(name=os.environ['AUTHENTIK_TEST_GROUP']); group.is_superuser = False; group.save(); group.users.add(user); auth_flow = Flow.objects.get(slug='default-authentication-flow'); authorization_flow = Flow.objects.get(slug='default-provider-authorization-implicit-consent'); invalidation_flow = Flow.objects.get(slug='default-provider-invalidation-flow'); openid_scope = ScopeMapping.objects.get(scope_name='openid'); profile_scope = ScopeMapping.objects.get(scope_name='profile'); email_scope = ScopeMapping.objects.get(scope_name='email'); signing_key = CertificateKeyPair.objects.get(name='authentik Self-signed Certificate'); provider, provider_created = OAuth2Provider.objects.update_or_create(name=os.environ['AUTHENTIK_PROVIDER_NAME'], defaults={'authentication_flow': auth_flow, 'authorization_flow': authorization_flow, 'invalidation_flow': invalidation_flow, 'client_type': 'confidential', 'client_id': os.environ['AUTHENTIK_CLIENT_ID'], 'client_secret': os.environ['AUTHENTIK_CLIENT_SECRET'], 'access_code_validity': 'minutes=1', 'access_token_validity': 'hours=1', 'refresh_token_validity': 'days=7', 'include_claims_in_id_token': True, 'signing_key': signing_key, 'sub_mode': 'user_email', 'issuer_mode': 'global', '_redirect_uris': [{'matching_mode': 'strict', 'url': os.environ['AUTHENTIK_REDIRECT_URI']}],}); provider.property_mappings.set([openid_scope, profile_scope, email_scope]); application, application_created = Application.objects.update_or_create(slug=os.environ['AUTHENTIK_APPLICATION_SLUG'], defaults={'name': os.environ['AUTHENTIK_APPLICATION_NAME'], 'provider': provider.provider_ptr, 'policy_engine_mode': 'any'}); print(f\"Reconciled Authentik seed user={user.username} group={group.name} provider={provider.name} application={application.slug}\")" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
echo "Waiting for Authentik seed reconciliation (${attempt}/${BOOTSTRAP_ATTEMPTS})"
|
||||
attempt=$((attempt + 1))
|
||||
sleep "$BOOTSTRAP_SLEEP_SECONDS"
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
reconcile_seed_objects
|
||||
|
||||
python - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
BASE_URL = os.environ.get("AUTHENTIK_BASE_URL", "http://authentik-server:9000")
|
||||
DISCOVERY_PATH = os.environ.get("AUTHENTIK_DISCOVERY_PATH", "/application/o/portabase/.well-known/openid-configuration")
|
||||
DISCOVERY_URL = os.environ.get("AUTHENTIK_DISCOVERY_URL", f"{BASE_URL}{DISCOVERY_PATH}")
|
||||
ATTEMPTS = int(os.environ.get("AUTHENTIK_BOOTSTRAP_ATTEMPTS", "90"))
|
||||
SLEEP_SECONDS = int(os.environ.get("AUTHENTIK_BOOTSTRAP_SLEEP_SECONDS", "2"))
|
||||
|
||||
|
||||
def http_request(path: str):
|
||||
url = f"{BASE_URL}{path}"
|
||||
request = urllib.request.Request(url, method="GET")
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=15) as response:
|
||||
body = response.read().decode("utf-8")
|
||||
return response.status, body
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8")
|
||||
return exc.code, body
|
||||
|
||||
def wait_for_discovery() -> dict:
|
||||
for attempt in range(1, ATTEMPTS + 1):
|
||||
status, body = http_request(DISCOVERY_PATH)
|
||||
if status == 200:
|
||||
parsed = json.loads(body)
|
||||
if "issuer" in parsed and "authorization_endpoint" in parsed:
|
||||
print(f"Portabase discovery document is reachable at {DISCOVERY_URL}")
|
||||
return parsed
|
||||
print(f"Waiting for Portabase discovery document ({attempt}/{ATTEMPTS})")
|
||||
time.sleep(SLEEP_SECONDS)
|
||||
raise SystemExit(f"Timed out waiting for discovery document at {DISCOVERY_URL}")
|
||||
wait_for_discovery()
|
||||
|
||||
print(f"Authentik bootstrap verification completed at {DISCOVERY_URL}")
|
||||
PY
|
||||
Reference in New Issue
Block a user