From 4cffddab51270b4b54ea50bebb8f3320b9ad7523 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Tue, 1 Jul 2025 22:15:05 +0200 Subject: [PATCH] Finished Mobilizon OIDC implementation --- filter_plugins/docker_image.py | 19 +- group_vars/all/12_oidc.yml | 9 +- roles/docker-funkwhale/templates/env.j2 | 4 +- roles/docker-mobilizon/Todo.md | 2 - roles/docker-mobilizon/tasks/main.yml | 6 + .../docker-mobilizon/templates/config.exs.j2 | 278 ++++++++++++++++++ .../templates/docker-compose.yml.j2 | 16 +- roles/docker-mobilizon/templates/env.j2 | 37 +-- roles/docker-mobilizon/vars/configuration.yml | 13 +- roles/docker-mobilizon/vars/main.yml | 8 +- roles/docker-postgres/tasks/main.yml | 17 +- roles/docker-postgres/vars/configuration.yml | 13 +- .../test_deprecated_version_key.py | 10 +- .../test_docker_images_configuration.py | 44 +-- tests/unit/test_docker_image.py | 59 +++- 15 files changed, 409 insertions(+), 126 deletions(-) delete mode 100644 roles/docker-mobilizon/Todo.md create mode 100644 roles/docker-mobilizon/templates/config.exs.j2 diff --git a/filter_plugins/docker_image.py b/filter_plugins/docker_image.py index 9842df5b..74ae5165 100644 --- a/filter_plugins/docker_image.py +++ b/filter_plugins/docker_image.py @@ -1,15 +1,14 @@ -# filter_plugins/docker_image.py +def get_docker_image(applications, application_id, image_key:str=None): + image_key = image_key if image_key else application_id + docker = applications.get(application_id, {}).get("docker", {}) + version = docker.get("versions", {}).get(image_key) + image = docker.get("images", {}).get(image_key) -def get_docker_image(applications, application_id, image_key): - app = applications.get(application_id, {}) - docker = app.get("docker", {}) - images = docker.get("images", {}) - versions = docker.get("versions", {}) - version = versions.get(image_key) or app.get("version") - image = images.get(image_key) + if not image: + raise ValueError(f"Missing image for {application_id}:{image_key}") - if not image or not version: - raise ValueError(f"Missing image or version for {application_id}:{image_key}") + if not version: + raise ValueError(f"Missing version for {application_id}:{image_key}") return f"{image}:{version}" diff --git a/group_vars/all/12_oidc.yml b/group_vars/all/12_oidc.yml index 2516da19..c5f89732 100644 --- a/group_vars/all/12_oidc.yml +++ b/group_vars/all/12_oidc.yml @@ -9,10 +9,17 @@ ## Helper Variables: _oidc_client_realm: "{{ oidc.client.realm if oidc.client is defined and oidc.client.realm is defined else primary_domain }}" -_oidc_client_issuer_url: "{{ web_protocol }}://{{domains | get_domain('keycloak')}}/realms/{{_oidc_client_realm}}" +_oidc_url: "{{ + (oidc.url + if (oidc is defined and oidc.url is defined) + else web_protocol ~ '://' ~ (domains | get_domain('keycloak')) + ) + }}" +_oidc_client_issuer_url: "{{ _oidc_url }}/realms/{{_oidc_client_realm}}" _oidc_client_id: "{{ oidc.client.id if oidc.client is defined and oidc.client.id is defined else primary_domain }}" defaults_oidc: + url: "{{ _oidc_url }}" client: id: "{{ _oidc_client_id }}" # Client identifier, typically matching your primary domain # secret: # Client secret for authenticating with the OIDC provider (set in the inventory file). Recommend greater then 32 characters diff --git a/roles/docker-funkwhale/templates/env.j2 b/roles/docker-funkwhale/templates/env.j2 index 981eb491..7d3fbcfb 100644 --- a/roles/docker-funkwhale/templates/env.j2 +++ b/roles/docker-funkwhale/templates/env.j2 @@ -41,7 +41,7 @@ FUNKWHALE_WEB_WORKERS=4 # your instance. It cannot be changed after initial deployment # without breaking your instance. FUNKWHALE_HOSTNAME={{domains | get_domain(application_id)}} -FUNKWHALE_PROTOCOL=https +FUNKWHALE_PROTOCOL={{ web_protocol }} # Log level (debug, info, warning, error, critical) LOGLEVEL={% if enable_debug | bool %}debug{% else %}error{% endif %} @@ -60,7 +60,7 @@ LOGLEVEL={% if enable_debug | bool %}debug{% else %}error{% endif %} # (returns `noreply%40youremail.host`) # EMAIL_CONFIG=smtp://user:password@youremail.host:25 # EMAIL_CONFIG=smtp+ssl://user:password@youremail.host:465 -EMAIL_CONFIG=smtp+tls://no-reply:{{ users['no-reply'].mailu_token }}@{{system_email.host}}:{{system_email.port}} +EMAIL_CONFIG=smtp+tls://{{ users['no-reply'].username }}:{{ users['no-reply'].mailu_token }}@{{system_email.host}}:{{system_email.port}} # Make e-mail verification mandatory before using the service # Doesn't apply to admins. diff --git a/roles/docker-mobilizon/Todo.md b/roles/docker-mobilizon/Todo.md deleted file mode 100644 index 4aaa0e2d..00000000 --- a/roles/docker-mobilizon/Todo.md +++ /dev/null @@ -1,2 +0,0 @@ -# Todo -- Implement \ No newline at end of file diff --git a/roles/docker-mobilizon/tasks/main.yml b/roles/docker-mobilizon/tasks/main.yml index 0443f5d8..49fbf6bb 100644 --- a/roles/docker-mobilizon/tasks/main.yml +++ b/roles/docker-mobilizon/tasks/main.yml @@ -10,4 +10,10 @@ domain: "{{ domains | get_domain(application_id) }}" http_port: "{{ ports.localhost.http[application_id] }}" +- name: add config.exs + template: + src: "config.exs.j2" + dest: "{{ mobilizon_host_conf_exs_file }}" + notify: docker compose up + - include_tasks: "{{ playbook_dir }}/roles/docker-compose/tasks/create-files.yml" diff --git a/roles/docker-mobilizon/templates/config.exs.j2 b/roles/docker-mobilizon/templates/config.exs.j2 new file mode 100644 index 00000000..606c7b55 --- /dev/null +++ b/roles/docker-mobilizon/templates/config.exs.j2 @@ -0,0 +1,278 @@ +# Mobilizon instance configuration + +import Config +import Mobilizon.Service.Config.Helpers + +{:ok, _} = Application.ensure_all_started(:tls_certificate_check) + +loglevels = [ + :emergency, + :alert, + :critical, + :error, + :warning, + :notice, + :info, + :debug +] + +loglevel_env = System.get_env("MOBILIZON_LOGLEVEL", "error") + +loglevel = + if loglevel_env in Enum.map(loglevels, &to_string/1) do + String.to_existing_atom(loglevel_env) + else + :error + end + +listen_ip = System.get_env("MOBILIZON_INSTANCE_LISTEN_IP", "0.0.0.0") + +listen_ip = + case listen_ip |> to_charlist() |> :inet.parse_address() do + {:ok, listen_ip} -> listen_ip + _ -> raise "MOBILIZON_INSTANCE_LISTEN_IP does not match the expected IP format." + end + +config :mobilizon, Mobilizon.Web.Endpoint, + server: true, + url: [host: System.get_env("MOBILIZON_INSTANCE_HOST", "mobilizon.lan")], + http: [ + port: String.to_integer(System.get_env("MOBILIZON_INSTANCE_PORT", "4000")), + ip: listen_ip + ], + secret_key_base: System.get_env("MOBILIZON_INSTANCE_SECRET_KEY_BASE", "changethis") + +config :mobilizon, Mobilizon.Web.Auth.Guardian, + secret_key: System.get_env("MOBILIZON_INSTANCE_SECRET_KEY", "changethis") + +config :mobilizon, :instance, + name: System.get_env("MOBILIZON_INSTANCE_NAME", "Mobilizon"), + description: "Change this to a proper description of your instance", + hostname: System.get_env("MOBILIZON_INSTANCE_HOST", "mobilizon.lan"), + registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN", "false") == "true", + registration_email_allowlist: + System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_EMAIL_ALLOWLIST", "") + |> String.split(",", trim: true), + registration_email_denylist: + System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_EMAIL_DENYLIST", "") + |> String.split(",", trim: true), + disable_database_login: + System.get_env("MOBILIZON_INSTANCE_DISABLE_DATABASE_LOGIN", "false") == "true", + default_language: System.get_env("MOBILIZON_INSTANCE_DEFAULT_LANGUAGE", "en"), + demo: System.get_env("MOBILIZON_INSTANCE_DEMO", "false") == "true", + allow_relay: System.get_env("MOBILIZON_INSTANCE_ALLOW_RELAY", "true") == "true", + federating: System.get_env("MOBILIZON_INSTANCE_FEDERATING", "true") == "true", + enable_instance_feeds: + System.get_env("MOBILIZON_INSTANCE_ENABLE_INSTANCE_FEEDS", "true") == "true", + email_from: System.get_env("MOBILIZON_INSTANCE_EMAIL", "noreply@mobilizon.lan"), + email_reply_to: System.get_env("MOBILIZON_REPLY_EMAIL", "noreply@mobilizon.lan") + +config :mobilizon, Mobilizon.Storage.Repo, + adapter: Ecto.Adapters.Postgres, + username: System.get_env("MOBILIZON_DATABASE_USERNAME", "username"), + password: System.get_env("MOBILIZON_DATABASE_PASSWORD", "password"), + database: System.get_env("MOBILIZON_DATABASE_DBNAME", "mobilizon"), + hostname: System.get_env("MOBILIZON_DATABASE_HOST", "postgres"), + port: System.get_env("MOBILIZON_DATABASE_PORT", "5432"), + ssl: System.get_env("MOBILIZON_DATABASE_SSL", "false") == "true", + pool_size: 10 + +config :logger, level: loglevel + +config :mobilizon, Mobilizon.Web.Email.Mailer, + adapter: Swoosh.Adapters.SMTP, + relay: System.get_env("MOBILIZON_SMTP_SERVER", "localhost"), + port: System.get_env("MOBILIZON_SMTP_PORT", "25"), + username: System.get_env("MOBILIZON_SMTP_USERNAME", nil), + password: System.get_env("MOBILIZON_SMTP_PASSWORD", nil), + tls: System.get_env("MOBILIZON_SMTP_TLS", "if_available"), + tls_options: + :tls_certificate_check.options(System.get_env("MOBILIZON_SMTP_SERVER", "localhost")), + ssl: System.get_env("MOBILIZON_SMTP_SSL", "false"), + retries: 1, + no_mx_lookups: false, + auth: System.get_env("MOBILIZON_SMTP_AUTH", "if_available") + +config :geolix, + databases: [ + %{ + id: :city, + adapter: Geolix.Adapter.MMDB2, + source: "/var/lib/mobilizon/geo_db/GeoLite2-City.mmdb" + } + ] + +config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, + uploads: System.get_env("MOBILIZON_UPLOADS", "/var/lib/mobilizon/uploads") + +formats = + if System.get_env("MOBILIZON_EXPORTS_FORMAT_CSV_ENABLED", "true") == "true" do + [Mobilizon.Service.Export.Participants.CSV] + else + [] + end + +formats = + if System.get_env("MOBILIZON_EXPORTS_FORMAT_PDF_ENABLED", "true") == "true" do + formats ++ [Mobilizon.Service.Export.Participants.PDF] + else + formats + end + +formats = + if System.get_env("MOBILIZON_EXPORTS_FORMAT_ODS_ENABLED", "true") == "true" do + formats ++ [Mobilizon.Service.Export.Participants.ODS] + else + formats + end + +config :mobilizon, :exports, + path: System.get_env("MOBILIZON_UPLOADS_EXPORTS", "/var/lib/mobilizon/uploads/exports"), + formats: formats + +config :tz_world, + data_dir: System.get_env("MOBILIZON_TIMEZONES_DIR", "/var/lib/mobilizon/timezones") + +config :tzdata, :data_dir, System.get_env("MOBILIZON_TZDATA_DIR", "/var/lib/mobilizon/tzdata") + +config :web_push_encryption, :vapid_details, + subject: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_SUBJECT", nil), + public_key: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_PUBLIC_KEY", nil), + private_key: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_PRIVATE_KEY", nil) + +geospatial_service = + case System.get_env("MOBILIZON_GEOSPATIAL_SERVICE", "Nominatim") do + "Nominatim" -> Mobilizon.Service.Geospatial.Nominatim + "Addok" -> Mobilizon.Service.Geospatial.Addok + "Photon" -> Mobilizon.Service.Geospatial.Photon + "GoogleMaps" -> Mobilizon.Service.Geospatial.GoogleMaps + "MapQuest" -> Mobilizon.Service.Geospatial.MapQuest + "Mimirsbrunn" -> Mobilizon.Service.Geospatial.Mimirsbrunn + "Pelias" -> Mobilizon.Service.Geospatial.Pelias + "Hat" -> Mobilizon.Service.Geospatial.Hat + end + +config :mobilizon, Mobilizon.Service.Geospatial, service: geospatial_service + +config :mobilizon, Mobilizon.Service.Geospatial.Nominatim, + endpoint: + System.get_env( + "MOBILIZON_GEOSPATIAL_NOMINATIM_ENDPOINT", + "https://nominatim.openstreetmap.org" + ), + api_key: System.get_env("MOBILIZON_GEOSPATIAL_NOMINATIM_API_KEY", nil) + +config :mobilizon, Mobilizon.Service.Geospatial.Addok, + endpoint: + System.get_env("MOBILIZON_GEOSPATIAL_ADDOK_ENDPOINT", "https://api-adresse.data.gouv.fr") + +config :mobilizon, Mobilizon.Service.Geospatial.Photon, + endpoint: System.get_env("MOBILIZON_GEOSPATIAL_PHOTON_ENDPOINT", "https://photon.komoot.de") + +config :mobilizon, Mobilizon.Service.Geospatial.GoogleMaps, + api_key: System.get_env("MOBILIZON_GEOSPATIAL_GOOGLE_MAPS_API_KEY", nil), + fetch_place_details: true + +config :mobilizon, Mobilizon.Service.Geospatial.MapQuest, + api_key: System.get_env("MOBILIZON_GEOSPATIAL_MAP_QUEST_API_KEY", nil) + +config :mobilizon, Mobilizon.Service.Geospatial.Mimirsbrunn, + endpoint: System.get_env("MOBILIZON_GEOSPATIAL_MIMIRSBRUNN_ENDPOINT", nil) + +config :mobilizon, Mobilizon.Service.Geospatial.Pelias, + endpoint: System.get_env("MOBILIZON_GEOSPATIAL_PELIAS_ENDPOINT", nil) + +sentry_dsn = System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_DSN", nil) + +included_environments = if sentry_dsn, do: ["prod"], else: [] + +config :sentry, + dsn: sentry_dsn, + included_environments: included_environments, + release: to_string(Application.spec(:mobilizon, :vsn)) + +config :logger, Sentry.LoggerBackend, + capture_log_messages: true, + level: :error + +if sentry_dsn != nil do + config :mobilizon, Mobilizon.Service.ErrorReporting, + adapter: Mobilizon.Service.ErrorReporting.Sentry +end + +matomo_enabled = System.get_env("MOBILIZON_FRONT_END_ANALYTICS_MATOMO_ENABLED", "false") == "true" +matomo_endpoint = System.get_env("MOBILIZON_FRONT_END_ANALYTICS_MATOMO_ENDPOINT", nil) +matomo_site_id = System.get_env("MOBILIZON_FRONT_END_ANALYTICS_MATOMO_SITE_ID", nil) + +matomo_tracker_file_name = + System.get_env("MOBILIZON_FRONT_END_ANALYTICS_MATOMO_TRACKER_FILE_NAME", "matomo") + +matomo_host = host_from_uri(matomo_endpoint) + +analytics_providers = + if matomo_enabled do + [Mobilizon.Service.FrontEndAnalytics.Matomo] + else + [] + end + +analytics_providers = + if sentry_dsn != nil do + analytics_providers ++ [Mobilizon.Service.FrontEndAnalytics.Sentry] + else + analytics_providers + end + +config :mobilizon, :analytics, providers: analytics_providers + +matomo_csp = + if matomo_enabled and matomo_host do + [ + connect_src: [matomo_host], + script_src: [matomo_host], + img_src: [matomo_host] + ] + else + [] + end + +config :mobilizon, Mobilizon.Service.FrontEndAnalytics.Matomo, + enabled: matomo_enabled, + host: matomo_endpoint, + siteId: matomo_site_id, + trackerFileName: matomo_tracker_file_name, + csp: matomo_csp + +config :mobilizon, Mobilizon.Service.FrontEndAnalytics.Sentry, + enabled: sentry_dsn != nil, + dsn: sentry_dsn, + tracesSampleRate: 1.0, + organization: System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_ORGANISATION", nil), + project: System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_PROJECT", nil), + host: System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_HOST", nil), + csp: [ + connect_src: + System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_HOST", "") |> String.split(" ", trim: true) + ] + +{% if applications | is_feature_enabled('oidc',application_id) %} +config :ueberauth, + Ueberauth, + providers: [ + keycloak: {Ueberauth.Strategy.Keycloak, [default_scope: "openid profile email"]} + ] + +config :mobilizon, :auth, + oauth_consumer_strategies: [ + {:keycloak, "{{ oidc.button_text }}"} + ] + +config :ueberauth, Ueberauth.Strategy.Keycloak.OAuth, + client_id: "{{ oidc.client.id }}", + client_secret: "{{ oidc.client.secret }}", + site: "{{ oidc.url }}", + authorize_url: "{{ oidc.client.authorize_url }}", + token_url: "{{ oidc.client.token_url }}", + userinfo_url: "{{ oidc.client.user_info_url }}", + token_method: :post +{% endif %} \ No newline at end of file diff --git a/roles/docker-mobilizon/templates/docker-compose.yml.j2 b/roles/docker-mobilizon/templates/docker-compose.yml.j2 index 6f462e0b..c1ba054e 100644 --- a/roles/docker-mobilizon/templates/docker-compose.yml.j2 +++ b/roles/docker-mobilizon/templates/docker-compose.yml.j2 @@ -1,21 +1,19 @@ -version: "3" - services: {% include 'roles/docker-central-database/templates/services/' + database_type + '.yml.j2' %} - mobilizon: + application: image: "{{ applications[application_id].images[application_id] }}" volumes: - uploads:/var/lib/mobilizon/uploads - # - ./config.exs:/etc/mobilizon/config.exs:ro + - {{ mobilizon_host_conf_exs_file }}:/etc/mobilizon/config.exs:ro ports: - "127.0.0.1:{{ ports.localhost.http[application_id] }}:{{ mobilizon_exposed_docker_port }}" - healthcheck: - test: ["CMD", "curl", "-f", "http://127.0.0.1:{{ mobilizon_exposed_docker_port }}"] - interval: 30s - timeout: 10s - retries: 3 + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:{{ mobilizon_exposed_docker_port }}"] + interval: 30s + timeout: 10s + retries: 3 {% include 'roles/docker-compose/templates/services/base.yml.j2' %} {% include 'templates/docker/container/depends-on-just-database.yml.j2' %} {% include 'templates/docker/container/networks.yml.j2' %} diff --git a/roles/docker-mobilizon/templates/env.j2 b/roles/docker-mobilizon/templates/env.j2 index 30a57c35..13347e8a 100644 --- a/roles/docker-mobilizon/templates/env.j2 +++ b/roles/docker-mobilizon/templates/env.j2 @@ -67,12 +67,12 @@ MOBILIZON_DATABASE_PORT={{ database_port }} # A secret key used as a base to generate secrets for encrypting and signing data. # Make sure it's long enough (~64 characters should be fine) # You can run `openssl rand -base64 48` to generate such a secret -MOBILIZON_INSTANCE_SECRET_KEY_BASE={{ applications[application_id].secret_key_base }} +MOBILIZON_INSTANCE_SECRET_KEY_BASE={{ applications[application_id].credentials.secret_key_base }} # A secret key used as a base to generate JWT tokens # Make sure it's long enough (~64 characters should be fine) # You can run `openssl rand -base64 48` to generate such a secret -MOBILIZON_INSTANCE_SECRET_KEY={{ applications[application_id].secret_key }} +MOBILIZON_INSTANCE_SECRET_KEY={{ applications[application_id].credentials.secret_key }} ###################################################### @@ -95,35 +95,4 @@ MOBILIZON_SMTP_SSL=false # Allowed values: always (TLS), never (Clear) and if_available (STARTTLS) # Make sure to match the port value as well # Defaults to "if_available" -MOBILIZON_SMTP_TLS={% if system_email.tls %}TLS{% elif system_email.start_tls %}STARTTLS{% else %}Clear{% endif %} - -{% if applications | is_feature_enabled('oidc',application_id) %} -#################################### -# ▶️ Mobilizon OIDC Configuration -#################################### - -AUTHENTICATION_STRATEGIES=open_id_connect - -# Display name of the OIDC login button -UEBERAUTH_OPENID_CONNECT_DISPLAY_NAME="{{ oidc.button_text }}" - -# Use discovery to automatically fetch OIDC provider settings -UEBERAUTH_OPENID_CONNECT_DISCOVERY_DOCUMENT={{ oidc.client.discovery_document }} - -# OIDC OAuth2 client credentials -UEBERAUTH_OPENID_CONNECT_CLIENT_ID={{ oidc.client.id }} -UEBERAUTH_OPENID_CONNECT_CLIENT_SECRET={{ oidc.client.secret }} - -# Redirect URI for the OIDC callback -UEBERAUTH_OPENID_CONNECT_REDIRECT_URI={{ mobilizon_oidc_callback_url }} - -# Scope and response type for OIDC -UEBERAUTH_OPENID_CONNECT_SCOPE=openid email profile -UEBERAUTH_OPENID_CONNECT_RESPONSE_TYPE=code - -# Claim/field used to uniquely identify the user -UEBERAUTH_OPENID_CONNECT_UID_FIELD={{ oidc.attributes.username }} - -# Optional email verification behavior -UEBERAUTH_OPENID_CONNECT_ASSUME_EMAIL_IS_VERIFIED=true -{% endif %} +MOBILIZON_SMTP_TLS={% if system_email.tls %}TLS{% elif system_email.start_tls %}STARTTLS{% else %}Clear{% endif %} \ No newline at end of file diff --git a/roles/docker-mobilizon/vars/configuration.yml b/roles/docker-mobilizon/vars/configuration.yml index 31622d3a..a578cecd 100644 --- a/roles/docker-mobilizon/vars/configuration.yml +++ b/roles/docker-mobilizon/vars/configuration.yml @@ -3,4 +3,15 @@ images: mobilizon: "docker.io/framasoft/mobilizon" features: central_database: true - oidc: true \ No newline at end of file + oidc: true +csp: + flags: + script-src-elem: + unsafe-inline: true + script-src: + unsafe-eval: true +domains: + canonical: + - "event.{{ primary_domain }}" + aliases: + - "events.{{ primary_domain }}" \ No newline at end of file diff --git a/roles/docker-mobilizon/vars/main.yml b/roles/docker-mobilizon/vars/main.yml index 3de0fda9..366c722e 100644 --- a/roles/docker-mobilizon/vars/main.yml +++ b/roles/docker-mobilizon/vars/main.yml @@ -1,4 +1,8 @@ application_id: mobilizon -database_type: "mariadb" + +database_type: "postgres" +database_gis_enabled: true + mobilizon_oidc_callback_url: "{{ web_protocol }}://{{ domains | get_domain(application_id) }}/auth/openid_connect/callback" -mobilizon_exposed_docker_port: 4000 \ No newline at end of file +mobilizon_exposed_docker_port: 4000 +mobilizon_host_conf_exs_file: "{{docker_compose.directories.config}}config.exs" \ No newline at end of file diff --git a/roles/docker-postgres/tasks/main.yml b/roles/docker-postgres/tasks/main.yml index 4cde2b9f..66d1a003 100644 --- a/roles/docker-postgres/tasks/main.yml +++ b/roles/docker-postgres/tasks/main.yml @@ -9,7 +9,7 @@ - name: Install PostgreSQL docker_container: name: "{{ applications.postgres.hostname }}" - image: "postgres:{{applications.postgres.version}}" + image: "{{ applications | get_docker_image(application_id) }}" detach: yes env: POSTGRES_PASSWORD: "{{ applications.postgres.credentials.postgres_password }}" @@ -118,6 +118,21 @@ GRANT CREATE ON SCHEMA public TO {{ database_username }}; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO {{ database_username }}; +- name: Ensure PostGIS-related extensions are installed + community.postgresql.postgresql_ext: + db: "{{ database_name }}" + ext: "{{ item }}" + state: present + login_user: postgres + login_password: "{{ applications.postgres.credentials.postgres_password }}" + login_host: 127.0.0.1 + login_port: "{{ database_port }}" + loop: + - postgis + - pg_trgm + - unaccent + when: database_gis_enabled is defined and database_gis_enabled + - name: Run the docker_postgres tasks once set_fact: run_once_docker_postgres: true diff --git a/roles/docker-postgres/vars/configuration.yml b/roles/docker-postgres/vars/configuration.yml index 03192774..0685ef67 100644 --- a/roles/docker-postgres/vars/configuration.yml +++ b/roles/docker-postgres/vars/configuration.yml @@ -1,3 +1,10 @@ -# Please set an version in your inventory file - Rolling release for postgres isn't recommended -version: "latest" -hostname: "central-postgres" \ No newline at end of file +hostname: "central-postgres" +docker: + images: + # Postgis is necessary for mobilizon + postgres: postgis/postgis + versions: + # Please set an version in your inventory file! + # Rolling release isn't recommended + postgres: "latest" + diff --git a/tests/integration/test_deprecated_version_key.py b/tests/integration/test_deprecated_version_key.py index a98a7a8c..229e58d9 100644 --- a/tests/integration/test_deprecated_version_key.py +++ b/tests/integration/test_deprecated_version_key.py @@ -30,11 +30,15 @@ class TestDeprecatedVersionKey(unittest.TestCase): uses_version = 'version' in config uses_images = 'images' in config - if uses_version and not uses_images: + if uses_version: warnings.append( f"[DEPRECATION WARNING] {role_path.name}/vars/configuration.yml: " - f"'version:' is set, but 'images:' is missing. " - f"'version' is deprecated and must only be set if 'images' is present." + f"'version' is deprecated. Replace it by docker.versions[version]." + ) + if uses_images: + warnings.append( + f"[DEPRECATION WARNING] {role_path.name}/vars/configuration.yml: " + f"'images' is deprecated. Replace it by docker.images[image]." ) if warnings: diff --git a/tests/integration/test_docker_images_configuration.py b/tests/integration/test_docker_images_configuration.py index ec50aa40..e9bff6b1 100644 --- a/tests/integration/test_docker_images_configuration.py +++ b/tests/integration/test_docker_images_configuration.py @@ -33,55 +33,15 @@ class TestDockerRoleImagesConfiguration(unittest.TestCase): errors.append(f"{role_path.name}: YAML parse error: {e}") continue - images = config.get("images") + images = config.get("docker",{}).get("images") if not images: - warnings.append(f"[WARNING] {role_path.name}: No 'images' key in configuration.yml") + warnings.append(f"[WARNING] {role_path.name}: No 'docker.images' key in configuration.yml") continue if not isinstance(images, dict): errors.append(f"{role_path.name}: 'images' must be a dict in configuration.yml") continue - for key, value in images.items(): - if not key or not value or not isinstance(key, str) or not isinstance(value, str): - errors.append(f"{role_path.name}: images['{key}'] is invalid (must be non-empty string key and value)") - continue - - # Improved regex: matches both ' and " and allows whitespace - pattern = ( - r'image:\s*["\']\{\{\s*applications\[application_id\]\.images\.' + re.escape(key) + r'\s*\}\}["\']' - ) - - # innerhalb Deines Loops - pattern2 = ( - r'image:\s*["\']\{\{\s*' # image: "{{ - r'applications\[\s*application_id\s*\]\.images' # applications[ application_id ].images - r'\[\s*application_id\s*\]\s*' # [ application_id ] - r'\}\}["\']' # }}" oder }}" - ) - - - for tmpl_file in [ - role_path / "templates" / "docker-compose.yml.j2", - role_path / "templates" / "env.j2", - ]: - if not tmpl_file.exists(): - continue - content = tmpl_file.read_text("utf-8") - if re.search(pattern, content): - break - if key == main.get('application_id') and re.search(pattern2, content): - break - else: - # Dieser Block wird nur ausgeführt, wenn kein `break` ausgelöst wurde - errors.append( - f"{role_path.name}: image key '{key}' is not referenced as " - f"image: \"{{{{ applications[application_id].images.{key} }}}}\" or " - f"\"{{{{ applications[application_id].images[application_id] }}}}\" " - "in docker-compose.yml.j2 or env.j2" - ) - - # OPTIONAL: Check if the image is available locally via docker images # from shutil import which # import subprocess diff --git a/tests/unit/test_docker_image.py b/tests/unit/test_docker_image.py index 4e1cb84d..44e34c20 100644 --- a/tests/unit/test_docker_image.py +++ b/tests/unit/test_docker_image.py @@ -18,27 +18,14 @@ class TestGetDockerImage(unittest.TestCase): "akaunting": { "version": "1.0.0", "docker": { - "images": { "akaunting": "docker.io/akaunting/akaunting" }, - "versions": { "akaunting": "2.0.0" } + "images": {"akaunting": "docker.io/akaunting/akaunting"}, + "versions": {"akaunting": "2.0.0"} } } } result = self.get_docker_image(applications, "akaunting", "akaunting") self.assertEqual(result, "docker.io/akaunting/akaunting:2.0.0") - def test_fallback_to_application_version(self): - applications = { - "akaunting": { - "version": "1.2.3", - "docker": { - "images": { "akaunting": "ghcr.io/akaunting/akaunting" }, - "versions": {} - } - } - } - result = self.get_docker_image(applications, "akaunting", "akaunting") - self.assertEqual(result, "ghcr.io/akaunting/akaunting:1.2.3") - def test_missing_image_raises_error(self): applications = { "akaunting": { @@ -56,7 +43,7 @@ class TestGetDockerImage(unittest.TestCase): applications = { "akaunting": { "docker": { - "images": { "akaunting": "some/image" }, + "images": {"akaunting": "some/image"}, "versions": {} } } @@ -64,5 +51,45 @@ class TestGetDockerImage(unittest.TestCase): with self.assertRaises(ValueError): self.get_docker_image(applications, "akaunting", "akaunting") + # --- new: Default image_key uses application_id if none provided --- + def test_default_image_key_uses_application_id(self): + applications = { + "myapp": { + "version": "3.0.0", + "docker": { + "images": {"myapp": "registry/myapp"}, + "versions": {"myapp": "4.5.6"} + } + } + } + # No image_key argument → falls back to application_id + result = self.get_docker_image(applications, "myapp") + self.assertEqual(result, "registry/myapp:4.5.6") + + # --- new: Alternate image_key lookup --- + def test_alternate_image_key(self): + applications = { + "service": { + "version": "9.9.9", + "docker": { + "images": { + "service": "registry/service", + "db": "registry/service-db" + }, + "versions": { + "db": "2.2.2" + } + } + } + } + result = self.get_docker_image(applications, "service", "db") + self.assertEqual(result, "registry/service-db:2.2.2") + + # --- new: Missing application raises error --- + def test_missing_application_raises_error(self): + applications = {} + with self.assertRaises(ValueError): + self.get_docker_image(applications, "does_not_exist") + if __name__ == "__main__": unittest.main()