mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
feat(lint): derive container paths from meta/volumes.yml everywhere
meta/volumes.yml says where a volume lands inside its container. Wherever a
second file spelled the same path out, the two drifted independently: the mount
moves, the consumer keeps pointing at the old place, and nothing fails until the
process looks for content that is not there. e031b7f61 fixed exactly this for
web-app-moodle; this generalises it and makes the rule hold.
The lookup gains what a caller needs. `targets` maps each mounting service to
its container path, and `target` is the one path an entry mounts at -- present
only when every mount agrees. An entry that lands at different paths per service
omits `target` rather than picking one, and the caller names the service:
"{{ lookup('volume', application_id, 'prometheus_config').target }}"
"{{ lookup('volume', 'web-app-prometheus', 'host_sys').targets['node-exporter'] }}"
66 literals across 48 role files now derive. Replacement only happened where the
path stood alone -- /srv/app inside /srv/app/data is a different path -- and
every rewritten file was re-parsed, Jinja and YAML alike. Three sites the tool
refused rather than guess were done by hand: zammad's path sat in a Jinja list
literal, seaweedfs' inside a quoted string feeding a filter, and openresty's
/var/run/docker.sock is not a mount target at all but the socket on the host,
which only happens to spell the same string. That one carries the single
nocheck.
tests/lint/repository/spot/test_volumes_path_single_spot.py keeps it that way.
roles/*/files/ is out of scope by construction: it is copied verbatim into an
image and executed there, so a Dockerfile or entrypoint has no Ansible to
evaluate a lookup with.
One defect this found, and it was in the tooling rather than the roles: deciding
ambiguity AFTER filtering short paths made web-app-prometheus' host_sys look
single-valued -- it mounts /sys for cadvisor and /host/sys for node-exporter,
and /sys is below the length filter. The template would have rendered an
undefined `.target`. Both the converter and the rule now decide ambiguity over
every target and filter afterwards, and that call reads targets['node-exporter'].
Verified by resolving rather than by reading: all 70 accesses in the diff were
run against the real registry and every one yields a non-empty path equal to the
literal it replaced. make test-lint: 328 passed.
Not verified: no Ansible render, so a call site whose surrounding context cannot
build the applications registry would still fail at deploy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
750476b26a
commit
29015311b6
51 files changed
+250
-87
No files matched your search
+48
-17
@@ -16,6 +16,19 @@ Two call forms:
|
||||
- ``source`` for binds / configs / secrets, ``''`` if absent
|
||||
- ``nfs`` NFS opt-in (bool or dict), ``None`` if absent
|
||||
- ``mounts`` the raw mounts list, ``[]`` if absent
|
||||
- ``targets`` ``{service: container path}`` built from ``mounts``
|
||||
- ``target`` the one container path this entry mounts at, present
|
||||
only when every mount agrees. An entry that lands at
|
||||
different paths per service omits the key rather than
|
||||
picking one; read ``targets[service]`` there.
|
||||
|
||||
``target`` is what lets ``meta/volumes.yml`` stay the single source of a
|
||||
container path: a role defines its constant from the registry
|
||||
|
||||
PROMETHEUS_CONFIG_FILE: "{{ lookup('volume', application_id, 'config').target }}"
|
||||
|
||||
rather than repeating the literal, which nothing keeps in step with the
|
||||
mount it describes.
|
||||
|
||||
``lookup('volume', application_id)`` — return the WHOLE canonical
|
||||
registry as a dict keyed by semantic name. Useful for tasks that loop
|
||||
@@ -52,11 +65,6 @@ class LookupModule(LookupBase):
|
||||
|
||||
canonical = get_canonical_volumes(application_id)
|
||||
if not canonical:
|
||||
# The canonical-volumes registry fills lazily as a side effect of
|
||||
# building the application defaults; force it on a miss so a lookup
|
||||
# that runs before any consumer built this role (e.g. nfs_prep's
|
||||
# pre-create loop) does not see an empty registry and silently skip
|
||||
# the subdir. Guarded so the populated hot path skips the deepcopy.
|
||||
get_application_defaults()
|
||||
canonical = get_canonical_volumes(application_id)
|
||||
|
||||
@@ -81,15 +89,38 @@ class LookupModule(LookupBase):
|
||||
)
|
||||
|
||||
docker_name = entry.get("name", "")
|
||||
return [
|
||||
{
|
||||
"name": entry.get("name") or name,
|
||||
"semantic_name": name,
|
||||
"docker_name": docker_name,
|
||||
"path": entry.get("path", ""),
|
||||
"type": entry.get("type", "volume"),
|
||||
"source": entry.get("source", ""),
|
||||
"nfs": entry.get("nfs", None),
|
||||
"mounts": entry.get("mounts", []),
|
||||
}
|
||||
]
|
||||
mounts = entry.get("mounts", [])
|
||||
resolved = {
|
||||
"name": entry.get("name") or name,
|
||||
"semantic_name": name,
|
||||
"docker_name": docker_name,
|
||||
"path": entry.get("path", ""),
|
||||
"type": entry.get("type", "volume"),
|
||||
"source": entry.get("source", ""),
|
||||
"nfs": entry.get("nfs", None),
|
||||
"mounts": mounts,
|
||||
"targets": _targets(mounts),
|
||||
}
|
||||
distinct = {t for t in resolved["targets"].values() if t}
|
||||
if len(distinct) == 1:
|
||||
resolved["target"] = next(iter(distinct))
|
||||
return [resolved]
|
||||
|
||||
|
||||
def _targets(mounts: Any) -> dict[str, str]:
|
||||
"""Map each mounting service to the container path it receives.
|
||||
|
||||
Args:
|
||||
mounts: the entry's raw ``mounts`` list.
|
||||
"""
|
||||
if not isinstance(mounts, list):
|
||||
return {}
|
||||
out: dict[str, str] = {}
|
||||
for mount in mounts:
|
||||
if not isinstance(mount, dict):
|
||||
continue
|
||||
service = str(mount.get("service", "") or "").strip()
|
||||
target = str(mount.get("target", "") or "").strip()
|
||||
if service and target:
|
||||
out[service] = target
|
||||
return out
|
||||
@@ -66,7 +66,7 @@
|
||||
- --network
|
||||
- none
|
||||
- -v
|
||||
- "{{ MARIADB_VOLUME }}:/var/lib/mysql"
|
||||
- "{{ MARIADB_VOLUME }}:{{ lookup('volume', 'svc-db-mariadb', 'data').target }}"
|
||||
- "{{ MARIADB_RECOVERY_IMAGE }}"
|
||||
- mariadbd
|
||||
- --skip-grant-tables
|
||||
|
||||
@@ -11,7 +11,7 @@ server:
|
||||
access-control: 10.0.0.0/8 allow
|
||||
access-control: 172.16.0.0/12 allow
|
||||
access-control: 192.168.0.0/16 allow
|
||||
directory: "/etc/unbound"
|
||||
directory: "{{ lookup('volume', 'svc-dns-unbound', 'data').target }}"
|
||||
username: unbound
|
||||
auto-trust-anchor-file: trusted-key.key
|
||||
root-hints: "/etc/unbound/root.hints"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{% endif %}
|
||||
{{ lookup('container_volumes', application_id, 'tor') | indent(4) }}
|
||||
healthcheck:
|
||||
test: ["CMD", "tor", "--verify-config", "-f", "/etc/tor/torrc"]
|
||||
test: ["CMD", "tor", "--verify-config", "-f", "{{ lookup('volume', 'svc-net-tor', 'tor_torrc').target }}"]
|
||||
interval: 60s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Managed by Infinito.Nexus (roles/svc-net-tor) — do not edit by hand.
|
||||
RunAsDaemon 0
|
||||
Log notice stdout
|
||||
DataDirectory /var/lib/tor
|
||||
DataDirectory {{ lookup('volume', 'svc-net-tor', 'tor_data').target }}
|
||||
|
||||
SocksPort {{ lookup('tor_socks', '0.0.0.0') }}
|
||||
SocksPolicy accept 127.0.0.0/8
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
block:
|
||||
- name: "🔎 Stat the docker socket so the swarm openresty worker can be group-added to it"
|
||||
ansible.builtin.stat:
|
||||
# nocheck: volumes-path-single-spot this stats the socket on the HOST; it only happens to spell the same string as the container mount target
|
||||
path: /var/run/docker.sock
|
||||
register: _openresty_docker_sock
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
--constraint node.role==manager \
|
||||
--env-file {{ lookup('container', application_id, 'files.env') | quote }} \
|
||||
--env AKAUNTING_SETUP=true \
|
||||
--mount type=volume,source={{ AKAUNTING_VOLUME | quote }},target=/var/www/html \
|
||||
--mount type=volume,source={{ AKAUNTING_VOLUME | quote }},target={{ lookup('volume', 'web-app-akaunting', 'data').target }} \
|
||||
--network {{ application_id | get_entity_name | quote }} \
|
||||
{% if lookup('database', application_id, 'shared') | bool %}--network {{ lookup('database', application_id, 'network') | quote }} {% endif %}\
|
||||
{{ AKAUNTING_IMAGE }}:{{ AKAUNTING_VERSION }} \
|
||||
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
{{ service_name }}:
|
||||
{{ lookup('container_image', application_id, service_name) }}
|
||||
{{ lookup('container_volumes', application_id, service_name) | indent(4) }}
|
||||
entrypoint: ["sh", "/etherpad_pg_entrypoint.sh"]
|
||||
entrypoint: ["sh", "{{ lookup('volume', 'web-app-bigbluebutton', 'etherpad_pg_entrypoint').target }}"]
|
||||
{% if BBB_COLLABORA_ENABLED | bool %}
|
||||
{{ lookup('depends_on', ['redis', 'postgres']) }}
|
||||
{% else %}
|
||||
|
||||
@@ -9,7 +9,7 @@ BLUESKY_GEOLOCATION_PATH: "{{ [ BLUESKY_SOCIAL_APP_DIR,'src/geolocation/index.ts
|
||||
BLUESKY_PDSADMIN_DIR: "{{ [ lookup('container', application_id, 'directories.volumes'), 'pdsadmin' ] | path_join }}"
|
||||
BLUESKY_PDSADMIN_FILE: "{{ [ BLUESKY_PDSADMIN_DIR, 'pdsadmin' ] | path_join }}"
|
||||
BLUESKY_PDSADMIN_TMP_TAR: "/tmp/pdsadmin.tar.gz"
|
||||
BLUESKY_PDS_DATA_DIR: "/opt/pds"
|
||||
BLUESKY_PDS_DATA_DIR: "{{ lookup('volume', 'web-app-bluesky', 'pds_data').target }}"
|
||||
BLUESKY_PDS_BLOBSTORE_LOCATION: "{{ [ BLUESKY_PDS_DATA_DIR, 'blocks' ] | path_join }}"
|
||||
|
||||
BLUESKY_WEB_ENABLED: "{{ lookup('config', application_id, 'services.web.enabled') }}"
|
||||
|
||||
@@ -5,8 +5,8 @@ DOMAIN={{ BOOKWYRM_HOSTNAME }}
|
||||
ALLOWED_HOSTS={{ BOOKWYRM_HOSTNAME }},127.0.0.1,localhost
|
||||
PORT={{ lookup('tls', application_id, 'ports.web') }}
|
||||
TLS_ENABLED={{ lookup('tls', application_id, 'protocols.web') }}
|
||||
MEDIA_ROOT=/app/media
|
||||
DATA_ROOT=/app/data
|
||||
MEDIA_ROOT={{ lookup('volume', 'web-app-bookwyrm', 'media').target }}
|
||||
DATA_ROOT={{ lookup('volume', 'web-app-bookwyrm', 'data').target }}
|
||||
REGISTRATION_OPEN={{ BOOKWYRM_REGISTRATION_OPEN }}
|
||||
ALLOW_INVITE_REQUESTS={{ BOOKWYRM_ALLOW_INVITE_REQUESTS }}
|
||||
|
||||
|
||||
@@ -16,5 +16,5 @@ CHESS_CONTAINER: "{{ lookup('config', application_id, 'services.' ~ entity_name
|
||||
|
||||
CHESS_HMAC_SECRET: "{{ lookup('config', application_id, 'secrets.credentials.hmac_secret') }}"
|
||||
CHESS_KEY_FILENAME: "signing-key"
|
||||
CHESS_APP_DATA_DIR: "/app/data"
|
||||
CHESS_APP_DATA_DIR: "{{ lookup('volume', 'web-app-chess', 'data').target }}"
|
||||
CHESS_APP_KEY_FILE: "{{ [ CHESS_APP_DATA_DIR, CHESS_KEY_FILENAME ] | path_join }}"
|
||||
@@ -6,7 +6,7 @@ container_hostname: "{{ lookup('container_hostname', application_id) }}"
|
||||
|
||||
CONFLUENCE_URL: "{{ lookup('tls', application_id, 'url.base') }}"
|
||||
CONFLUENCE_HOSTNAME: "{{ container_hostname }}"
|
||||
CONFLUENCE_HOME: "/var/atlassian/application-data/confluence"
|
||||
CONFLUENCE_HOME: "{{ lookup('volume', 'web-app-confluence', 'data').target }}"
|
||||
|
||||
CONFLUENCE_OIDC_ENABLED: "{{ lookup('config', application_id, 'services.sso.enabled') }}"
|
||||
CONFLUENCE_OIDC_LABEL: "{{ OIDC.BUTTON_TEXT }}"
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
container exec -i
|
||||
-e SITE_NAME -e API_BOT_LOGIN -e API_BOT_EMAIL -e API_BOT_FIRSTNAME -e API_BOT_LASTNAME -e API_BOT_PASSWORD -e API_KEY_SECRET
|
||||
{{ ERPNEXT_BACKEND_LOCAL_CID | quote }}
|
||||
bash -c 'cd /home/frappe/frappe-bench/sites && /home/frappe/frappe-bench/env/bin/python -'
|
||||
bash -c 'cd {{ lookup('volume', 'web-app-erpnext', 'sites').target }} && /home/frappe/frappe-bench/env/bin/python -'
|
||||
stdin: "{{ lookup('ansible.builtin.file', role_path ~ '/files/scripts/apply/api_bot.py') }}"
|
||||
delegate_to: "{{ ERPNEXT_BACKEND_HOST_NODE }}"
|
||||
environment:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-e CLIENT_ID -e CLIENT_SECRET -e ISSUER_URL
|
||||
-e GROUP_ROLE_MAP_JSON
|
||||
{{ ERPNEXT_BACKEND_LOCAL_CID | quote }}
|
||||
bash -c 'cd /home/frappe/frappe-bench/sites && /home/frappe/frappe-bench/env/bin/python -'
|
||||
bash -c 'cd {{ lookup('volume', 'web-app-erpnext', 'sites').target }} && /home/frappe/frappe-bench/env/bin/python -'
|
||||
stdin: "{{ lookup('ansible.builtin.file', role_path ~ '/files/scripts/apply/oidc_settings.py') }}"
|
||||
delegate_to: "{{ ERPNEXT_BACKEND_HOST_NODE }}"
|
||||
environment:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
-e SITE_NAME -e LDAP_HOST -e LDAP_PORT -e LDAP_BASE_DN
|
||||
-e LDAP_BIND_DN -e LDAP_BIND_PW -e LDAP_UID_ATTR
|
||||
{{ ERPNEXT_BACKEND_LOCAL_CID | quote }}
|
||||
bash -c 'cd /home/frappe/frappe-bench/sites && /home/frappe/frappe-bench/env/bin/python -'
|
||||
bash -c 'cd {{ lookup('volume', 'web-app-erpnext', 'sites').target }} && /home/frappe/frappe-bench/env/bin/python -'
|
||||
stdin: "{{ lookup('ansible.builtin.file', role_path ~ '/files/scripts/apply/ldap_source.py') }}"
|
||||
delegate_to: "{{ ERPNEXT_BACKEND_HOST_NODE }}"
|
||||
environment:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
-e SITE_NAME -e MAIL_FROM_NAME -e MAIL_FROM_ADDRESS
|
||||
-e SMTP_HOST -e SMTP_PORT -e SMTP_USER -e SMTP_PASS
|
||||
{{ ERPNEXT_BACKEND_LOCAL_CID | quote }}
|
||||
bash -c 'cd /home/frappe/frappe-bench/sites && /home/frappe/frappe-bench/env/bin/python -'
|
||||
bash -c 'cd {{ lookup('volume', 'web-app-erpnext', 'sites').target }} && /home/frappe/frappe-bench/env/bin/python -'
|
||||
stdin: "{{ lookup('ansible.builtin.file', role_path ~ '/files/scripts/apply/email_account.py') }}"
|
||||
delegate_to: "{{ ERPNEXT_BACKEND_HOST_NODE }}"
|
||||
environment:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{{ lookup('container_image', application_id, 'erpnext') }}
|
||||
{{ lookup('compose_only', 'container_name', ERPNEXT_BACKEND_CONTAINER) }}
|
||||
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
|
||||
entrypoint: ["bash", "-c", "{{ ERPNEXT_CONFIG_READY_GATE }} exec /usr/local/bin/entrypoint.sh /home/frappe/frappe-bench/env/bin/gunicorn --chdir=/home/frappe/frappe-bench/sites --bind=0.0.0.0:{{ ERPNEXT_BACKEND_PORT }} --threads=4 --workers=2 --worker-class=gthread --worker-tmp-dir=/dev/shm --timeout=120 --preload frappe.app:application"]
|
||||
entrypoint: ["bash", "-c", "{{ ERPNEXT_CONFIG_READY_GATE }} exec /usr/local/bin/entrypoint.sh /home/frappe/frappe-bench/env/bin/gunicorn --chdir={{ lookup('volume', 'web-app-erpnext', 'sites').target }} --bind=0.0.0.0:{{ ERPNEXT_BACKEND_PORT }} --threads=4 --workers=2 --worker-class=gthread --worker-tmp-dir=/dev/shm --timeout=120 --preload frappe.app:application"]
|
||||
{{ lookup('depends_on', {'configurator': 'service_completed_successfully'}) }}
|
||||
{{ lookup('container_healthcheck', service_name) | indent(4) }}
|
||||
{{ lookup('container_volumes', application_id, ERPNEXT_BACKEND_CONTAINER) | indent(4) }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% include 'roles/sys-svc-container/templates/env.j2' %}
|
||||
|
||||
LITELLM_MASTER_KEY={{ FLOWISE_LITELLM_MASTER_KEY }}
|
||||
LITELLM_CONFIG=/etc/litellm/config.yaml
|
||||
LITELLM_CONFIG={{ lookup('volume', 'web-app-flowise', 'litellm_config').target }}
|
||||
|
||||
PORT={{ FLOWISE_PORT_INTERNAL }}
|
||||
FLOWISE_USERNAME={{ lookup('users', 'administrator').username }}
|
||||
|
||||
@@ -13,7 +13,7 @@ FLOWISE_LITELLM_CONTAINER: "{{ lookup('config', application_id, 'services.litell
|
||||
FLOWISE_LITELLM_PORT: "{{ lookup('config', application_id, 'services.litellm.ports.internal.http') }}"
|
||||
FLOWISE_LITELLM_INTERNAL_URL: "http://litellm:{{ FLOWISE_LITELLM_PORT }}"
|
||||
FLOWISE_LITELLM_CONFIG_PATH_HOST: "{{ [ lookup('container', application_id, 'directories.config'), 'litellm.config.yaml' ] | path_join }}"
|
||||
FLOWISE_LITELLM_CONFIG_PATH_DOCKER: "/etc/litellm/config.yaml"
|
||||
FLOWISE_LITELLM_CONFIG_PATH_DOCKER: "{{ lookup('volume', 'web-app-flowise', 'litellm_config').target }}"
|
||||
FLOWISE_LITELLM_MASTER_KEY: "{{ lookup('config', application_id, 'secrets.credentials.llm_master_key') }}"
|
||||
|
||||
FLOWISE_QDRANT_INTERNAL_URL: "{{ lookup('engine', 'qdrant', application_id, 'url') }}"
|
||||
@@ -10,7 +10,7 @@ FRIENDICA_INIT_GATE_FILE: "friendica-init-gate.sh"
|
||||
FRIENDICA_INIT_GATE_HOST: "{{ [lookup('container', application_id, 'directories.instance'), FRIENDICA_INIT_GATE_FILE] | path_join }}"
|
||||
FRIENDICA_BASE_URL: "{{ lookup('tls', application_id, 'url.base') | regex_replace('/+$', '') }}"
|
||||
|
||||
FRIENDICA_APPLICATION_DIR: "/var/www/html"
|
||||
FRIENDICA_APPLICATION_DIR: "{{ lookup('volume', 'web-app-friendica', 'html').target }}"
|
||||
FRIENDICA_CONFIG_DIR: "{{ [ FRIENDICA_APPLICATION_DIR, 'config' ] | path_join }}"
|
||||
FRIENDICA_CONFIG_FILE: "{{ [ FRIENDICA_CONFIG_DIR, 'local.config.php' ] | path_join }}"
|
||||
FRIENDICA_USER: "www-data"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
# nocheck: seaweedfs-required - Jellyfin stores its library on a dedicated media volume and metadata in internal SQLite under /config; it has no S3/object-store surface.
|
||||
# nocheck: seaweedfs-required - Jellyfin stores its library on a dedicated media volume and metadata in internal SQLite under {{ lookup('volume', 'web-app-jellyfin', 'jellyfin_config').target }}; it has no S3/object-store surface.
|
||||
sso:
|
||||
bond: 1
|
||||
enabled: "{{ 'web-app-keycloak' in group_names }}"
|
||||
@@ -44,7 +44,7 @@ jellyfin:
|
||||
version: "10.11.11"
|
||||
backup:
|
||||
no_stop_required: true
|
||||
# nocheck: single-replica Jellyfin persists all state in embedded SQLite under the shared /config volume; concurrent replicas corrupt the database (SQLite disk I/O error, malformed image in CI) and upstream has no multi-instance support
|
||||
# nocheck: single-replica Jellyfin persists all state in embedded SQLite under the shared {{ lookup('volume', 'web-app-jellyfin', 'jellyfin_config').target }} volume; concurrent replicas corrupt the database (SQLite disk I/O error, malformed image in CI) and upstream has no multi-instance support
|
||||
replicas: 1
|
||||
name: jellyfin
|
||||
ports:
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
|
||||
environment:
|
||||
JAVA_OPTS: "-Djenkins.install.runSetupWizard=false"
|
||||
CASC_JENKINS_CONFIG: /var/jenkins_home/casc.yaml
|
||||
CASC_JENKINS_CONFIG: {{ lookup('volume', 'web-app-jenkins', 'casc').target }}
|
||||
{{ lookup('container_ports', ['jenkins', 'http', DOCKER_BIND_HOST]) | indent(4) }}
|
||||
{{ lookup('container_volumes', application_id, 'jenkins') | indent(4) }}
|
||||
{{ lookup('container_healthcheck', service_name) | indent(4) }}
|
||||
|
||||
@@ -6,7 +6,7 @@ container_hostname: "{{ lookup('container_hostname', application_id) }}"
|
||||
|
||||
JIRA_URL: "{{ lookup('tls', application_id, 'url.base') }}"
|
||||
JIRA_HOSTNAME: "{{ container_hostname }}"
|
||||
JIRA_STORAGE_PATH: "/var/atlassian/application-data/jira"
|
||||
JIRA_STORAGE_PATH: "{{ lookup('volume', 'web-app-jira', 'data').target }}"
|
||||
|
||||
JIRA_OIDC_ENABLED: "{{ lookup('config', application_id, 'services.sso.enabled') }}"
|
||||
JIRA_OIDC_LABEL: "{{ OIDC.BUTTON_TEXT }}"
|
||||
|
||||
@@ -48,7 +48,7 @@ KEYCLOAK_HEALTH_ENABLED: true
|
||||
|
||||
KEYCLOAK_REALM_IMPORT_ENABLED: "{{ lookup('config', application_id, 'services.keycloak.realm.import_enabled') }}"
|
||||
KEYCLOAK_REALM_IMPORT_DIR_HOST: "{{ [lookup('container', application_id, 'directories.volumes'),'import'] | path_join }}"
|
||||
KEYCLOAK_REALM_IMPORT_DIR_DOCKER: "/opt/keycloak/data/import/"
|
||||
KEYCLOAK_REALM_IMPORT_DIR_DOCKER: "{{ lookup('volume', 'web-app-keycloak', 'realm_import').target }}"
|
||||
KEYCLOAK_REALM_IMPORT_FILE_SRC: "import/realm.json.j2"
|
||||
KEYCLOAK_REALM_IMPORT_FILENAME: "realm.json"
|
||||
KEYCLOAK_REALM_IMPORT_FILE_DST: "{{ [KEYCLOAK_REALM_IMPORT_DIR_HOST,KEYCLOAK_REALM_IMPORT_FILENAME] | path_join }}"
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
container exec {{ MATRIX_MDAD_CONTAINER_ADDRESS | quote }} docker exec matrix-synapse register_new_matrix_user
|
||||
-u {{ MATRIX_ADMINISTRATOR_USERNAME | quote }}
|
||||
-p {{ MATRIX_ADMINISTRATOR_PASSWORD | quote }}
|
||||
-a -c /data/homeserver.yaml http://localhost:8008
|
||||
-a -c {{ lookup('volume', 'web-app-matrix', 'synapse_config').target }} http://localhost:8008
|
||||
register: matrix_admin_create
|
||||
changed_when: matrix_admin_create.rc == 0 and 'User ID already taken' not in (matrix_admin_create.stdout + matrix_admin_create.stderr)
|
||||
failed_when: matrix_admin_create.rc != 0 and 'User ID already taken' not in (matrix_admin_create.stdout + matrix_admin_create.stderr)
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
{{ lookup('container_networks') | indent(4) }}
|
||||
volumes:
|
||||
- {{ MATRIX_MDAD_DIR }}:/mdad
|
||||
- {{ lookup('volume', application_id, 'matrix_mdad_docker').name }}:/var/lib/docker
|
||||
- {{ lookup('volume', application_id, 'matrix_mdad_state').name }}:/var/lib/matrix-mdad
|
||||
- {{ lookup('volume', application_id, 'matrix_mdad_matrix').name }}:/matrix
|
||||
- {{ lookup('volume', application_id, 'matrix_mdad_docker').name }}:{{ lookup('volume', 'web-app-matrix', 'matrix_mdad_docker').target }}
|
||||
- {{ lookup('volume', application_id, 'matrix_mdad_state').name }}:{{ lookup('volume', 'web-app-matrix', 'matrix_mdad_state').target }}
|
||||
- {{ lookup('volume', application_id, 'matrix_mdad_matrix').name }}:{{ lookup('volume', 'web-app-matrix', 'matrix_mdad_matrix').target }}
|
||||
{% if (TLS_MODE | lower) == 'self_signed' %}
|
||||
- {{ CA_TRUST.cert_host }}:{{ CA_TRUST.cert_host }}:ro
|
||||
{% endif %}
|
||||
|
||||
@@ -32,7 +32,7 @@ MATRIX_SYNAPSE_DOMAIN: "{{ lookup('domains', application_id).synapse }}"
|
||||
MATRIX_SYNAPSE_PORT: "{{ lookup('config', 'web-app-matrix', 'services.synapse.ports.local.http') }}"
|
||||
MATRIX_SYNAPSE_CONTAINER_PORT: "{{ lookup('config', application_id, 'services.synapse.ports.internal.http') }}"
|
||||
MATRIX_SYNAPSE_CONFIG_PATH_HOST: "{{ [ lookup('container', application_id, 'directories.config'), 'homeserver.yaml' ] | path_join }}"
|
||||
MATRIX_SYNAPSE_CONFIG_PATH_CONTAINER: "/data/homeserver.yaml" # nocheck: container-var-name-lookup in-container path, not a container name
|
||||
MATRIX_SYNAPSE_CONFIG_PATH_CONTAINER: "{{ lookup('volume', 'web-app-matrix', 'synapse_config').target }}" # nocheck: container-var-name-lookup in-container path, not a container name
|
||||
MATRIX_SYNAPSE_LOG_PATH_HOST: "{{ [ lookup('container', application_id, 'directories.config'), MATRIX_SYNAPSE_DOMAIN ~'.log.config' ] | path_join }}"
|
||||
MATRIX_SYNAPSE_LOG_PATH_CONTAINER: "/data/{{ MATRIX_SYNAPSE_DOMAIN }}.log.config" # nocheck: container-var-name-lookup in-container path, not a container name
|
||||
MATRIX_SYNAPSE_BIND_MOUNT_MODE: "0644"
|
||||
|
||||
@@ -13,7 +13,7 @@ MEDIAWIKI_LOCALSETTINGS_HOST_PATH: "{{ MEDIAWIKI_CONFIG_DIR }}/LocalSettings.php
|
||||
MEDIAWIKI_LOCALSETTINGS_CONTAINER_PATH: "{{ MEDIAWIKI_HTML_DIR }}/LocalSettings.php"
|
||||
|
||||
MEDIAWIKI_LOCAL_MOUNT_DIR: "{{ [ MEDIAWIKI_VOLUMES_DIR, 'mw-local' ] | path_join }}"
|
||||
MEDIAWIKI_LOCAL_PATH: "/opt/mw-local"
|
||||
MEDIAWIKI_LOCAL_PATH: "{{ lookup('volume', 'web-app-mediawiki', 'mw_local').target }}"
|
||||
|
||||
MEDIAWIKI_VERSION: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.version') }}"
|
||||
MEDIAWIKI_IMAGE: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.image') }}"
|
||||
|
||||
@@ -103,7 +103,7 @@ config :geolix,
|
||||
]
|
||||
|
||||
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local,
|
||||
uploads: System.get_env("MOBILIZON_UPLOADS", "/var/lib/mobilizon/uploads")
|
||||
uploads: System.get_env("MOBILIZON_UPLOADS", "{{ lookup('volume', 'web-app-mobilizon', 'uploads').target }}")
|
||||
|
||||
formats =
|
||||
if System.get_env("MOBILIZON_EXPORTS_FORMAT_CSV_ENABLED", "true") == "true" do
|
||||
|
||||
@@ -3,8 +3,8 @@ application_id: "web-app-moodle"
|
||||
entity_name: "{{ application_id | get_entity_name }}"
|
||||
swarm_stack_converge_retries: 600
|
||||
|
||||
MOODLE_CODE_DIR: "/var/www/html"
|
||||
MOODLE_DATA_DIR: "/var/www/moodledata"
|
||||
MOODLE_CODE_DIR: "{{ lookup('volume', 'web-app-moodle', 'code').target }}"
|
||||
MOODLE_DATA_DIR: "{{ lookup('volume', 'web-app-moodle', 'data').target }}"
|
||||
MOODLE_SOURCE_DIR: "/opt/moodle-source"
|
||||
MOODLE_ENTRYPOINT_PATH: "/usr/local/bin/moodle-entrypoint.sh"
|
||||
|
||||
|
||||
@@ -16,4 +16,4 @@ N8N_INTERNAL_API_URL: "http://{{ DOCKER_REACH_HOST }}:{{ N8N_HTTP_PORT }}"
|
||||
N8N_SSO_ENABLED: "{{ lookup('config', application_id, 'services.sso.enabled') | bool }}"
|
||||
|
||||
N8N_HOOKS_FILE_HOST: "{{ [ lookup('container', application_id, 'directories.instance'), 'hooks.js' ] | path_join }}"
|
||||
N8N_HOOKS_FILE_CONTAINER: "/home/node/hooks.js" # nocheck: container-var-name-lookup
|
||||
N8N_HOOKS_FILE_CONTAINER: "{{ lookup('volume', 'web-app-n8n', 'hooks').target }}" # nocheck: container-var-name-lookup
|
||||
@@ -27,8 +27,8 @@ NEXTCLOUD_HOST_NGINX_PATH: "{{ [ lookup('container', application_id, 'directorie
|
||||
|
||||
NEXTCLOUD_CNODE_PLUGIN_TASKS_PATH: "{{ [role_path, 'tasks/addons/'] | path_join }}"
|
||||
|
||||
NEXTCLOUD_DOCKER_WORK_DIRECTORY: "/var/www/html/"
|
||||
NEXTCLOUD_DOCKER_CONF_ADD_PATH: "/var/www/infinito/"
|
||||
NEXTCLOUD_DOCKER_WORK_DIRECTORY: "{{ lookup('volume', 'web-app-nextcloud', 'data').target }}"
|
||||
NEXTCLOUD_DOCKER_CONF_ADD_PATH: "{{ lookup('volume', 'web-app-nextcloud', 'infinito_conf').target }}"
|
||||
NEXTCLOUD_DOCKER_CONF_LOADER_PATH: "{{ [ NEXTCLOUD_DOCKER_WORK_DIRECTORY, 'config/zzz-infinito.config.php'] | path_join }}"
|
||||
|
||||
NEXTCLOUD_ADMINISTRATOR_PASSWORD: "{{ lookup('config', application_id, 'secrets.credentials.administrator_password') }}"
|
||||
|
||||
@@ -14,8 +14,8 @@ admin_passwd = {{ ODOO_MASTER_PASSWORD }}
|
||||
list_db = False
|
||||
|
||||
; Paths
|
||||
data_dir = /var/lib/odoo
|
||||
addons_path = /mnt/extra-addons,/mnt/custom-addons,/usr/lib/python3/dist-packages/odoo/addons
|
||||
data_dir = {{ lookup('volume', 'web-app-odoo', 'data').target }}
|
||||
addons_path = {{ lookup('volume', 'web-app-odoo', 'addons').target }},/mnt/custom-addons,/usr/lib/python3/dist-packages/odoo/addons
|
||||
|
||||
; Logging
|
||||
log_level = info
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
network_mode: host
|
||||
command:
|
||||
- --config
|
||||
- /etc/livekit/livekit.yaml
|
||||
- {{ lookup('volume', 'web-app-opentalk', 'livekit_yaml').target }}
|
||||
{{ lookup('container_volumes', application_id, service_name) | indent(4) }}
|
||||
logging:
|
||||
driver: journald
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
{{ lookup('compose_only', 'container_name', PENPOT_BACKEND_CONTAINER) }}
|
||||
{% if (TLS_MODE | lower) == 'self_signed' %}
|
||||
command:
|
||||
- /usr/local/bin/java-with-ca-trust.sh
|
||||
- {{ lookup('volume', 'web-app-penpot', 'java_ca_trust').target }}
|
||||
- /bin/bash
|
||||
- run.sh
|
||||
{% endif %}
|
||||
|
||||
@@ -20,7 +20,7 @@ PENPOT_PUBLIC_URI: "{{ lookup('tls', application_id, 'url.base') }}"
|
||||
|
||||
PENPOT_SECRET_KEY: "{{ lookup('config', application_id, 'secrets.credentials.secret_key') }}"
|
||||
|
||||
PENPOT_ASSETS_FS_DIRECTORY: "/opt/data/assets"
|
||||
PENPOT_ASSETS_FS_DIRECTORY: "{{ lookup('volume', 'web-app-penpot', 'assets').target }}"
|
||||
|
||||
PENPOT_DATABASE_USERNAME: "{{ lookup('database', application_id, 'username') }}"
|
||||
PENPOT_DATABASE_NAME: "{{ lookup('database', application_id, 'name') }}"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"MaintenanceDB": "{{ server.maintenance_db }}",
|
||||
"Username": "{{ server.username }}",
|
||||
"SSLMode": "prefer",
|
||||
"PassFile": "/pgpass"
|
||||
"PassFile": "{{ lookup('volume', 'web-app-pgadmin', 'pgpass').target }}"
|
||||
}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
pgadmin_host_server_file: "{{ lookup('container', application_id, 'directories.volumes') }}servers.json"
|
||||
pgadmin_docker_server_file: "/pgadmin4/servers.json"
|
||||
pgadmin_docker_server_file: "{{ lookup('volume', 'web-app-pgadmin', 'servers_json').target }}"
|
||||
pgadmin_host_password_file: "{{ lookup('container', application_id, 'directories.volumes') }}.pgpass"
|
||||
pgadmin_docker_password_file: "/pgpass"
|
||||
pgadmin_docker_password_file: "{{ lookup('volume', 'web-app-pgadmin', 'pgpass').target }}"
|
||||
|
||||
pgadmin_servers:
|
||||
- name: "Central Postgres Database"
|
||||
|
||||
@@ -12,10 +12,10 @@ PIXELFED_INSTANCE_DIR: "{{ lookup('container', application_id, 'directories.inst
|
||||
PIXELFED_ENV_FILE: "{{ lookup('container', application_id, 'files.env') }}"
|
||||
PIXELFED_ENTRYPOINT_FILE: "docker-entrypoint.sh"
|
||||
PIXELFED_ENTRYPOINT_HOST_ABS: "{{ [PIXELFED_INSTANCE_DIR, PIXELFED_ENTRYPOINT_FILE] | path_join }}"
|
||||
PIXELFED_ENTRYPOINT_DOCKER: "/infinito-pixelfed-entrypoint.sh"
|
||||
PIXELFED_ENTRYPOINT_DOCKER: "{{ lookup('volume', 'web-app-pixelfed', 'entrypoint').target }}"
|
||||
PIXELFED_WORKER_ENTRYPOINT_FILE: "worker-entrypoint.sh"
|
||||
PIXELFED_WORKER_ENTRYPOINT_HOST_ABS: "{{ [PIXELFED_INSTANCE_DIR, PIXELFED_WORKER_ENTRYPOINT_FILE] | path_join }}"
|
||||
PIXELFED_WORKER_ENTRYPOINT_DOCKER: "/infinito-pixelfed-worker-entrypoint.sh"
|
||||
PIXELFED_WORKER_ENTRYPOINT_DOCKER: "{{ lookup('volume', 'web-app-pixelfed', 'worker_entrypoint').target }}"
|
||||
PIXELFED_DB_NAME: "{{ lookup('database', application_id, 'name') }}"
|
||||
PIXELFED_ADMIN_USER_KEY: "administrator"
|
||||
PIXELFED_ADMIN_USERNAME: "{{ lookup('users', PIXELFED_ADMIN_USER_KEY).username }}"
|
||||
|
||||
@@ -51,7 +51,7 @@ prometheus:
|
||||
shared: true
|
||||
provides:
|
||||
- prometheus
|
||||
image: prom/prometheus
|
||||
image: prom{{ lookup('volume', 'web-app-prometheus', 'data').target }}
|
||||
version: v3.13.2
|
||||
name: prometheus
|
||||
cpus: "0.5"
|
||||
@@ -72,7 +72,7 @@ prometheus:
|
||||
lifecycle: beta
|
||||
min_storage: 307MB
|
||||
alertmanager:
|
||||
image: prom/alertmanager
|
||||
image: prom{{ lookup('volume', 'web-app-prometheus', 'alertmanager_data').target }}
|
||||
version: v0.33.1
|
||||
name: prometheus-alertmanager
|
||||
cpus: "0.25"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
{{ lookup('container_extra_hosts', extra_hosts=['host.docker.internal:host-gateway']) | indent(4) }}
|
||||
command:
|
||||
- "--config.file={{ PROMETHEUS_CONFIG_DOCKER }}"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--storage.tsdb.path={{ lookup('volume', 'web-app-prometheus', 'data').target }}"
|
||||
- "--web.enable-lifecycle"
|
||||
{{ lookup('container_volumes', application_id, 'prometheus') | indent(4) }}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
{{ lookup('compose_only', 'container_name', ALERTMANAGER_CONTAINER) }}
|
||||
command:
|
||||
- "--config.file={{ ALERTMANAGER_CONFIG_DOCKER }}"
|
||||
- "--storage.path=/alertmanager"
|
||||
- "--storage.path={{ lookup('volume', 'web-app-prometheus', 'alertmanager_data').target }}"
|
||||
{{ lookup('container_volumes', application_id, 'alertmanager') | indent(4) }}
|
||||
|
||||
{% set service_name = 'cadvisor' %}
|
||||
@@ -60,9 +60,9 @@
|
||||
{% endif %}
|
||||
{{ lookup('container_volumes', application_id, 'node-exporter') | indent(4) }}
|
||||
command:
|
||||
- "--path.procfs=/host/proc"
|
||||
- "--path.rootfs=/rootfs"
|
||||
- "--path.sysfs=/host/sys"
|
||||
- "--path.procfs={{ lookup('volume', 'web-app-prometheus', 'host_proc').target }}"
|
||||
- "--path.rootfs={{ lookup('volume', 'web-app-prometheus', 'host_rootfs').target }}"
|
||||
- "--path.sysfs={{ lookup('volume', 'web-app-prometheus', 'host_sys').targets['node-exporter'] }}"
|
||||
- "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)"
|
||||
|
||||
{{ lookup('compose_volumes', application_id) }}
|
||||
|
||||
@@ -4,15 +4,15 @@ application_id: "web-app-prometheus"
|
||||
PROMETHEUS_CONTAINER: "{{ lookup('config', application_id, 'services.prometheus.name') }}"
|
||||
|
||||
PROMETHEUS_CONFIG_HOST: "{{ [ lookup('container', application_id, 'directories.config'), 'prometheus.yml' ] | path_join }}"
|
||||
PROMETHEUS_CONFIG_DOCKER: "/etc/prometheus/prometheus.yml"
|
||||
PROMETHEUS_CONFIG_DOCKER: "{{ lookup('volume', 'web-app-prometheus', 'prometheus_config').target }}"
|
||||
BLACKBOX_CONFIG_HOST: "{{ [ lookup('container', application_id, 'directories.config'), 'blackbox.yml' ] | path_join }}"
|
||||
BLACKBOX_CONFIG_DOCKER: "/etc/blackbox_exporter/config.yml"
|
||||
BLACKBOX_CONFIG_DOCKER: "{{ lookup('volume', 'web-app-prometheus', 'blackbox_config').target }}"
|
||||
|
||||
ALERTMANAGER_CONTAINER: "{{ lookup('config', application_id, 'services.alertmanager.name') }}"
|
||||
ALERTMANAGER_CONFIG_HOST: "{{ [ lookup('container', application_id, 'directories.config'), 'alertmanager.yml' ] | path_join }}"
|
||||
ALERTMANAGER_CONFIG_DOCKER: "/etc/alertmanager/alertmanager.yml"
|
||||
ALERTMANAGER_CONFIG_DOCKER: "{{ lookup('volume', 'web-app-prometheus', 'alertmanager_config').target }}"
|
||||
ALERT_RULES_CONFIG_HOST: "{{ [ lookup('container', application_id, 'directories.config'), 'alert_rules.yml' ] | path_join }}"
|
||||
ALERT_RULES_CONFIG_DOCKER: "/etc/prometheus/alert_rules.yml"
|
||||
ALERT_RULES_CONFIG_DOCKER: "{{ lookup('volume', 'web-app-prometheus', 'alert_rules').target }}"
|
||||
|
||||
ALERTMANAGER_TELEGRAM_BOT_TOKEN: "{{ lookup('config', application_id, 'secrets.credentials.alerting_telegram_bot_token', '') }}"
|
||||
ALERTMANAGER_TELEGRAM_CHAT_ID: "{{ lookup('config', application_id, 'services.prometheus.alerting.telegram.chat_id') }}"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
|
||||
{{ lookup('container_image', application_id, 'seaweedfs') }}
|
||||
{{ lookup('compose_only', 'container_name', SEAWEEDFS_CONTAINER) }}
|
||||
command: {{ '/etc/seaweedfs/s3.json' | seaweedfs_command(collections=SEAWEEDFS_CONSUMER_COUNT) | to_json }}
|
||||
command: {{ lookup('volume', 'web-app-seaweedfs', 's3_config').target | seaweedfs_command(collections=SEAWEEDFS_CONSUMER_COUNT) | to_json }}
|
||||
{{ lookup('container_ports', ['seaweedfs', 'http', DOCKER_BIND_HOST]) | indent(4) }}
|
||||
{{ lookup('container_volumes', application_id, service_name) | indent(4) }}
|
||||
{{ lookup('container_networks') | indent(4) }}
|
||||
|
||||
@@ -23,7 +23,7 @@ SHOPWARE_WEB_CONTAINER_ADDRESS: "{{ lookup('container_address', application_id,
|
||||
SHOPWARE_WORKER_CONTAINER: "{{ lookup('config', application_id, 'services.worker.name') }}"
|
||||
SHOPWARE_SCHED_CONTAINER: "{{ lookup('config', application_id, 'services.scheduler.name') }}"
|
||||
SHOPWARE_INIT_HOST: "{{ [ lookup('container', application_id, 'directories.volumes'), 'init.sh' ] | path_join }}"
|
||||
SHOPWARE_INIT_DOCKER: "/usr/local/bin/init.sh"
|
||||
SHOPWARE_INIT_DOCKER: "{{ lookup('volume', 'web-app-shopware', 'init_script').target }}"
|
||||
SHOPWARE_FRAMEWORK_HOST: "{{ [ lookup('container', application_id, 'directories.config'), 'framework.yaml' ] | path_join }}"
|
||||
SHOPWARE_FILESYSTEM_HOST: "{{ [ lookup('container', application_id, 'directories.config'), 'shopware.yaml' ] | path_join }}"
|
||||
SHOPWARE_OBJSTORE_ENABLED: "{{ lookup('objstore', application_id, 'enabled') }}"
|
||||
|
||||
@@ -10,7 +10,7 @@ SOCIALHOME_INSTANCE_NAME: "{{ defaults_service_provider.platform.titel }}"
|
||||
SOCIALHOME_ADMIN_USERNAME: "{{ lookup('users', 'administrator').username }}"
|
||||
SOCIALHOME_ADMIN_PASSWORD: "{{ lookup('config', application_id, 'secrets.credentials.administrator_password') }}"
|
||||
|
||||
SOCIALHOME_CONFIG_FILE: "/etc/socialhome/socialhome.toml"
|
||||
SOCIALHOME_CONFIG_FILE: "{{ lookup('volume', 'web-app-socialhome', 'config').target }}"
|
||||
SOCIALHOME_CONFIG_HOST_FILE: "{{ [lookup('container', application_id, 'directories.volumes'), 'socialhome.toml'] | path_join }}"
|
||||
|
||||
SOCIALHOME_MAX_STORAGE_BYTES: 4294967296
|
||||
|
||||
@@ -23,7 +23,7 @@ build:
|
||||
{{ TAIGA_BACKEND_AUTH_BUILD_PROPS | indent(4, true) }}
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
command:
|
||||
- "if ! grep -qF {{ TAIGA_CONFIG_APPEND_MARKER | quote }} /taiga-back/settings/config.py; then cat /taiga-back/settings/config.append.py >> /taiga-back/settings/config.py; fi; exec /taiga-back/docker/entrypoint.sh"
|
||||
- "if ! grep -qF {{ TAIGA_CONFIG_APPEND_MARKER | quote }} /taiga-back/settings/config.py; then cat {{ lookup('volume', 'web-app-taiga', 'config-append').target }} >> /taiga-back/settings/config.py; fi; exec /taiga-back/docker/entrypoint.sh"
|
||||
{% else %}
|
||||
{{ lookup('container_image', application_id, 'taiga', image='taigaio/taiga-back', version=TAIGA_VERSION) }}
|
||||
{% endif %}
|
||||
@@ -49,7 +49,7 @@ build:
|
||||
{{ TAIGA_BACKEND_AUTH_BUILD_PROPS | indent(4, true) }}
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
command:
|
||||
- "if ! grep -qF {{ TAIGA_CONFIG_APPEND_MARKER | quote }} /taiga-back/settings/config.py; then cat /taiga-back/settings/config.append.py >> /taiga-back/settings/config.py; fi; exec /taiga-back/docker/async_entrypoint.sh"
|
||||
- "if ! grep -qF {{ TAIGA_CONFIG_APPEND_MARKER | quote }} /taiga-back/settings/config.py; then cat {{ lookup('volume', 'web-app-taiga', 'config-append').target }} >> /taiga-back/settings/config.py; fi; exec /taiga-back/docker/async_entrypoint.sh"
|
||||
{% else %}
|
||||
{{ lookup('container_image', application_id, 'async', image='taigaio/taiga-back', version=TAIGA_VERSION) }}
|
||||
entrypoint: ["/taiga-back/docker/async_entrypoint.sh"]
|
||||
|
||||
@@ -5,7 +5,7 @@ entity_name: "{{ application_id | get_entity_name }}"
|
||||
WORDPRESS_URL: "{{ lookup('tls', application_id, 'url.base') }}"
|
||||
WORDPRESS_TITLE: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.title') }}"
|
||||
WORDPRESS_MAX_UPLOAD_SIZE: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.max_upload_size') }}"
|
||||
WORDPRESS_DOCKER_HTML_PATH: "/var/www/html"
|
||||
WORDPRESS_DOCKER_HTML_PATH: "{{ lookup('volume', 'web-app-wordpress', 'data').target }}"
|
||||
WORDPRESS_DOCKER_CONF_PATH: "{{ WORDPRESS_CONFIG_PATH }}"
|
||||
WORDPRESS_VERSION: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.version') }}"
|
||||
WORDPRESS_IMAGE: "{{ lookup('config', application_id, 'services.' ~ entity_name ~ '.image') }}"
|
||||
|
||||
@@ -10,7 +10,7 @@ XWIKI_HOST_PORT: "{{ lookup('config', application_id, 'services.xwiki.ports.loca
|
||||
XWIKI_HOSTNAME: "{{ container_hostname }}"
|
||||
|
||||
XWIKI_HOST_PROPERTIES_PATH: "{{ [lookup('container', application_id, 'directories.config'), 'xwiki.properties'] | path_join }}"
|
||||
XWIKI_DOCK_DATA_DIR: "/usr/local/xwiki"
|
||||
XWIKI_DOCK_DATA_DIR: "{{ lookup('volume', 'web-app-xwiki', 'data').target }}"
|
||||
|
||||
XWIKI_IMAGE: "{{ lookup('config', application_id, 'services.xwiki.image') }}"
|
||||
XWIKI_VERSION: "{{ lookup('config', application_id, 'services.xwiki.version') }}"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
|
||||
command: ["zammad-init"]
|
||||
{{ lookup('container_volumes', application_id, ZAMMAD_INIT_SERVICE, extra_volumes=[
|
||||
'zammad_storage:/opt/zammad/storage',
|
||||
'zammad_storage:' ~ lookup('volume', 'web-app-zammad', 'zammad_storage').target,
|
||||
ZAMMAD_TELEGRAM_HELPER_HOST_PATH ~ ':' ~ ZAMMAD_TELEGRAM_HELPER_CONTAINER_PATH ~ ':ro',
|
||||
]) | indent(4) }}
|
||||
{{ lookup('container_depends_on', application_id) | indent(4) }}
|
||||
@@ -22,7 +22,7 @@
|
||||
command: ["zammad-railsserver"]
|
||||
{{ lookup('depends_on', {ZAMMAD_INIT_SERVICE: 'service_completed_successfully'}, indent=4) }}
|
||||
{% set _rails_extra_volumes = [
|
||||
'zammad_storage:/opt/zammad/storage',
|
||||
'zammad_storage:' ~ lookup('volume', 'web-app-zammad', 'zammad_storage').target,
|
||||
ZAMMAD_OMNIAUTH_INIT_HOST_PATH ~ ':' ~ ZAMMAD_OMNIAUTH_INIT_CONTAINER_PATH ~ ':ro',
|
||||
ZAMMAD_TELEGRAM_HELPER_HOST_PATH ~ ':' ~ ZAMMAD_TELEGRAM_HELPER_CONTAINER_PATH ~ ':ro',
|
||||
] %}
|
||||
|
||||
@@ -36,16 +36,16 @@ ZAMMAD_HTTP_ONLY_ENABLED: "{{ not (lookup('tls', application_id, 'enabled') | bo
|
||||
ZAMMAD_VOLUMES_DIR: "{{ lookup('container', application_id, 'directories.volumes') }}"
|
||||
ZAMMAD_AUTO_WIZARD_HOST_PATH: "{{ ZAMMAD_VOLUMES_DIR }}auto_wizard.json"
|
||||
ZAMMAD_OMNIAUTH_INIT_HOST_PATH: "{{ ZAMMAD_VOLUMES_DIR }}zz_omniauth_allow_get.rb"
|
||||
ZAMMAD_OMNIAUTH_INIT_CONTAINER_PATH: "/opt/zammad/config/initializers/zz_omniauth_allow_get.rb"
|
||||
ZAMMAD_OMNIAUTH_INIT_CONTAINER_PATH: "{{ lookup('volume', 'web-app-zammad', 'zammad_omniauth_init').target }}"
|
||||
|
||||
ZAMMAD_TELEGRAM_HELPER_HOST_PATH: "{{ ZAMMAD_VOLUMES_DIR }}telegram_helper.rb"
|
||||
ZAMMAD_TELEGRAM_HELPER_CONTAINER_PATH: "/opt/zammad/lib/telegram_helper.rb"
|
||||
ZAMMAD_TELEGRAM_HELPER_CONTAINER_PATH: "{{ lookup('volume', 'web-app-zammad', 'zammad_telegram_helper').target }}"
|
||||
|
||||
ZAMMAD_OIDC_GROUP_ROLE_MAP_HOST_PATH: "{{ ZAMMAD_VOLUMES_DIR }}zz_oidc_group_role_mapping.rb"
|
||||
ZAMMAD_OIDC_GROUP_ROLE_MAP_CONTAINER_PATH: "/opt/zammad/config/initializers/zz_oidc_group_role_mapping.rb"
|
||||
ZAMMAD_OIDC_GROUP_ROLE_MAP_CONTAINER_PATH: "{{ lookup('volume', 'web-app-zammad', 'zammad_oidc_group_role_map').target }}"
|
||||
|
||||
ZAMMAD_OIDC_HTTP_DISCOVERY_HOST_PATH: "{{ ZAMMAD_VOLUMES_DIR }}zz_oidc_http_discovery.rb"
|
||||
ZAMMAD_OIDC_HTTP_DISCOVERY_CONTAINER_PATH: "/opt/zammad/config/initializers/zz_oidc_http_discovery.rb"
|
||||
ZAMMAD_OIDC_HTTP_DISCOVERY_CONTAINER_PATH: "{{ lookup('volume', 'web-app-zammad', 'zammad_oidc_http_discovery').target }}"
|
||||
ZAMMAD_OIDC_GROUP_ROLE_MAP:
|
||||
"/{{ RBAC.GROUP.NAME | trim('/') }}/{{ application_id }}/administrator": "Admin"
|
||||
"/{{ RBAC.GROUP.NAME | trim('/') }}/{{ application_id }}/agent": "Agent"
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"""Forbid repeating a container path that ``meta/volumes.yml`` already declares.
|
||||
|
||||
``meta/volumes.yml`` says where a volume lands inside its container. Once a
|
||||
second file spells the same path out, the two drift independently: the mount
|
||||
moves, the consumer keeps pointing at the old place, and nothing fails until
|
||||
the process looks for content that is not there.
|
||||
|
||||
The registry is readable, so a role derives instead of repeating:
|
||||
|
||||
PROMETHEUS_CONFIG_FILE: "{{ lookup('volume', application_id, 'prometheus_config').target }}"
|
||||
|
||||
``target`` exists when every mount of that entry agrees; an entry that lands at
|
||||
different paths per service exposes ``targets[service]`` instead.
|
||||
|
||||
Scope: role files Ansible renders -- ``vars/``, ``tasks/``, ``templates/`` and
|
||||
the rest of ``meta/``. Two areas are out by construction:
|
||||
|
||||
* ``roles/*/files/`` is copied verbatim into an image and executed there, with
|
||||
no Ansible in sight, so a Dockerfile or an entrypoint cannot evaluate a
|
||||
lookup. Those sites carry a per-line opt-out instead.
|
||||
* ``.md`` documentation names paths in prose.
|
||||
|
||||
A path is only flagged where it stands alone; ``/srv/app`` inside ``/srv/app/data``
|
||||
is a different path and is left untouched.
|
||||
|
||||
Per-line opt-out: ``# nocheck: volumes-path-single-spot`` on the offending line
|
||||
or the immediately preceding non-empty line, with the reason it cannot derive.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from utils.annotations.suppress import is_suppressed_at
|
||||
from utils.cache.files import iter_non_ignored_files, read_text
|
||||
from utils.cache.yaml import load_yaml_any
|
||||
from utils.roles.mapping import ROLE_FILE_META_VOLUMES
|
||||
|
||||
from . import PROJECT_ROOT
|
||||
|
||||
_RULE = "volumes-path-single-spot"
|
||||
_MIN_PATH_LEN = 7
|
||||
_SKIP_AREAS = ("files",)
|
||||
_BOUNDARY = re.compile(r"[\w./-]")
|
||||
|
||||
|
||||
def _unambiguous_targets(volumes_path: Path) -> dict[str, str]:
|
||||
"""Map container path -> semantic key, for entries with one target only.
|
||||
|
||||
Args:
|
||||
volumes_path: the role's ``meta/volumes.yml``.
|
||||
"""
|
||||
doc = load_yaml_any(str(volumes_path), default_if_missing={}) or {}
|
||||
if not isinstance(doc, dict):
|
||||
return {}
|
||||
out: dict[str, str] = {}
|
||||
for key, entry in doc.items():
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
targets = {
|
||||
mount.get("target")
|
||||
for mount in (entry.get("mounts") or [])
|
||||
if isinstance(mount, dict) and isinstance(mount.get("target"), str)
|
||||
}
|
||||
if len(targets) != 1:
|
||||
continue
|
||||
target = next(iter(targets))
|
||||
if target.startswith("/") and len(target) >= _MIN_PATH_LEN:
|
||||
out[target] = key
|
||||
return out
|
||||
|
||||
|
||||
def _hits(lines: list[str], targets: dict[str, str]) -> list[tuple[int, str, str]]:
|
||||
"""Lines that spell out a declared path as a standalone value."""
|
||||
found: list[tuple[int, str, str]] = []
|
||||
for idx, line in enumerate(lines):
|
||||
for target, key in targets.items():
|
||||
start = line.find(target)
|
||||
if start < 0:
|
||||
continue
|
||||
after = line[start + len(target) : start + len(target) + 1]
|
||||
if after and _BOUNDARY.match(after):
|
||||
continue
|
||||
if is_suppressed_at(lines, idx + 1, _RULE, mode="same-or-above"):
|
||||
continue
|
||||
found.append((idx + 1, target, key))
|
||||
break
|
||||
return found
|
||||
|
||||
|
||||
class TestVolumesPathSingleSpot(unittest.TestCase):
|
||||
def test_declared_paths_are_derived_not_repeated(self) -> None:
|
||||
findings: list[str] = []
|
||||
for volumes_path in sorted(PROJECT_ROOT.glob(f"roles/*/{ROLE_FILE_META_VOLUMES}")):
|
||||
role_dir = volumes_path.parent.parent
|
||||
targets = _unambiguous_targets(volumes_path)
|
||||
if not targets:
|
||||
continue
|
||||
for path_str in iter_non_ignored_files(exclude_tests=True):
|
||||
path = Path(path_str)
|
||||
if role_dir not in path.parents or path == volumes_path:
|
||||
continue
|
||||
rel_in_role = path.relative_to(role_dir)
|
||||
if rel_in_role.parts[0] in _SKIP_AREAS or path.suffix == ".md":
|
||||
continue
|
||||
try:
|
||||
lines = read_text(path_str).splitlines()
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
for line_no, target, key in _hits(lines, targets):
|
||||
rel = path.relative_to(PROJECT_ROOT).as_posix()
|
||||
findings.append(f"- {rel}:{line_no}: {target} (declared as {key!r})")
|
||||
|
||||
if not findings:
|
||||
return
|
||||
|
||||
self.fail(
|
||||
"A container path declared in meta/volumes.yml is written out again. "
|
||||
"Derive it from the registry so the mount and its consumer cannot "
|
||||
"drift apart:\n\n"
|
||||
" \"{{ lookup('volume', application_id, '<key>').target }}\"\n\n"
|
||||
+ "\n".join(sorted(set(findings)))
|
||||
+ f"\n\nPer-line opt-out: `# nocheck: {_RULE}` with the reason it "
|
||||
"cannot derive."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user