mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
1
Commits
1.19.0
...
feat/oidc-e2e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
252aab9b90 |
@@ -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_LINKING=true
|
||||||
AUTH_ALLOW_UNLINKING=true
|
AUTH_ALLOW_UNLINKING=true
|
||||||
|
|
||||||
|
|
||||||
# Social OAuth2 Authentification
|
# Social OAuth2 Authentification
|
||||||
AUTH_SOCIAL_ID=""
|
AUTH_SOCIAL_ID=""
|
||||||
AUTH_SOCIAL_TITLE=""
|
AUTH_SOCIAL_TITLE=""
|
||||||
@@ -65,5 +64,4 @@ AUTH_PASSKEY_ENABLED=true
|
|||||||
# Retention
|
# Retention
|
||||||
RETENTION_CRON="* * * * *"
|
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
|
load: true
|
||||||
tags: portabase/portabase:test
|
tags: portabase/portabase:test
|
||||||
|
|
||||||
- name: Run app container
|
- name: Create shared Docker network
|
||||||
run: |
|
run: |
|
||||||
docker run -d --name myapp \
|
docker network inspect portabase >/dev/null 2>&1 || \
|
||||||
-p 8887:80 \
|
docker network create portabase
|
||||||
-e NODE_ENV=production \
|
|
||||||
-e PROJECT_URL=http://localhost:8887 \
|
- name: Seed OIDC services
|
||||||
-e PROJECT_SECRET=80af5e4c875dfded3a6ba511c6ed8b0160cad723f848ee0851403ed6141f6ba1 \
|
run: make seed-auth
|
||||||
portabase/portabase:test
|
|
||||||
|
- 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
|
- name: Wait for app port to be listening
|
||||||
run: |
|
run: |
|
||||||
for i in {1..40}; do
|
for i in {1..40}; do
|
||||||
|
|
||||||
if curl -fsS http://localhost:8887/ >/dev/null; then
|
if curl -fsS http://localhost:8887/ >/dev/null; then
|
||||||
echo "App is responding on port 8887"
|
echo "App is responding on port 8887"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -54,7 +99,7 @@ jobs:
|
|||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
echo "Timeout: app never became ready"
|
echo "Timeout: app never became ready"
|
||||||
docker logs myapp
|
docker compose -f docker-compose.prod.yml logs
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
- name: Install Playwright browsers
|
||||||
@@ -114,4 +159,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
if: always()
|
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:
|
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)
|
@[ $$(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.oidc.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 compose -f docker-compose.oidc.yml rm -f -s keycloak >/dev/null 2>&1 || true
|
||||||
@docker volume rm portabase-dev-func_keycloak-data >/dev/null 2>&1 || true
|
@docker volume rm portabase-oidc_keycloak-data >/dev/null 2>&1 || true
|
||||||
@docker compose -f docker-compose.func.yml up -d keycloak
|
@docker compose -f docker-compose.oidc.yml up -d keycloak
|
||||||
@echo "Keycloak seed import triggered from seeds/keycloak/*.json"
|
@echo "Keycloak seed import triggered from seeds/keycloak/*.json"
|
||||||
|
|
||||||
seed-pocket:
|
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)
|
@[ -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.oidc.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 compose -f docker-compose.oidc.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 volume rm portabase-oidc_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.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.func.yml up -d pocket-id
|
@docker compose -f docker-compose.oidc.yml up -d pocket-id
|
||||||
@sleep 2
|
@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"
|
@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:
|
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:
|
export-pocket:
|
||||||
@echo "Exporting Pocket ID data to seeds/pocket-id/portabase.zip..."
|
@echo "Exporting Pocket ID data to seeds/pocket-id/portabase.zip..."
|
||||||
@mkdir -p ./seeds/pocket-id
|
@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.oidc.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.oidc.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 rm /tmp/portabase-export.zip
|
||||||
@echo "Pocket ID data exported to seeds/pocket-id/portabase.zip"
|
@echo "Pocket ID data exported to seeds/pocket-id/portabase.zip"
|
||||||
|
|
||||||
export-keycloak:
|
export-keycloak:
|
||||||
@echo "Exporting Keycloak configuration and users to seeds/keycloak/*.json..."
|
@echo "Exporting Keycloak configuration and users to seeds/keycloak/*.json..."
|
||||||
@mkdir -p ./seeds/keycloak
|
@mkdir -p ./seeds/keycloak
|
||||||
@rm -f ./seeds/keycloak/*.json
|
@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 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 cp kc-exporter:/tmp/kc-export/. ./seeds/keycloak/
|
||||||
@docker rm -f kc-exporter >/dev/null 2>&1
|
@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"
|
@echo "Keycloak configuration and users exported to seeds/keycloak/*.json"
|
||||||
|
|
||||||
end-to-end:
|
e2e-auto:
|
||||||
@echo "Starting E2E testing..."
|
@echo "Starting E2E testing..."
|
||||||
@docker compose -f docker-compose.e2e.yml up -d
|
@docker network inspect portabase >/dev/null 2>&1 || docker network create portabase
|
||||||
@CI=true PROJECT_URL=http://localhost:8887 pnpm playwright test --project=chromium || (docker compose -f docker-compose.e2e.yml down --volumes && exit 1)
|
@$(MAKE) seed-auth
|
||||||
@docker compose -f docker-compose.e2e.yml down --volumes
|
@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."
|
@echo "Finished E2E testing successfully."
|
||||||
|
|
||||||
e2e-manual:
|
e2e-manual:
|
||||||
@echo "Starting E2E testing..."
|
@echo "Starting E2E testing..."
|
||||||
@docker compose -f docker-compose.e2e.yml up -d
|
@docker network inspect portabase >/dev/null 2>&1 || docker network create portabase
|
||||||
@pnpm playwright test --ui || (docker compose -f docker-compose.e2e.yml down --volumes && exit 1)
|
@docker compose -f e2e/helpers/agent/docker-compose.database.yml up -d
|
||||||
@docker compose -f docker-compose.e2e.yml down --volumes
|
@$(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."
|
@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:
|
services:
|
||||||
|
|
||||||
app:
|
app:
|
||||||
# build:
|
build:
|
||||||
# context: .
|
context: .
|
||||||
# dockerfile: docker/dockerfile/Dockerfile
|
dockerfile: docker/dockerfile/Dockerfile
|
||||||
# target: prod
|
target: prod
|
||||||
image: portabase/portabase:1
|
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- '8887:80'
|
||||||
environment:
|
environment:
|
||||||
TZ: "Europe/Paris"
|
TZ: "Europe/Paris"
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
volumes:
|
volumes:
|
||||||
- portabase-data:/data
|
- portabase-prod-data:/data
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
container_name: portabase-app-prod
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost/api/health"]
|
test: [ "CMD-SHELL", "curl -f http://localhost/api/health" ]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 60s
|
start_period: 60s
|
||||||
|
extra_hosts:
|
||||||
|
- "localhost:host-gateway"
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:17-alpine
|
||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- "5433:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres-data:/var/lib/postgresql/data
|
- portabase-prod-postgres-data:/var/lib/postgresql/data
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=devdb
|
- POSTGRES_DB=devdb
|
||||||
- POSTGRES_USER=devuser
|
- POSTGRES_USER=devuser
|
||||||
@@ -41,5 +42,5 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
portabase-data:
|
portabase-prod-data:
|
||||||
postgres-data:
|
portabase-prod-postgres-data:
|
||||||
+2
-2
@@ -6,7 +6,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- "5433:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres-data:/var/lib/postgresql/data
|
- portabase-dev-postgres-data:/var/lib/postgresql/data
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=devdb
|
- POSTGRES_DB=devdb
|
||||||
- POSTGRES_USER=devuser
|
- POSTGRES_USER=devuser
|
||||||
@@ -42,4 +42,4 @@ services:
|
|||||||
- "1025:1025"
|
- "1025:1025"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
portabase-dev-postgres-data:
|
||||||
|
|||||||
+14
-51
@@ -1,10 +1,5 @@
|
|||||||
import {expect, test} from "@playwright/test";
|
import {expect, test} from "@playwright/test";
|
||||||
import {execSync} from "child_process";
|
import {create, edit, get, launch, remove} from "./helpers/agent";
|
||||||
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 {LOCAL_STORAGE_PATH} from "./helpers/session";
|
import {LOCAL_STORAGE_PATH} from "./helpers/session";
|
||||||
|
|
||||||
const agent = {
|
const agent = {
|
||||||
@@ -64,53 +59,21 @@ test.describe.serial(() => {
|
|||||||
await expect(page.getByText(agent.bName)).toHaveCount(0);
|
await expect(page.getByText(agent.bName)).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// test("Launch the updated agent", async ({page}) => {
|
test("Launch the updated agent", async ({page}) => {
|
||||||
// await page.goto("/dashboard/agents");
|
await page.goto("/dashboard/agents");
|
||||||
// await expect(page.getByRole("heading", {name: "Agents"})).toBeVisible();
|
await expect(page.getByRole("heading", {name: "Agents"})).toBeVisible();
|
||||||
// await get(page, agent.aName).click();
|
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.afterAll(async () => {
|
await expect(page).toHaveURL(/\/dashboard\/agents\/.+/);
|
||||||
if (agentWorkspace) {
|
await expect(page.getByText(agent.aUpdatedName, {exact: true})).toBeVisible();
|
||||||
try {
|
await expect(page.getByText("Registration & Setup")).toBeVisible();
|
||||||
execSync(`portabase stop "${agent.aName}"`, {
|
|
||||||
cwd: agentWorkspace,
|
|
||||||
stdio: "pipe",
|
|
||||||
timeout: 30_000,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const commandInput = page.locator("input[readonly]").last();
|
||||||
execSync(`portabase uninstall --force "${agent.aName}"`, {
|
await page.locator("input[readonly]").first().locator("xpath=following-sibling::button[1]").click();
|
||||||
cwd: agentWorkspace,
|
const edgeKey = await commandInput.inputValue();
|
||||||
stdio: "pipe",
|
|
||||||
timeout: 30_000,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.rmSync(agentWorkspace, {recursive: true, force: true});
|
await launch(edgeKey)
|
||||||
agentWorkspace = null;
|
|
||||||
}
|
await expect(page.getByRole("heading", { name: "Managed Databases" })).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import {Page} from "@playwright/test";
|
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 Agent/i}).click();
|
||||||
await page.getByRole("button", {name: "Delete", exact: true}).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.
|
* Read environment variables from file.
|
||||||
* https://github.com/motdotla/dotenv
|
* 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.
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
@@ -43,10 +44,15 @@ export default defineConfig({
|
|||||||
testMatch: '**/auth.spec.ts',
|
testMatch: '**/auth.spec.ts',
|
||||||
dependencies: ['setup'],
|
dependencies: ['setup'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'oidc',
|
||||||
|
testMatch: '**/oidc.spec.ts',
|
||||||
|
dependencies: ['auth'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'access-management',
|
name: 'access-management',
|
||||||
testMatch: '**/access-management.spec.ts',
|
testMatch: '**/access-management.spec.ts',
|
||||||
dependencies: ['auth'],
|
dependencies: ['oidc'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'notification',
|
name: 'notification',
|
||||||
@@ -63,11 +69,10 @@ export default defineConfig({
|
|||||||
testMatch: '**/agent.spec.ts',
|
testMatch: '**/agent.spec.ts',
|
||||||
dependencies: ['access-management'],
|
dependencies: ['access-management'],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: 'project',
|
name: 'project',
|
||||||
testMatch: '**/project.spec.ts',
|
testMatch: '**/project.spec.ts',
|
||||||
dependencies: ['access-management'],
|
dependencies: ['agent'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'cleanup',
|
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