208 lines
9.2 KiB
YAML
208 lines
9.2 KiB
YAML
---
|
|
# Deploys one stack to the current Unraid host: sync the compose file, render
|
|
# its .env from Vault, provision its database if it declares one, then
|
|
# `docker compose up -d`. Compose files stay static/generic — only the
|
|
# rendered .env differs per run, which is what keeps re-runs idempotent.
|
|
#
|
|
# Included from main.yml when the stack's `state:` is `present`; the facts it
|
|
# relies on (stack_local_dir, stack_remote_dir) are set there.
|
|
|
|
- name: Load portable stack variables
|
|
ansible.builtin.include_vars:
|
|
file: "{{ stack_local_dir }}/common/vars.yml"
|
|
name: stack_common_vars
|
|
|
|
- name: Load Unraid-specific stack variables
|
|
ansible.builtin.include_vars:
|
|
file: "{{ stack_local_dir }}/ansible/unraid/vars.yml"
|
|
name: stack_platform_vars
|
|
|
|
- name: Ensure remote project directory exists
|
|
ansible.builtin.file:
|
|
path: "{{ stack_remote_dir }}"
|
|
state: directory
|
|
mode: "0750"
|
|
|
|
# Bind-mount source directories the stack needs to exist *with a specific
|
|
# owner* before `docker compose up`. Opt-in per stack (`appdata:` on the
|
|
# host_vars entry) and normally unset: the LinuxServer images (arr, jellyfin)
|
|
# start as root and chown their own /config on boot, and postgres/forgejo run
|
|
# as root outright, so for those Docker creating a missing bind path as
|
|
# root:root is fine. A rootless image (watchstate runs as uid 99 and refuses
|
|
# to start if it can't write /config) has nothing to do the chown, so the
|
|
# directory has to arrive already owned correctly — otherwise the first
|
|
# `up` creates it as root:root and the container exits.
|
|
#
|
|
# `recurse` defaults on, matching upstream's `chown -R`, to also fix a
|
|
# directory that a previous run already created wrong. Keep the list to small
|
|
# state/config dirs; don't point it at a media share.
|
|
- name: Ensure stack appdata directories exist with the right ownership
|
|
ansible.builtin.file:
|
|
path: "{{ item.path }}"
|
|
state: directory
|
|
owner: "{{ item.owner | default(omit) }}"
|
|
group: "{{ item.group | default(omit) }}"
|
|
mode: "{{ item.mode | default('0750') }}"
|
|
recurse: "{{ item.recurse | default(true) }}"
|
|
loop: "{{ stack.appdata | default([]) }}"
|
|
loop_control:
|
|
label: "{{ item.path }}"
|
|
|
|
- name: Copy docker-compose.yml
|
|
ansible.builtin.copy:
|
|
src: "{{ stack_local_dir }}/ansible/unraid/docker-compose.yml"
|
|
dest: "{{ stack_remote_dir }}/docker-compose.yml"
|
|
mode: "0640"
|
|
|
|
# --- Unraid Docker page presentation ---------------------------------------
|
|
#
|
|
# The committed icon.png has two destinations, because Unraid draws the stack
|
|
# row and the containers under it from entirely different sources:
|
|
#
|
|
# stack row — Compose Manager serves <project dir>/icon.png directly
|
|
# (icon.php, which also accepts .jpg/.gif/.svg or a bare
|
|
# `icon`). No label, no template, no URL: a file or nothing.
|
|
# Present only in the maintained fork, Compose Manager Plus;
|
|
# the original plugin has no icon support at all, and there
|
|
# the copy is inert rather than harmful.
|
|
# containers — the Docker page reads a container's icon, WebUI link and
|
|
# console shell from the dockerMan template that created it.
|
|
# A Compose stack has no template, so 6.10+ falls back to the
|
|
# `net.unraid.docker.*` labels the compose files set, and
|
|
# STACK_ICON below is what the icon label resolves to.
|
|
#
|
|
# Hence the appdata copy as well: the label is a path the webgui resolves at
|
|
# page-render time, and pointing it into /boot to reuse the project-dir copy
|
|
# would put the flash drive in the path of every Docker page load.
|
|
#
|
|
# Only the second of those is per-container, so a stack running more than one
|
|
# service can also commit icon-<service>.png files. Those get the appdata copy
|
|
# only — there is exactly one stack row and it already has its icon. See
|
|
# main.yml, which is where the names are resolved.
|
|
- name: Sync stack icons to the Unraid host
|
|
when: stack_icon_dest | length > 0 or stack_service_icon_names | length > 0
|
|
block:
|
|
- name: Ensure the icon directory exists
|
|
ansible.builtin.file:
|
|
path: "{{ unraid_icons_root }}"
|
|
state: directory
|
|
mode: "0755"
|
|
|
|
- name: Copy stack icon for the container labels
|
|
ansible.builtin.copy:
|
|
src: "{{ stack_local_dir }}/ansible/unraid/icon.png"
|
|
dest: "{{ stack_icon_dest }}"
|
|
mode: "0644"
|
|
when: stack_icon_dest | length > 0
|
|
|
|
# Into the project folder on the flash drive, next to the compose file.
|
|
# `copy` is checksum-based, so a redeploy that hasn't changed the icon
|
|
# writes nothing — this costs one flash write per icon, not one per run.
|
|
- name: Copy stack icon for the Compose Manager stack row
|
|
ansible.builtin.copy:
|
|
src: "{{ stack_local_dir }}/ansible/unraid/icon.png"
|
|
dest: "{{ stack_remote_dir }}/icon.png"
|
|
mode: "0644"
|
|
when: stack_icon_dest | length > 0
|
|
|
|
# Appdata only, and no flash-drive copy: these label individual containers,
|
|
# which is the one thing the Compose Manager stack row isn't.
|
|
- name: Copy per-service icons for the container labels
|
|
ansible.builtin.copy:
|
|
src: "{{ stack_local_dir }}/ansible/unraid/icon-{{ item }}.png"
|
|
dest: "{{ unraid_icons_root }}/{{ stack.name }}-{{ item }}.png"
|
|
mode: "0644"
|
|
loop: "{{ stack_service_icon_names }}"
|
|
|
|
- name: Look up stack secrets from Vault
|
|
ansible.builtin.set_fact:
|
|
vault_secrets: >-
|
|
{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
stack.vault_path,
|
|
engine_mount_point=vault_kv_mount,
|
|
url=vault_addr,
|
|
auth_method=vault_auth_method,
|
|
role_id=vault_role_id | default(omit),
|
|
secret_id=vault_secret_id | default(omit)).secret }}
|
|
no_log: true
|
|
|
|
- name: Render .env file
|
|
ansible.builtin.template:
|
|
src: env.j2
|
|
dest: "{{ stack_remote_dir }}/.env"
|
|
mode: "0600"
|
|
vars:
|
|
# The role-computed icon paths first, then portable values, then
|
|
# Unraid-specific overrides on top; Vault secrets win over all three (see
|
|
# env.j2). STACK_ICON and the per-service STACK_ICON_<SERVICE> keys sit at
|
|
# the bottom so either vars.yml layer can replace one with a hosted URL,
|
|
# and STACK_ICON is always defined so Compose never warns about an unset
|
|
# variable in the label block.
|
|
#
|
|
# STACK_ICON_<SERVICE> only exists when the matching icon-<service>.png is
|
|
# committed. A compose file that interpolates one without the file present
|
|
# gets Compose's empty-value warning and the placeholder icon — set it in
|
|
# vars.yml instead if the icon is meant to be a URL.
|
|
env_defaults: >-
|
|
{{ {'STACK_ICON': stack_icon_dest}
|
|
| combine(stack_service_icons)
|
|
| combine(stack_common_vars.env_defaults | default({}))
|
|
| combine(stack_platform_vars.env_defaults | default({})) }}
|
|
no_log: true
|
|
|
|
- name: Provision application database
|
|
when: stack.db is defined
|
|
no_log: true
|
|
block:
|
|
- name: Look up Postgres superuser credentials from Vault
|
|
ansible.builtin.set_fact:
|
|
pg_admin_secrets: >-
|
|
{{ lookup('community.hashi_vault.vault_kv2_get',
|
|
stack.db.admin_vault_path,
|
|
engine_mount_point=vault_kv_mount,
|
|
url=vault_addr,
|
|
auth_method=vault_auth_method,
|
|
role_id=vault_role_id | default(omit),
|
|
secret_id=vault_secret_id | default(omit)).secret }}
|
|
|
|
- name: Ensure application database role exists
|
|
community.postgresql.postgresql_user:
|
|
name: "{{ stack.db.user }}"
|
|
password: "{{ vault_secrets[stack.db.password_vault_key] }}"
|
|
login_host: "{{ stack.db.provision_host }}"
|
|
login_port: "{{ stack.db.provision_port }}"
|
|
login_user: "{{ pg_admin_secrets.POSTGRES_SUPERUSER }}"
|
|
login_password: "{{ pg_admin_secrets.POSTGRES_SUPERUSER_PASSWORD }}"
|
|
state: present
|
|
delegate_to: localhost
|
|
become: false
|
|
|
|
# Role first, then the database with `owner:` — see the same pair of
|
|
# tasks in roles/lxc_app for the full reasoning. The short version is
|
|
# that `priv: ALL` on postgresql_user was removed from
|
|
# community.postgresql in 4.0.0, and ownership is what the replacement
|
|
# should have been anyway.
|
|
#
|
|
# This instance is pinned to Postgres 13, so unlike the Proxmox one it
|
|
# was never *also* broken by the Postgres 15 `public` schema change —
|
|
# the removal of `priv` is the only thing that forces the edit here.
|
|
# Kept identical to the other two roles regardless: the point of the
|
|
# three app roles sharing this block is that an app moving between
|
|
# platforms gets the same database either way.
|
|
- name: Ensure application database exists
|
|
community.postgresql.postgresql_db:
|
|
name: "{{ stack.db.name }}"
|
|
owner: "{{ stack.db.user }}"
|
|
login_host: "{{ stack.db.provision_host }}"
|
|
login_port: "{{ stack.db.provision_port }}"
|
|
login_user: "{{ pg_admin_secrets.POSTGRES_SUPERUSER }}"
|
|
login_password: "{{ pg_admin_secrets.POSTGRES_SUPERUSER_PASSWORD }}"
|
|
state: present
|
|
delegate_to: localhost
|
|
become: false
|
|
|
|
- name: Deploy stack with Docker Compose
|
|
community.docker.docker_compose_v2:
|
|
project_src: "{{ stack_remote_dir }}"
|
|
state: present
|
|
pull: policy
|