diff --git a/.github/workflows/test-features.yml b/.github/workflows/test-features.yml index ab85e3a2..6e58a42e 100644 --- a/.github/workflows/test-features.yml +++ b/.github/workflows/test-features.yml @@ -249,7 +249,7 @@ jobs: registry-accounts: runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 10 steps: - uses: actions/checkout@v4 with: @@ -330,6 +330,10 @@ jobs: env: PYTHONDONTWRITEBYTECODE: '1' run: python3 -m unittest discover -s oci/tests -p test_catalog_web_links.py -v + - name: Run full OCI engine test suite + env: + PYTHONDONTWRITEBYTECODE: '1' + run: python3 -m unittest discover -s oci/tests -v pbs: runs-on: ubuntu-latest diff --git a/lang/sk.json b/lang/sk.json index 2ea57715..ba19689a 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -3174,6 +3174,7 @@ "Identical:": "Zhodné:", "Identify candidate disk (never use system disk):": "Nájsť vhodný disk (nikdy nepoužívajte systémový disk):", "Identify persistent disk paths": "Zistiť trvalé cesty k diskom", + "Identity provider for single sign-on and access control": "Poskytovateľ identity pre jednotné prihlasovanie a riadenie prístupu", "If 'proxmox-ve' removal warning:": "Ak sa zobrazí varovanie o odstránení 'proxmox-ve':", "If Code 43 error appears, KVM hiding is already configured.": "Ak sa objaví chyba Code 43, KVM skrytie už je nastavené.", "If GUI does not load:": "Ak sa GUI nenačíta:", @@ -3336,6 +3337,7 @@ "Initial Stirling-PDF account": "Počiatočný účet Stirling-PDF", "Initial Tandoor administrator": "Počiatočný správca Tandoor", "Initial Umami administrator": "Počiatočný správca Umami", + "Initial administrator account": "Úvodný účet správcu", "Initial administrator created:": "Pôvodný správca vytvorený:", "Initial administrator user": "Počiatočné používateľské meno správcu", "Initializing Borg repository if needed...": "Pripravujem Borg repozitár, ak je to potrebné...", @@ -4889,6 +4891,7 @@ "Open the dashboard from this host on port 8008 to create a new admin account.": "Otvorte dashboard z tohto servera na porte 8008 a vytvorte nový admin účet.", "Open the dashboard to create a new admin account:": "Otvorte dashboard a vytvorte nový admin účet:", "Open the page of an application in the menu": "Otvoriť stránku aplikácie v ponuke", + "Open the web address and create the password of the akadmin user in the first-run setup.": "Otvorte webovú adresu a v prvotnom nastavení vytvorte heslo používateľa akadmin.", "Open-source AI-powered coding assistant": "Programátorský asistent využívajúci AI s otvoreným zdrojovým kódom", "Open-source UI for building and debugging multi-agent and RAG applications": "Rozhranie s otvoreným zdrojovým kódom na tvorbu a ladenie aplikácií s viacerými agentmi a RAG", "Open-source in-memory data store.": "Úložisko údajov v pamäti s otvoreným zdrojovým kódom.", @@ -8181,6 +8184,7 @@ "Web access": "Adresa webového rozhrania", "Web address of the AdGuard Home that receives the settings (e.g. http://192.168.1.3)": "Webová adresa AdGuard Home, ktorá prijíma nastavenia (napr. http://192.168.1.3)", "Web address of the main AdGuard Home, whose settings are copied (e.g. http://192.168.1.2)": "Webová adresa hlavného AdGuard Home, ktorého nastavenia sú kopírované (napr. http://192.168.1.2)", + "Web interface": "Webové rozhranie", "Web interface for Borg Backup repositories and jobs.": "Webové rozhranie na správu úložísk a úloh Borg Backup.", "Web interface for managing 3D printers.": "Webové rozhranie na správu 3D tlačiarní.", "Web interface to manage devices running Tasmota firmware.": "Webové rozhranie pre správu zariadení bežiacich na Tasmota firmware.", diff --git a/oci/VALIDATION.md b/oci/VALIDATION.md index 9e682809..44a88c6c 100644 --- a/oci/VALIDATION.md +++ b/oci/VALIDATION.md @@ -2,7 +2,7 @@ -44 of 530 applications in the OCI catalog have been tested for real: 41 in the ProxMenux lab and 3 by the community. The OCI installer shows them as verified, with a ✓ in the lists. +44 of 531 applications in the OCI catalog have been tested for real: 41 in the ProxMenux lab and 3 by the community. The OCI installer shows them as verified, with a ✓ in the lists. Each test describes the scenario that was run and names the image it ran on. It does not cover every possible configuration of the application, and a newer image has not been tested until someone reports it. diff --git a/oci/catalog/apps/authentik.json b/oci/catalog/apps/authentik.json new file mode 100644 index 00000000..23d011ff --- /dev/null +++ b/oci/catalog/apps/authentik.json @@ -0,0 +1,640 @@ +{ + "schema_version": "0.5.0", + "kind": "proxmenux.oci-template", + "id": "image-authentik", + "status": "generated-unvalidated", + "catalog_ui": { + "title": { + "en_US": "Authentik" + }, + "tagline": { + "en_US": "Identity provider for single sign-on and access control" + }, + "description": { + "en_US": "Authentik is an open-source identity provider and access platform. This recipe follows the official three-service deployment with PostgreSQL, the server and a worker, each in its own Proxmox OCI LXC." + }, + "category": "security", + "category_label": "Authentication & Security", + "author": "Authentik", + "developer": "Authentik", + "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/authentik.webp", + "thumbnail": null, + "screenshots": [], + "architectures": [ + "amd64", + "arm64" + ], + "launch": { + "scheme": "http", + "port": 9000, + "path": "/" + }, + "website": "https://goauthentik.io/", + "documentation": "https://docs.goauthentik.io/install-config/install/docker-compose/", + "repository": "https://github.com/goauthentik/authentik", + "tips": [ + "The server and worker share /data and /templates through host directories; those directories are not included in Proxmox CT backups.", + "The worker Docker-socket mount is omitted. Use the embedded outpost or deploy and manage a separate outpost manually.", + "The worker gets an interface on the selected Proxmox bridge for outbound access; PostgreSQL stays on the private stack network." + ], + "mini_changelog": [], + "display_version": "2026.8.5", + "updated_at": "2026-10-21" + }, + "source": { + "provider": "goauthentik", + "repository": "https://github.com/goauthentik/authentik", + "default_branch": "main", + "revision": "4ec0d19b98da3c329781894cd550da95d3033954", + "readme_raw_url": "https://goauthentik.io/version/2026.8/lifecycle/container/compose.yml", + "image_repository_url": "https://github.com/goauthentik/authentik/pkgs/container/server", + "readme_pushed_at": "2026-10-21T12:57:59Z", + "compose_sha256": "a3e138b5a85844d15c4a7ba6339997e43c0479d9d198582c67dd19523aadc694", + "generated_at": "2026-10-30T18:09:01+00:00" + }, + "container_contract": { + "service_name": "server", + "container_name": "authentik-server", + "image": { + "reference": "ghcr.io/goauthentik/server:2026.8.5", + "registry": "ghcr.io", + "repository": "ghcr.io/goauthentik/server", + "tag": "2026.8.5", + "digest": null, + "pull_policy": "resolve-written-reference-to-architecture-digest-at-install" + }, + "environment": [ + { + "name": "AUTHENTIK_POSTGRESQL__HOST", + "example": "postgresql", + "required": true, + "sensitive": false, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_POSTGRESQL__NAME", + "example": "authentik", + "required": true, + "sensitive": false, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_POSTGRESQL__PASSWORD", + "example": "${GENERATED_GENERATED_PG_PASS}", + "required": true, + "sensitive": true, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_POSTGRESQL__USER", + "example": "authentik", + "required": true, + "sensitive": false, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_SECRET_KEY", + "example": "${GENERATED_GENERATED_AUTHENTIK_SECRET_KEY}", + "required": true, + "sensitive": true, + "source": "docker-compose" + } + ], + "volumes": [ + { + "id": "volume-0", + "container_path": "/data", + "compose_source_example": "./data", + "read_only": false, + "required": true, + "installation_choice": [ + "host-bind" + ], + "default": "host-bind", + "managed_volume": { + "backup": false, + "default_size_gb": 8 + } + }, + { + "id": "volume-1", + "container_path": "/templates", + "compose_source_example": "./custom-templates", + "read_only": false, + "required": true, + "installation_choice": [ + "host-bind" + ], + "default": "host-bind", + "managed_volume": { + "backup": false, + "default_size_gb": 8 + } + } + ], + "ports": [ + { + "container_port": 9000, + "published_example": 9000, + "protocol": "tcp", + "required": true, + "proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat" + }, + { + "container_port": 9443, + "published_example": 9443, + "protocol": "tcp", + "required": true, + "proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat" + } + ], + "related_services": [ + { + "name": "postgresql", + "image": "docker.io/library/postgres:16-alpine" + }, + { + "name": "worker", + "image": "ghcr.io/goauthentik/server:2026.8.5" + } + ], + "restart": "unless-stopped", + "stop_grace_period": null, + "original_compose": "name: authentik\nservices:\n postgresql:\n environment:\n POSTGRES_DB: authentik\n POSTGRES_PASSWORD: ${GENERATED_PG_PASS}\n POSTGRES_USER: authentik\n healthcheck:\n interval: 30s\n retries: 5\n start_period: 20s\n test:\n - CMD-SHELL\n - pg_isready -h 127.0.0.1 -d authentik -U authentik\n timeout: 5s\n image: docker.io/library/postgres:16-alpine\n restart: unless-stopped\n volumes:\n - read_only: false\n source: database\n target: /var/lib/postgresql/data\n server:\n command: server\n depends_on:\n postgresql:\n condition: service_healthy\n environment:\n AUTHENTIK_POSTGRESQL__HOST: postgresql\n AUTHENTIK_POSTGRESQL__NAME: authentik\n AUTHENTIK_POSTGRESQL__PASSWORD: ${GENERATED_PG_PASS}\n AUTHENTIK_POSTGRESQL__USER: authentik\n AUTHENTIK_SECRET_KEY: ${GENERATED_AUTHENTIK_SECRET_KEY}\n image: ghcr.io/goauthentik/server:2026.8.5\n ports:\n - 9000:9000\n - 9443:9443\n restart: unless-stopped\n shm_size: 512mb\n volumes:\n - read_only: false\n source: ./data\n target: /data\n - read_only: false\n source: ./custom-templates\n target: /templates\n worker:\n command: worker\n depends_on:\n postgresql:\n condition: service_healthy\n environment:\n AUTHENTIK_POSTGRESQL__HOST: postgresql\n AUTHENTIK_POSTGRESQL__NAME: authentik\n AUTHENTIK_POSTGRESQL__PASSWORD: ${GENERATED_PG_PASS}\n AUTHENTIK_POSTGRESQL__USER: authentik\n AUTHENTIK_SECRET_KEY: ${GENERATED_AUTHENTIK_SECRET_KEY}\n image: ghcr.io/goauthentik/server:2026.8.5\n restart: unless-stopped\n shm_size: 512mb\n user: root\n volumes:\n - read_only: false\n source: ./data\n target: /data\n - read_only: false\n source: ./certs\n target: /certs\n - read_only: false\n source: ./custom-templates\n target: /templates\nvolumes:\n database: {}\n" + }, + "compose_stack": { + "project_name": "authentik", + "deployment_model": "one-native-oci-lxc-per-compose-service", + "user_experience": "single-application-install", + "main_service": "server", + "service_count": 3, + "services": [ + { + "name": "postgresql", + "image": "docker.io/library/postgres:16-alpine", + "is_main": false, + "role": "dependency", + "vmid_offset": 1, + "depends_on": [], + "frontend_network": false, + "private_network": true, + "compose": { + "environment": { + "POSTGRES_DB": "authentik", + "POSTGRES_PASSWORD": "${GENERATED_PG_PASS}", + "POSTGRES_USER": "authentik" + }, + "healthcheck": { + "interval": "30s", + "retries": 5, + "start_period": "20s", + "test": [ + "CMD-SHELL", + "pg_isready -h 127.0.0.1 -d authentik -U authentik" + ], + "timeout": "5s" + }, + "image": "docker.io/library/postgres:16-alpine", + "restart": "unless-stopped", + "volumes": [ + { + "source": "database", + "target": "/var/lib/postgresql/data", + "read_only": false + } + ] + } + }, + { + "name": "worker", + "image": "ghcr.io/goauthentik/server:2026.8.5", + "is_main": false, + "role": "dependency", + "vmid_offset": 2, + "depends_on": [ + "postgresql" + ], + "frontend_network": true, + "private_network": true, + "compose": { + "command": "worker", + "depends_on": { + "postgresql": { + "condition": "service_healthy" + } + }, + "environment": { + "AUTHENTIK_POSTGRESQL__HOST": "postgresql", + "AUTHENTIK_POSTGRESQL__NAME": "authentik", + "AUTHENTIK_POSTGRESQL__PASSWORD": "${GENERATED_PG_PASS}", + "AUTHENTIK_POSTGRESQL__USER": "authentik", + "AUTHENTIK_SECRET_KEY": "${GENERATED_AUTHENTIK_SECRET_KEY}" + }, + "image": "ghcr.io/goauthentik/server:2026.8.5", + "restart": "unless-stopped", + "shm_size": "512mb", + "user": "root", + "volumes": [ + { + "source": "./data", + "target": "/data", + "read_only": false + }, + { + "source": "./certs", + "target": "/certs", + "read_only": false + }, + { + "source": "./custom-templates", + "target": "/templates", + "read_only": false + } + ] + } + }, + { + "name": "server", + "image": "ghcr.io/goauthentik/server:2026.8.5", + "is_main": true, + "role": "frontend", + "vmid_offset": 0, + "depends_on": [ + "postgresql" + ], + "frontend_network": true, + "private_network": true, + "compose": { + "command": "server", + "depends_on": { + "postgresql": { + "condition": "service_healthy" + } + }, + "environment": { + "AUTHENTIK_POSTGRESQL__HOST": "postgresql", + "AUTHENTIK_POSTGRESQL__NAME": "authentik", + "AUTHENTIK_POSTGRESQL__PASSWORD": "${GENERATED_PG_PASS}", + "AUTHENTIK_POSTGRESQL__USER": "authentik", + "AUTHENTIK_SECRET_KEY": "${GENERATED_AUTHENTIK_SECRET_KEY}" + }, + "image": "ghcr.io/goauthentik/server:2026.8.5", + "ports": [ + "9000:9000", + "9443:9443" + ], + "restart": "unless-stopped", + "shm_size": "512mb", + "volumes": [ + { + "source": "./data", + "target": "/data", + "read_only": false + }, + { + "source": "./custom-templates", + "target": "/templates", + "read_only": false + } + ] + } + } + ], + "top_level": { + "name": "authentik", + "volumes": { + "database": {} + } + }, + "networking": { + "frontend": "selected-proxmox-bridge", + "private_required": true, + "private_creation": "automatic-create-if-missing", + "private_address_allocation": "automatic-static-address-per-service", + "service_discovery": "private-addresses-with-compose-service-host-aliases", + "dependency_external_access": "worker-through-selected-proxmox-bridge; postgresql-private-only", + "prompt_user_for_private_network": false + }, + "storage": [ + { + "id": "postgresql-volume-0", + "service": "postgresql", + "container_path": "/var/lib/postgresql/data", + "mode": "managed-volume", + "user_selectable": false, + "backup": true, + "shared_with_other_lxc": false, + "source_path": null, + "source_path_prompt": null + }, + { + "id": "server-volume-0", + "service": "server", + "container_path": "/data", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/data" + }, + { + "id": "server-volume-1", + "service": "server", + "container_path": "/templates", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/templates" + }, + { + "id": "worker-volume-0", + "service": "worker", + "container_path": "/data", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/data" + }, + { + "id": "worker-volume-1", + "service": "worker", + "container_path": "/certs", + "mode": "managed-volume", + "user_selectable": false, + "backup": true, + "shared_with_other_lxc": false, + "source_path": null, + "source_path_prompt": null + }, + { + "id": "worker-volume-2", + "service": "worker", + "container_path": "/templates", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/templates" + } + ], + "orchestration": { + "reserve_vmids_atomically": 3, + "start_order": [ + "postgresql", + "worker", + "server" + ], + "stop_order": [ + "server", + "worker", + "postgresql" + ], + "dependency_readiness": "compose-healthcheck-then-port-or-process-fallback", + "rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes" + }, + "installer_inputs": { + "prompted": [ + "stack_name", + "base_vmid", + "rootfs_storage", + "persistent_data_destinations", + "frontend_bridge", + "frontend_ipv4_mode" + ], + "automatic": [ + "dependent_vmids", + "private_bridge", + "private_subnet", + "private_service_addresses", + "compose_service_aliases", + "generated_secrets", + "dependency_start_and_stop_order" + ], + "generated_secrets": [ + { + "id": "pg-pass", + "strategy": "generate-cryptographically-random-at-install", + "bindings": [ + { + "service": "postgresql", + "environment_variable": "POSTGRES_PASSWORD" + }, + { + "service": "server", + "environment_variable": "AUTHENTIK_POSTGRESQL__PASSWORD" + }, + { + "service": "worker", + "environment_variable": "AUTHENTIK_POSTGRESQL__PASSWORD" + } + ] + }, + { + "id": "authentik-secret-key", + "strategy": "generate-cryptographically-random-at-install", + "bindings": [ + { + "service": "server", + "environment_variable": "AUTHENTIK_SECRET_KEY" + }, + { + "service": "worker", + "environment_variable": "AUTHENTIK_SECRET_KEY" + } + ] + } + ] + } + }, + "first_run": { + "endpoints": [ + { + "label": "Web interface", + "scheme": "http", + "port": 9000, + "path": "/", + "source": "official-compose" + } + ], + "credentials": [] + }, + "proxmox": { + "runtime": "native-oci-lxc", + "technology_status": "proxmox-technology-preview", + "defaults": { + "unprivileged": true, + "ostype": "auto-from-image", + "cores": 2, + "memory_mb": 1024, + "swap_mb": 512, + "rootfs_size_gb": 8, + "rootfs_storage": "local-lvm", + "volume_storage": "local-lvm", + "template_storage": "local", + "bridge": "vmbr0", + "ipv4": "dhcp", + "firewall": true, + "host_managed_network": true, + "onboot": true, + "features": [ + "nesting=1" + ], + "shutdown_timeout_seconds": 30 + }, + "image_metadata_policy": { + "entrypoint": "import-from-oci-image", + "cmd": "import-from-oci-image", + "environment": "import-image-env-then-apply-compose-overrides", + "user": "import-from-oci-image", + "working_dir": "import-from-oci-image", + "stop_signal": "import-from-oci-image" + }, + "installer_profile": { + "stack_driver": "generic-multi-lxc-stack", + "volume_preparations": [ + { + "container_path": "/data", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + }, + { + "container_path": "/templates", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + }, + { + "container_path": "/certs", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + }, + { + "container_path": "/var/lib/postgresql/data", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + } + ] + }, + "adaptations": [ + { + "id": "worker-docker-socket-removed", + "upstream_behavior": "The official Compose mounts the Docker socket into the worker for automatic deployment and management of separate outposts.", + "native_lxc_behavior": "The Docker socket mount is omitted. Authentik itself runs as server, PostgreSQL and worker in native OCI LXCs.", + "reason": "The ProxMenux native OCI stack does not run Docker Engine or provide a Docker daemon socket.", + "behavioral_impact": "Use the embedded outpost for a simple proxy-provider deployment, or deploy and manage an outpost separately.", + "validation": "passed-ms03-2026-10-30" + }, + { + "id": "postgres-healthcheck-normalized", + "upstream_behavior": "The official PostgreSQL healthcheck expands Compose environment variables in its command.", + "native_lxc_behavior": "The check uses the recipe’s fixed database and username with pg_isready against localhost.", + "reason": "The native stack healthcheck runs in the database LXC and does not evaluate Compose variable interpolation.", + "behavioral_impact": "It checks the same local database readiness before dependent services start.", + "validation": "passed-ms03-postgresql-readiness-2026-10-30" + }, + { + "id": "server-worker-shared-data", + "upstream_behavior": "Authentik server and worker both mount /data and /templates from the same Compose source directories.", + "native_lxc_behavior": "ProxMenux uses shared host directories for these two paths across the server and worker LXCs.", + "reason": "The same data and custom templates must be visible to both services; the stack driver shares named Compose sources through host paths.", + "behavioral_impact": "These shared host directories are not included in native LXC backups and need a separate backup.", + "validation": "passed-ms03-2026-10-30" + } + ], + "custom_compose_stack": true, + "custom_compose_interpolated": false, + "stack_image_entrypoints": {}, + "stack_completion_notes": [ + "Open the web address and create the password of the akadmin user in the first-run setup." + ] + }, + "compatibility": { + "automatic_install_candidate": true, + "validated": false, + "supported_compose_keys": [ + "cap_add", + "command", + "container_name", + "cpu_shares", + "deploy", + "devices", + "entrypoint", + "environment", + "extra_hosts", + "group_add", + "healthcheck", + "hostname", + "image", + "init", + "ipc", + "labels", + "logging", + "mac_address", + "mem_limit", + "network_mode", + "networks", + "ports", + "privileged", + "restart", + "runtime", + "security_opt", + "shm_size", + "stdin_open", + "stop_grace_period", + "stop_signal", + "sysctls", + "tty", + "ulimits", + "user", + "volumes", + "working_dir" + ], + "untranslated_blockers": [], + "policy": "Official Authentik Compose translated to three native OCI LXCs. The Docker socket mount is deliberately removed; the embedded outpost remains available." + }, + "validation": { + "schema": "passed-local-2026-10-30", + "image_pull": "passed-ms03-skopeo-and-OCI-integrity-server-worker-postgresql-2026-10-30", + "default_install": "passed-ms03-three-LXCs-started-PostgreSQL-healthy-2026-10-30", + "advanced_install": "pending", + "credentials": "pending-user-first-run-and-login", + "web_ui": "pending-user-first-run-and-login", + "storage_modes": "pending", + "restart_persistence": "pending", + "backup_restore": "pending", + "update_preserves_data": "pending", + "rollback": "pending", + "modification": "pending", + "removal": "pending", + "oci_engine_tests": "pending", + "offline_i18n_tests": "pending", + "runtime_architecture": "amd64-ms03; Skopeo manifests include amd64-and-arm64", + "database_health": "passed-pg_isready-2026-10-30" + }, + "lifecycle": { + "update_strategy": "resolve-selected-tag-and-architecture-digest-then-apply-reviewed-native-lxc-update", + "registry_state": { + "resolved_architecture": null, + "resolved_digest": null, + "image_version_label": "2026.8.5", + "image_created": null + }, + "change_detection": "compare-compose-sha256-and-resolved-image-digest", + "automatic_unattended_updates": false, + "dependency_lifecycle": { + "implementation": "proxmox-hookscript", + "trigger": "main-lxc-pre-start", + "waits_for_dependency_healthchecks": true, + "stops_dependencies_with_main": false + } + } +} diff --git a/oci/catalog/curated/authentik.json b/oci/catalog/curated/authentik.json new file mode 100644 index 00000000..23d011ff --- /dev/null +++ b/oci/catalog/curated/authentik.json @@ -0,0 +1,640 @@ +{ + "schema_version": "0.5.0", + "kind": "proxmenux.oci-template", + "id": "image-authentik", + "status": "generated-unvalidated", + "catalog_ui": { + "title": { + "en_US": "Authentik" + }, + "tagline": { + "en_US": "Identity provider for single sign-on and access control" + }, + "description": { + "en_US": "Authentik is an open-source identity provider and access platform. This recipe follows the official three-service deployment with PostgreSQL, the server and a worker, each in its own Proxmox OCI LXC." + }, + "category": "security", + "category_label": "Authentication & Security", + "author": "Authentik", + "developer": "Authentik", + "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/authentik.webp", + "thumbnail": null, + "screenshots": [], + "architectures": [ + "amd64", + "arm64" + ], + "launch": { + "scheme": "http", + "port": 9000, + "path": "/" + }, + "website": "https://goauthentik.io/", + "documentation": "https://docs.goauthentik.io/install-config/install/docker-compose/", + "repository": "https://github.com/goauthentik/authentik", + "tips": [ + "The server and worker share /data and /templates through host directories; those directories are not included in Proxmox CT backups.", + "The worker Docker-socket mount is omitted. Use the embedded outpost or deploy and manage a separate outpost manually.", + "The worker gets an interface on the selected Proxmox bridge for outbound access; PostgreSQL stays on the private stack network." + ], + "mini_changelog": [], + "display_version": "2026.8.5", + "updated_at": "2026-10-21" + }, + "source": { + "provider": "goauthentik", + "repository": "https://github.com/goauthentik/authentik", + "default_branch": "main", + "revision": "4ec0d19b98da3c329781894cd550da95d3033954", + "readme_raw_url": "https://goauthentik.io/version/2026.8/lifecycle/container/compose.yml", + "image_repository_url": "https://github.com/goauthentik/authentik/pkgs/container/server", + "readme_pushed_at": "2026-10-21T12:57:59Z", + "compose_sha256": "a3e138b5a85844d15c4a7ba6339997e43c0479d9d198582c67dd19523aadc694", + "generated_at": "2026-10-30T18:09:01+00:00" + }, + "container_contract": { + "service_name": "server", + "container_name": "authentik-server", + "image": { + "reference": "ghcr.io/goauthentik/server:2026.8.5", + "registry": "ghcr.io", + "repository": "ghcr.io/goauthentik/server", + "tag": "2026.8.5", + "digest": null, + "pull_policy": "resolve-written-reference-to-architecture-digest-at-install" + }, + "environment": [ + { + "name": "AUTHENTIK_POSTGRESQL__HOST", + "example": "postgresql", + "required": true, + "sensitive": false, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_POSTGRESQL__NAME", + "example": "authentik", + "required": true, + "sensitive": false, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_POSTGRESQL__PASSWORD", + "example": "${GENERATED_GENERATED_PG_PASS}", + "required": true, + "sensitive": true, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_POSTGRESQL__USER", + "example": "authentik", + "required": true, + "sensitive": false, + "source": "docker-compose" + }, + { + "name": "AUTHENTIK_SECRET_KEY", + "example": "${GENERATED_GENERATED_AUTHENTIK_SECRET_KEY}", + "required": true, + "sensitive": true, + "source": "docker-compose" + } + ], + "volumes": [ + { + "id": "volume-0", + "container_path": "/data", + "compose_source_example": "./data", + "read_only": false, + "required": true, + "installation_choice": [ + "host-bind" + ], + "default": "host-bind", + "managed_volume": { + "backup": false, + "default_size_gb": 8 + } + }, + { + "id": "volume-1", + "container_path": "/templates", + "compose_source_example": "./custom-templates", + "read_only": false, + "required": true, + "installation_choice": [ + "host-bind" + ], + "default": "host-bind", + "managed_volume": { + "backup": false, + "default_size_gb": 8 + } + } + ], + "ports": [ + { + "container_port": 9000, + "published_example": 9000, + "protocol": "tcp", + "required": true, + "proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat" + }, + { + "container_port": 9443, + "published_example": 9443, + "protocol": "tcp", + "required": true, + "proxmox_behavior": "listener-on-dedicated-lxc-address-no-nat" + } + ], + "related_services": [ + { + "name": "postgresql", + "image": "docker.io/library/postgres:16-alpine" + }, + { + "name": "worker", + "image": "ghcr.io/goauthentik/server:2026.8.5" + } + ], + "restart": "unless-stopped", + "stop_grace_period": null, + "original_compose": "name: authentik\nservices:\n postgresql:\n environment:\n POSTGRES_DB: authentik\n POSTGRES_PASSWORD: ${GENERATED_PG_PASS}\n POSTGRES_USER: authentik\n healthcheck:\n interval: 30s\n retries: 5\n start_period: 20s\n test:\n - CMD-SHELL\n - pg_isready -h 127.0.0.1 -d authentik -U authentik\n timeout: 5s\n image: docker.io/library/postgres:16-alpine\n restart: unless-stopped\n volumes:\n - read_only: false\n source: database\n target: /var/lib/postgresql/data\n server:\n command: server\n depends_on:\n postgresql:\n condition: service_healthy\n environment:\n AUTHENTIK_POSTGRESQL__HOST: postgresql\n AUTHENTIK_POSTGRESQL__NAME: authentik\n AUTHENTIK_POSTGRESQL__PASSWORD: ${GENERATED_PG_PASS}\n AUTHENTIK_POSTGRESQL__USER: authentik\n AUTHENTIK_SECRET_KEY: ${GENERATED_AUTHENTIK_SECRET_KEY}\n image: ghcr.io/goauthentik/server:2026.8.5\n ports:\n - 9000:9000\n - 9443:9443\n restart: unless-stopped\n shm_size: 512mb\n volumes:\n - read_only: false\n source: ./data\n target: /data\n - read_only: false\n source: ./custom-templates\n target: /templates\n worker:\n command: worker\n depends_on:\n postgresql:\n condition: service_healthy\n environment:\n AUTHENTIK_POSTGRESQL__HOST: postgresql\n AUTHENTIK_POSTGRESQL__NAME: authentik\n AUTHENTIK_POSTGRESQL__PASSWORD: ${GENERATED_PG_PASS}\n AUTHENTIK_POSTGRESQL__USER: authentik\n AUTHENTIK_SECRET_KEY: ${GENERATED_AUTHENTIK_SECRET_KEY}\n image: ghcr.io/goauthentik/server:2026.8.5\n restart: unless-stopped\n shm_size: 512mb\n user: root\n volumes:\n - read_only: false\n source: ./data\n target: /data\n - read_only: false\n source: ./certs\n target: /certs\n - read_only: false\n source: ./custom-templates\n target: /templates\nvolumes:\n database: {}\n" + }, + "compose_stack": { + "project_name": "authentik", + "deployment_model": "one-native-oci-lxc-per-compose-service", + "user_experience": "single-application-install", + "main_service": "server", + "service_count": 3, + "services": [ + { + "name": "postgresql", + "image": "docker.io/library/postgres:16-alpine", + "is_main": false, + "role": "dependency", + "vmid_offset": 1, + "depends_on": [], + "frontend_network": false, + "private_network": true, + "compose": { + "environment": { + "POSTGRES_DB": "authentik", + "POSTGRES_PASSWORD": "${GENERATED_PG_PASS}", + "POSTGRES_USER": "authentik" + }, + "healthcheck": { + "interval": "30s", + "retries": 5, + "start_period": "20s", + "test": [ + "CMD-SHELL", + "pg_isready -h 127.0.0.1 -d authentik -U authentik" + ], + "timeout": "5s" + }, + "image": "docker.io/library/postgres:16-alpine", + "restart": "unless-stopped", + "volumes": [ + { + "source": "database", + "target": "/var/lib/postgresql/data", + "read_only": false + } + ] + } + }, + { + "name": "worker", + "image": "ghcr.io/goauthentik/server:2026.8.5", + "is_main": false, + "role": "dependency", + "vmid_offset": 2, + "depends_on": [ + "postgresql" + ], + "frontend_network": true, + "private_network": true, + "compose": { + "command": "worker", + "depends_on": { + "postgresql": { + "condition": "service_healthy" + } + }, + "environment": { + "AUTHENTIK_POSTGRESQL__HOST": "postgresql", + "AUTHENTIK_POSTGRESQL__NAME": "authentik", + "AUTHENTIK_POSTGRESQL__PASSWORD": "${GENERATED_PG_PASS}", + "AUTHENTIK_POSTGRESQL__USER": "authentik", + "AUTHENTIK_SECRET_KEY": "${GENERATED_AUTHENTIK_SECRET_KEY}" + }, + "image": "ghcr.io/goauthentik/server:2026.8.5", + "restart": "unless-stopped", + "shm_size": "512mb", + "user": "root", + "volumes": [ + { + "source": "./data", + "target": "/data", + "read_only": false + }, + { + "source": "./certs", + "target": "/certs", + "read_only": false + }, + { + "source": "./custom-templates", + "target": "/templates", + "read_only": false + } + ] + } + }, + { + "name": "server", + "image": "ghcr.io/goauthentik/server:2026.8.5", + "is_main": true, + "role": "frontend", + "vmid_offset": 0, + "depends_on": [ + "postgresql" + ], + "frontend_network": true, + "private_network": true, + "compose": { + "command": "server", + "depends_on": { + "postgresql": { + "condition": "service_healthy" + } + }, + "environment": { + "AUTHENTIK_POSTGRESQL__HOST": "postgresql", + "AUTHENTIK_POSTGRESQL__NAME": "authentik", + "AUTHENTIK_POSTGRESQL__PASSWORD": "${GENERATED_PG_PASS}", + "AUTHENTIK_POSTGRESQL__USER": "authentik", + "AUTHENTIK_SECRET_KEY": "${GENERATED_AUTHENTIK_SECRET_KEY}" + }, + "image": "ghcr.io/goauthentik/server:2026.8.5", + "ports": [ + "9000:9000", + "9443:9443" + ], + "restart": "unless-stopped", + "shm_size": "512mb", + "volumes": [ + { + "source": "./data", + "target": "/data", + "read_only": false + }, + { + "source": "./custom-templates", + "target": "/templates", + "read_only": false + } + ] + } + } + ], + "top_level": { + "name": "authentik", + "volumes": { + "database": {} + } + }, + "networking": { + "frontend": "selected-proxmox-bridge", + "private_required": true, + "private_creation": "automatic-create-if-missing", + "private_address_allocation": "automatic-static-address-per-service", + "service_discovery": "private-addresses-with-compose-service-host-aliases", + "dependency_external_access": "worker-through-selected-proxmox-bridge; postgresql-private-only", + "prompt_user_for_private_network": false + }, + "storage": [ + { + "id": "postgresql-volume-0", + "service": "postgresql", + "container_path": "/var/lib/postgresql/data", + "mode": "managed-volume", + "user_selectable": false, + "backup": true, + "shared_with_other_lxc": false, + "source_path": null, + "source_path_prompt": null + }, + { + "id": "server-volume-0", + "service": "server", + "container_path": "/data", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/data" + }, + { + "id": "server-volume-1", + "service": "server", + "container_path": "/templates", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/templates" + }, + { + "id": "worker-volume-0", + "service": "worker", + "container_path": "/data", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/data" + }, + { + "id": "worker-volume-1", + "service": "worker", + "container_path": "/certs", + "mode": "managed-volume", + "user_selectable": false, + "backup": true, + "shared_with_other_lxc": false, + "source_path": null, + "source_path_prompt": null + }, + { + "id": "worker-volume-2", + "service": "worker", + "container_path": "/templates", + "mode": "host-bind", + "user_selectable": true, + "backup": false, + "shared_with_other_lxc": true, + "source_path": null, + "source_path_prompt": "Host directory for authentik:/templates" + } + ], + "orchestration": { + "reserve_vmids_atomically": 3, + "start_order": [ + "postgresql", + "worker", + "server" + ], + "stop_order": [ + "server", + "worker", + "postgresql" + ], + "dependency_readiness": "compose-healthcheck-then-port-or-process-fallback", + "rollback_on_failure": "remove-new-rootfs-preserve-created-persistent-volumes" + }, + "installer_inputs": { + "prompted": [ + "stack_name", + "base_vmid", + "rootfs_storage", + "persistent_data_destinations", + "frontend_bridge", + "frontend_ipv4_mode" + ], + "automatic": [ + "dependent_vmids", + "private_bridge", + "private_subnet", + "private_service_addresses", + "compose_service_aliases", + "generated_secrets", + "dependency_start_and_stop_order" + ], + "generated_secrets": [ + { + "id": "pg-pass", + "strategy": "generate-cryptographically-random-at-install", + "bindings": [ + { + "service": "postgresql", + "environment_variable": "POSTGRES_PASSWORD" + }, + { + "service": "server", + "environment_variable": "AUTHENTIK_POSTGRESQL__PASSWORD" + }, + { + "service": "worker", + "environment_variable": "AUTHENTIK_POSTGRESQL__PASSWORD" + } + ] + }, + { + "id": "authentik-secret-key", + "strategy": "generate-cryptographically-random-at-install", + "bindings": [ + { + "service": "server", + "environment_variable": "AUTHENTIK_SECRET_KEY" + }, + { + "service": "worker", + "environment_variable": "AUTHENTIK_SECRET_KEY" + } + ] + } + ] + } + }, + "first_run": { + "endpoints": [ + { + "label": "Web interface", + "scheme": "http", + "port": 9000, + "path": "/", + "source": "official-compose" + } + ], + "credentials": [] + }, + "proxmox": { + "runtime": "native-oci-lxc", + "technology_status": "proxmox-technology-preview", + "defaults": { + "unprivileged": true, + "ostype": "auto-from-image", + "cores": 2, + "memory_mb": 1024, + "swap_mb": 512, + "rootfs_size_gb": 8, + "rootfs_storage": "local-lvm", + "volume_storage": "local-lvm", + "template_storage": "local", + "bridge": "vmbr0", + "ipv4": "dhcp", + "firewall": true, + "host_managed_network": true, + "onboot": true, + "features": [ + "nesting=1" + ], + "shutdown_timeout_seconds": 30 + }, + "image_metadata_policy": { + "entrypoint": "import-from-oci-image", + "cmd": "import-from-oci-image", + "environment": "import-image-env-then-apply-compose-overrides", + "user": "import-from-oci-image", + "working_dir": "import-from-oci-image", + "stop_signal": "import-from-oci-image" + }, + "installer_profile": { + "stack_driver": "generic-multi-lxc-stack", + "volume_preparations": [ + { + "container_path": "/data", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + }, + { + "container_path": "/templates", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + }, + { + "container_path": "/certs", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + }, + { + "container_path": "/var/lib/postgresql/data", + "remove_lost_found": true, + "owner_strategy": "mapped-application-user", + "only_when_mount_type": "managed-volume" + } + ] + }, + "adaptations": [ + { + "id": "worker-docker-socket-removed", + "upstream_behavior": "The official Compose mounts the Docker socket into the worker for automatic deployment and management of separate outposts.", + "native_lxc_behavior": "The Docker socket mount is omitted. Authentik itself runs as server, PostgreSQL and worker in native OCI LXCs.", + "reason": "The ProxMenux native OCI stack does not run Docker Engine or provide a Docker daemon socket.", + "behavioral_impact": "Use the embedded outpost for a simple proxy-provider deployment, or deploy and manage an outpost separately.", + "validation": "passed-ms03-2026-10-30" + }, + { + "id": "postgres-healthcheck-normalized", + "upstream_behavior": "The official PostgreSQL healthcheck expands Compose environment variables in its command.", + "native_lxc_behavior": "The check uses the recipe’s fixed database and username with pg_isready against localhost.", + "reason": "The native stack healthcheck runs in the database LXC and does not evaluate Compose variable interpolation.", + "behavioral_impact": "It checks the same local database readiness before dependent services start.", + "validation": "passed-ms03-postgresql-readiness-2026-10-30" + }, + { + "id": "server-worker-shared-data", + "upstream_behavior": "Authentik server and worker both mount /data and /templates from the same Compose source directories.", + "native_lxc_behavior": "ProxMenux uses shared host directories for these two paths across the server and worker LXCs.", + "reason": "The same data and custom templates must be visible to both services; the stack driver shares named Compose sources through host paths.", + "behavioral_impact": "These shared host directories are not included in native LXC backups and need a separate backup.", + "validation": "passed-ms03-2026-10-30" + } + ], + "custom_compose_stack": true, + "custom_compose_interpolated": false, + "stack_image_entrypoints": {}, + "stack_completion_notes": [ + "Open the web address and create the password of the akadmin user in the first-run setup." + ] + }, + "compatibility": { + "automatic_install_candidate": true, + "validated": false, + "supported_compose_keys": [ + "cap_add", + "command", + "container_name", + "cpu_shares", + "deploy", + "devices", + "entrypoint", + "environment", + "extra_hosts", + "group_add", + "healthcheck", + "hostname", + "image", + "init", + "ipc", + "labels", + "logging", + "mac_address", + "mem_limit", + "network_mode", + "networks", + "ports", + "privileged", + "restart", + "runtime", + "security_opt", + "shm_size", + "stdin_open", + "stop_grace_period", + "stop_signal", + "sysctls", + "tty", + "ulimits", + "user", + "volumes", + "working_dir" + ], + "untranslated_blockers": [], + "policy": "Official Authentik Compose translated to three native OCI LXCs. The Docker socket mount is deliberately removed; the embedded outpost remains available." + }, + "validation": { + "schema": "passed-local-2026-10-30", + "image_pull": "passed-ms03-skopeo-and-OCI-integrity-server-worker-postgresql-2026-10-30", + "default_install": "passed-ms03-three-LXCs-started-PostgreSQL-healthy-2026-10-30", + "advanced_install": "pending", + "credentials": "pending-user-first-run-and-login", + "web_ui": "pending-user-first-run-and-login", + "storage_modes": "pending", + "restart_persistence": "pending", + "backup_restore": "pending", + "update_preserves_data": "pending", + "rollback": "pending", + "modification": "pending", + "removal": "pending", + "oci_engine_tests": "pending", + "offline_i18n_tests": "pending", + "runtime_architecture": "amd64-ms03; Skopeo manifests include amd64-and-arm64", + "database_health": "passed-pg_isready-2026-10-30" + }, + "lifecycle": { + "update_strategy": "resolve-selected-tag-and-architecture-digest-then-apply-reviewed-native-lxc-update", + "registry_state": { + "resolved_architecture": null, + "resolved_digest": null, + "image_version_label": "2026.8.5", + "image_created": null + }, + "change_detection": "compare-compose-sha256-and-resolved-image-digest", + "automatic_unattended_updates": false, + "dependency_lifecycle": { + "implementation": "proxmox-hookscript", + "trigger": "main-lxc-pre-start", + "waits_for_dependency_healthchecks": true, + "stops_dependencies_with_main": false + } + } +} diff --git a/oci/catalog/index.json b/oci/catalog/index.json index 1b6d7b4f..0457de05 100644 --- a/oci/catalog/index.json +++ b/oci/catalog/index.json @@ -2,7 +2,7 @@ "schema_version": "0.2.0", "kind": "proxmenux.oci-catalog-index", "provider": "multiple-container-images", - "generated_at": "2026-10-26T08:05:54+00:00", + "generated_at": "2026-10-30T18:13:36+00:00", "applications": [ { "id": "2fauth", @@ -752,6 +752,39 @@ "hidden": false, "template_id": "image-audiobookshelf" }, + { + "id": "authentik", + "provider": "goauthentik", + "template_family": "curated-profile", + "title": "Authentik", + "repository": "https://github.com/goauthentik/authentik", + "description": "Authentik is an open-source identity provider and access platform. This recipe follows the official three-service deployment with PostgreSQL, the server and a worker, each in its own Proxmox OCI LXC.", + "website": "https://goauthentik.io/", + "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/authentik.webp", + "architectures": [ + "amd64", + "arm64" + ], + "updated_at": "2026-10-21", + "main_image": "ghcr.io/goauthentik/server:2026.8.5", + "category": "security", + "category_label": "Authentication & Security", + "replaces_discovered_ids": [], + "curated_path": "catalog/curated/authentik.json", + "template_id": "image-authentik", + "template": "apps/authentik.json", + "template_status": "generated-unvalidated", + "hidden": false, + "automatic_install_candidate": true, + "untranslated_blockers": [], + "requires_privileged_lxc": false, + "optional_privileged_lxc": false, + "requires_host_pid_namespace": false, + "requires_relaxed_confinement": false, + "optional_relaxed_confinement": false, + "requires_security_confirmation": false, + "multi_container": true + }, { "id": "autobrr", "source_app_id": "autobrr", @@ -18976,7 +19009,7 @@ "skipped": [] }, "curated_profiles": { - "included_count": 17 + "included_count": 18 } } } diff --git a/oci/src/proxmenux_oci/stack.py b/oci/src/proxmenux_oci/stack.py index 1c383b44..11e196f2 100644 --- a/oci/src/proxmenux_oci/stack.py +++ b/oci/src/proxmenux_oci/stack.py @@ -655,8 +655,14 @@ def build_stack(template, ui, mode='advanced'): from .custom_mounts import ask_stack_custom_mounts ask_stack_custom_mounts(ui, drafts, volumes) bridge = ask_bridge(ui, translate('Access bridge'), defaults['bridge'], mode) - frontend_names = [s['name'] for s in services if s['name'] == template['compose_stack']['main_service'] or (custom and service_endpoint(s))] - address_labels = frontend_names if custom else [''] + frontend_names = [ + s['name'] for s in services + if (s['name'] == template['compose_stack']['main_service'] + or s.get('frontend_network') is True + or (custom and service_endpoint(s))) + ] + labelled_frontends = custom or len(frontend_names) > 1 + address_labels = frontend_names if labelled_frontends else [''] addresses, gateway = access.ask_addresses(ui, bridge, address_labels) timezone = quiet.ask(translate('Timezone'), host.timezone()) envs = resolve_environments(services, timezone, generators, literal=literal) @@ -749,7 +755,7 @@ def build_stack(template, ui, mode='advanced'): plans[-1]['public_environment_bindings'] = public_bindings[s['name']] if s['name'] in frontend_names and s['name'] not in jobs: plans[-1]['frontend'] = True - plans[-1]['frontend_ipv4'] = addresses[s['name']] if custom else addresses[''] + plans[-1]['frontend_ipv4'] = addresses[s['name']] if labelled_frontends else addresses[''] if mode != DEFAULT_MODE: from .extra_devices import ask_stack_extra_devices ask_stack_extra_devices(ui, plans) diff --git a/oci/tests/test_authentik_recipe.py b/oci/tests/test_authentik_recipe.py new file mode 100644 index 00000000..268d84f9 --- /dev/null +++ b/oci/tests/test_authentik_recipe.py @@ -0,0 +1,166 @@ +"""Authentik keeps the vendor's three-service deployment as native OCI LXCs.""" + +import json +from pathlib import Path +import sys +import unittest +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "src")) + +from proxmenux_oci.installer import DEFAULT_MODE +from proxmenux_oci.stack import DefaultsUI, build_stack + + +class AuthentikRecipeTests(unittest.TestCase): + def setUp(self): + self.curated = json.loads( + (ROOT / "catalog/curated/authentik.json").read_text(encoding="utf-8") + ) + self.generated = json.loads( + (ROOT / "catalog/apps/authentik.json").read_text(encoding="utf-8") + ) + self.services = { + service["name"]: service + for service in self.curated["compose_stack"]["services"] + } + + def test_generated_catalog_copy_matches_curated_profile(self): + self.assertEqual(self.curated, self.generated) + + def test_official_three_service_stack_uses_pinned_images_and_order(self): + self.assertEqual(set(self.services), {"postgresql", "server", "worker"}) + self.assertEqual(self.curated["compose_stack"]["main_service"], "server") + self.assertEqual( + self.services["server"]["image"], "ghcr.io/goauthentik/server:2026.8.5" + ) + self.assertEqual( + self.services["worker"]["image"], "ghcr.io/goauthentik/server:2026.8.5" + ) + self.assertEqual( + self.services["postgresql"]["image"], + "docker.io/library/postgres:16-alpine", + ) + self.assertEqual(self.services["server"]["depends_on"], ["postgresql"]) + self.assertEqual(self.services["worker"]["depends_on"], ["postgresql"]) + + def test_generated_database_and_application_secrets_are_shared_correctly(self): + services = { + name: service["compose"].get("environment", {}) + for name, service in self.services.items() + } + self.assertEqual( + services["postgresql"]["POSTGRES_PASSWORD"], "${GENERATED_PG_PASS}" + ) + for name in ("server", "worker"): + self.assertEqual( + services[name]["AUTHENTIK_POSTGRESQL__PASSWORD"], + "${GENERATED_PG_PASS}", + ) + self.assertEqual( + services[name]["AUTHENTIK_SECRET_KEY"], + "${GENERATED_AUTHENTIK_SECRET_KEY}", + ) + generated = self.curated["compose_stack"]["installer_inputs"][ + "generated_secrets" + ] + self.assertEqual({secret["id"] for secret in generated}, { + "pg-pass", + "authentik-secret-key", + }) + + def test_persistent_mounts_and_database_healthcheck_are_preserved(self): + mounts = { + name: service["compose"].get("volumes", []) + for name, service in self.services.items() + } + self.assertIn( + {"source": "database", "target": "/var/lib/postgresql/data", "read_only": False}, + mounts["postgresql"], + ) + self.assertIn( + {"source": "./data", "target": "/data", "read_only": False}, + mounts["server"], + ) + self.assertIn( + {"source": "./data", "target": "/data", "read_only": False}, + mounts["worker"], + ) + self.assertIn( + {"source": "./custom-templates", "target": "/templates", "read_only": False}, + mounts["server"], + ) + self.assertIn( + {"source": "./custom-templates", "target": "/templates", "read_only": False}, + mounts["worker"], + ) + self.assertIn( + {"source": "./certs", "target": "/certs", "read_only": False}, + mounts["worker"], + ) + check = self.services["postgresql"]["compose"]["healthcheck"]["test"] + self.assertEqual( + check, + ["CMD-SHELL", "pg_isready -h 127.0.0.1 -d authentik -U authentik"], + ) + + def test_cross_lxc_data_and_templates_are_host_shared_and_backed_up_separately(self): + shared = [ + item + for item in self.curated["compose_stack"]["storage"] + if item["container_path"] in ("/data", "/templates") + ] + self.assertEqual(len(shared), 4) + self.assertTrue(all(item["mode"] == "host-bind" for item in shared)) + self.assertTrue(all(item["shared_with_other_lxc"] for item in shared)) + self.assertTrue(all(item["backup"] is False for item in shared)) + self.assertTrue( + all( + volume["default"] == "host-bind" + and volume["managed_volume"]["backup"] is False + for volume in self.curated["container_contract"]["volumes"] + ) + ) + + def test_docker_socket_is_not_mounted_and_both_web_ports_are_exposed(self): + all_mounts = [ + mount + for service in self.services.values() + for mount in service["compose"].get("volumes", []) + ] + self.assertFalse(any("docker.sock" in mount for mount in all_mounts)) + self.assertEqual( + self.services["server"]["compose"]["ports"], + ["9000:9000", "9443:9443"], + ) + + def test_worker_gets_lan_interface_while_postgresql_stays_private(self): + selected = {"worker": "192.0.2.21/24", "server": "192.0.2.22/24"} + with patch("proxmenux_oci.installer.ask_storage", return_value="local-zfs"), \ + patch("proxmenux_oci.installer.ask_bridge", return_value="vmbr0"), \ + patch("proxmenux_oci.host.timezone", return_value="UTC"), \ + patch("proxmenux_oci.network.ask_addresses", return_value=(selected, "192.0.2.1")) as ask_addresses: + plan = build_stack(self.curated, DefaultsUI(), DEFAULT_MODE) + + services = {service["name"]: service for service in plan["services"]} + self.assertEqual(ask_addresses.call_args.args[2], ["worker", "server"]) + self.assertTrue(services["worker"]["frontend"]) + self.assertEqual(services["worker"]["frontend_ipv4"], selected["worker"]) + self.assertTrue(services["server"]["frontend"]) + self.assertEqual(services["server"]["frontend_ipv4"], selected["server"]) + self.assertNotIn("frontend", services["postgresql"]) + + def test_first_run_discloses_the_initial_admin_setup_without_faking_a_password(self): + # akadmin has no password until it is created in the setup wizard, + # which opens from the root address. + first_run = self.curated["first_run"] + self.assertEqual(first_run["endpoints"][0]["port"], 9000) + self.assertEqual(first_run["endpoints"][0]["path"], "/") + self.assertEqual(first_run["credentials"], []) + self.assertIn("akadmin", self.curated["proxmox"]["stack_completion_notes"][0]) + + +if __name__ == "__main__": + unittest.main()