diff --git a/Vagrantfile b/Vagrantfile index 4da8435..9799b5e 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -9,7 +9,7 @@ if File.exist?(settings_path) settings = YAML.load_file(settings_path) end -VAGRANT_BOX = settings['VAGRANT_BOX'] || 'debian/bookworm64' +VAGRANT_BOX = settings['VAGRANT_BOX'] || 'krislamo.org/debian13' VAGRANT_CPUS = settings['VAGRANT_CPUS'] || 2 VAGRANT_MEM = settings['VAGRANT_MEM'] || 2048 SSH_FORWARD = settings['SSH_FORWARD'] || false diff --git a/dev/host_vars/podman.yml b/dev/host_vars/podman.yml index e9a8ba2..6584ab1 100644 --- a/dev/host_vars/podman.yml +++ b/dev/host_vars/podman.yml @@ -1,14 +1,45 @@ -# base +############## +#### base #### +############## + allow_reboot: false manage_network: false -users: - kris: - uid: 1001 - gid: 1001 - home: true +################ +#### proxy ##### +################ -# podman -user_namespaces: - - kris +proxy: + servers: + - domain: cloud.local.krislamo.org + proxy_pass: http://127.0.0.1:8000 +################ +#### podman #### +################ + +podman_compose: + vagrant: + root: /opt/oci + trusted_keys: + - id: 42A3A92C5DA0F3E5F71A3710105B748C1362EB96 + compose: + - name: traefik + url: https://github.com/krislamo/traefik + version: d7197ddd5b7019c60faf5d164e555b6374972d40 + enabled: true + accept_newhostkey: true # Consider verifying manually instead + env: + VERSION: latest + SOCKET: /run/user/1000/podman/podman.sock + DASHBOARD: true + - name: nextcloud + url: https://github.com/krislamo/nextcloud + version: 245c91a22fa75e5dde1d423e88540529a4fa4f27 + enabled: true + env: + VERSION: latest + DOMAIN: cloud.local.krislamo.org + DATA: /opt/oci/nextcloud/data/ + REDIS_VERSION: latest + REDIS_PASSWORD: changeme diff --git a/dev/podman.yml b/dev/podman.yml index 21ea295..6bc2822 100644 --- a/dev/podman.yml +++ b/dev/podman.yml @@ -5,4 +5,5 @@ - host_vars/podman.yml roles: - base + - proxy - podman diff --git a/roles/base/tasks/system.yml b/roles/base/tasks/system.yml index 852585c..d7a3382 100644 --- a/roles/base/tasks/system.yml +++ b/roles/base/tasks/system.yml @@ -90,6 +90,20 @@ label: "{{ item.key }}" when: users is defined +- name: Create Ansible's temporary remote directory for users + ansible.builtin.file: + path: "{{ item.value.homedir | default('/home/' + item.key) }}/.ansible/tmp" + state: directory + mode: "700" + owner: "{{ item.key }}" + group: "{{ item.value.gid }}" + loop: "{{ users | dict2items }}" + loop_control: + label: "{{ item.key }}" + when: + - users is defined + - item.value.tmp | default(true) + - name: Set authorized_keys for system users ansible.posix.authorized_key: user: "{{ item.key }}" diff --git a/roles/podman/defaults/main.yml b/roles/podman/defaults/main.yml new file mode 100644 index 0000000..8639764 --- /dev/null +++ b/roles/podman/defaults/main.yml @@ -0,0 +1,4 @@ +# Default configuration for podman role +podman_repos_keytype: ed25519 +podman_ssh_key_path: "{{ ansible_user_dir }}/.ssh" +podman_nodocker: false diff --git a/roles/podman/files/docker-host.sh b/roles/podman/files/docker-host.sh new file mode 100644 index 0000000..ef4175c --- /dev/null +++ b/roles/podman/files/docker-host.sh @@ -0,0 +1,22 @@ +# shellcheck shell=sh +: "${UID:=$(id -u)}" + +if [ "$UID" -ne 0 ]; then + if [ -z "$XDG_RUNTIME_DIR" ] && [ -d "/run/user/$UID" ]; then + XDG_RUNTIME_DIR="/run/user/$UID" + export XDG_RUNTIME_DIR + fi + + PODMAN_SOCKET="$XDG_RUNTIME_DIR/podman/podman.sock" + if [ -S "$PODMAN_SOCKET" ]; then + DOCKER_HOST="unix://$PODMAN_SOCKET" + export DOCKER_HOST + fi + + if [ -z "$DBUS_SESSION_BUS_ADDRESS" ]; then + if [ -S "$XDG_RUNTIME_DIR/bus" ]; then + DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus" + export DBUS_SESSION_BUS_ADDRESS + fi + fi +fi diff --git a/roles/podman/handlers/main.yml b/roles/podman/handlers/main.yml new file mode 100644 index 0000000..172a369 --- /dev/null +++ b/roles/podman/handlers/main.yml @@ -0,0 +1,33 @@ +- name: Reload systemd manager configuration for all podman users + ansible.builtin.systemd: + daemon_reload: true + scope: user + become: true + become_user: "{{ item }}" + loop: "{{ podman_compose.keys() | list }}" + listen: podman_compose_systemd + +- name: Restart docker compose (podman) services + ansible.builtin.systemd: + state: restarted + name: "compose@{{ item.service }}" + scope: user + become: true + become_user: "{{ item.user }}" + loop: "{{ podman_compose_restart_list | default([]) | unique }}" + when: podman_compose_restart_list is defined + listen: podman_compose_restart + +- name: Start docker compose (podman) services and enable on boot + ansible.builtin.systemd: + name: "compose@{{ item.service }}" + state: started + enabled: true + scope: user + become: true + become_user: "{{ item.user }}" + loop: "{{ podman_compose_enable_list | default([]) }}" + loop_control: + label: "{{ item.user }}/{{ item.service }}" + when: item.enabled is defined and item.enabled is true + listen: podman_compose_enable diff --git a/roles/podman/tasks/deploy.yml b/roles/podman/tasks/deploy.yml new file mode 100644 index 0000000..1d55e65 --- /dev/null +++ b/roles/podman/tasks/deploy.yml @@ -0,0 +1,219 @@ +- name: Get user info for podman compose user + ansible.builtin.getent: + database: passwd + key: "{{ podman_user }}" + register: podman_user_info + +- name: Set user-specific variables + ansible.builtin.set_fact: + podman_rootdir: "{{ podman_compose_config.root }}" + podman_userid: "{{ podman_user_info.ansible_facts.getent_passwd[podman_user][1] }}" + podman_homedir: "{{ podman_user_info.ansible_facts.getent_passwd[podman_user][4] }}" + podman_project: "{{ podman_compose_config.compose }}" + podman_repos: "{{ podman_compose_config.root }}/.compose_repos" + +- name: Create docker compose (podman) root directory for user + ansible.builtin.file: + path: "{{ podman_rootdir }}" + state: directory + owner: "{{ podman_user }}" + group: "{{ podman_user }}" + mode: "0700" + +- name: Create user systemd directory + ansible.builtin.file: + path: "/home/{{ podman_user }}/.config/systemd/user" + state: directory + owner: "{{ podman_user }}" + group: "{{ podman_user }}" + mode: "0755" + +- name: Install docker compose (podman) systemd service for user + ansible.builtin.template: + src: compose.service.j2 + dest: "/home/{{ podman_user }}/.config/systemd/user/compose@.service" + owner: "{{ podman_user }}" + group: "{{ podman_user }}" + mode: "0644" + notify: podman_compose_systemd + +- name: Create directories for cloning docker compose (podman) repositories + ansible.builtin.file: + path: "{{ repo_dir }}" + state: directory + owner: "{{ podman_user }}" + group: "{{ podman_user }}" + mode: "0700" + loop: + - "{{ podman_repos }}" + loop_control: + loop_var: repo_dir + when: + - podman_project is defined + - podman_project | length > 0 + +- name: Create .ssh directory for podman compose user + ansible.builtin.file: + path: "{{ podman_homedir }}/.ssh" + state: directory + owner: "{{ podman_user }}" + group: "{{ podman_user }}" + mode: "0700" + when: + - podman_project is defined + - podman_project | length > 0 + +- name: Generate OpenSSH deploy keys for docker compose (podman) clones + community.crypto.openssh_keypair: + path: "{{ podman_ssh_key_path }}/podman-id_{{ podman_repos_keytype }}" + type: "{{ podman_repos_keytype }}" + comment: "{{ ansible_hostname }}-{{ podman_user }}-deploy-key" + owner: "{{ podman_user }}" + group: "{{ podman_user }}" + mode: "0600" + state: present + when: podman_project is defined + +- name: Import trusted GPG keys for docker compose (podman) projects + ansible.builtin.command: + cmd: "gpg --keyserver {{ key.keyserver | default('keys.openpgp.org') }} --recv-key {{ key.id }}" + become: true + become_user: "{{ podman_user }}" + loop: "{{ podman_compose_config.trusted_keys }}" + loop_control: + loop_var: key + label: "{{ key.id }}" + changed_when: false + when: podman_compose_config.trusted_keys is defined + +- name: Clone external docker compose (podman) projects + ansible.builtin.git: + repo: "{{ project.url }}" + dest: "{{ podman_repos }}/{{ project.name }}" + version: "{{ project.version }}" + accept_newhostkey: "{{ project.accept_newhostkey | default(false) }}" + gpg_allowlist: "{{ (project.trusted_keys | + default(podman_compose_config.trusted_keys | default([]))) | + map(attribute='id') | list }}" + verify_commit: >- + {{ + true if + (project.trusted_keys is defined and project.trusted_keys) or + ( + podman_compose_config.trusted_keys is defined and + podman_compose_config.trusted_keys + ) + else false + }} + key_file: "{{ podman_ssh_key_path }}/podman-id_{{ podman_repos_keytype }}" + become: true + become_user: "{{ podman_user }}" + loop: "{{ podman_project }}" + loop_control: + loop_var: project + label: "{{ project.url }}" + when: + - podman_project is defined + - podman_project | length > 0 + +- name: Create directories for docker compose (podman) projects + ansible.builtin.file: + path: "{{ podman_rootdir }}/{{ project.name }}" + state: directory + owner: "{{ podman_user }}" + group: "{{ podman_user }}" + mode: "0700" + loop: "{{ podman_project }}" + loop_control: + loop_var: project + label: "{{ project.name }}" + when: + - podman_project is defined + - podman_project | length > 0 + +- name: Synchronize docker-compose.yml + ansible.posix.synchronize: + # noqa jinja[spacing] + src: >- + {{ podman_repos }}/{{ project.name }}/ + {{- project.path | default('docker-compose.yml') }} + dest: "{{ podman_rootdir }}/{{ project.name }}/docker-compose.yml" + owner: false + group: false + delegate_to: "{{ inventory_hostname }}" + register: podman_compose_update + notify: + - podman_compose_restart + - podman_compose_enable + loop: "{{ podman_project | default([]) }}" + loop_control: + loop_var: project + label: "{{ project.name }}" + when: + - podman_project is defined + - podman_project | length > 0 + +- name: Update list of compose projects updated + ansible.builtin.set_fact: + podman_compose_restart_list: + "{{ (podman_compose_restart_list | default([])) + + [{'user': podman_user, 'service': item.project.name}] }}" + loop: "{{ podman_compose_update.results }}" + loop_control: + label: "{{ podman_user }}/{{ item.project.name }}" + when: (podman_compose_update.results | default([]) | length) > 0 + +- name: Fix ownership of synchronized compose files + ansible.builtin.file: + path: "{{ podman_rootdir }}/{{ project.name }}/docker-compose.yml" + owner: "{{ podman_user }}" + group: "{{ podman_user }}" + mode: "0664" + loop: "{{ podman_project | default([]) }}" + loop_control: + loop_var: project + label: "{{ project.name }}" + when: + - podman_project is defined + - podman_project | length > 0 + +- name: Set environment variables for docker compose (podman) projects + ansible.builtin.template: + src: compose-env.j2 + dest: "{{ podman_rootdir }}/{{ project.name }}/.env" + owner: "{{ podman_user }}" + group: "{{ podman_user }}" + mode: "0600" + register: podman_compose_env_update + notify: + - podman_compose_restart + - podman_compose_enable + no_log: true + loop: "{{ podman_project }}" + loop_control: + loop_var: project + label: "{{ project.name }}" + when: podman_project is defined and project.env is defined + +- name: Update list of compose projects who updated their .env + ansible.builtin.set_fact: + # noqa jinja[spacing] + podman_compose_restart_list: "{{ + (podman_compose_restart_list | default([])) + + ([{'user': podman_user,'service': item.project.name}] + if {'user': podman_user, 'service': item.project.name} + not in (podman_compose_restart_list | default([])) + else []) + }}" + loop: "{{ podman_compose_env_update.results }}" + loop_control: + label: "{{ podman_user }}/{{ item.project.name }}" + when: (podman_compose_env_update.results | default([]) | length) > 0 + +- name: Update list of enabled compose projects + ansible.builtin.set_fact: + podman_compose_enable_list: >- + {{ (podman_compose_enable_list | default([])) + + [{'user': podman_user, 'service': project.name}] }} + when: project.enabled | default(false) + notify: podman_compose_enable diff --git a/roles/podman/tasks/main.yml b/roles/podman/tasks/main.yml index fac30bc..3005b23 100644 --- a/roles/podman/tasks/main.yml +++ b/roles/podman/tasks/main.yml @@ -1,14 +1,23 @@ -- name: Install Podman +- name: Install Podman with Docker CLI tools ansible.builtin.apt: - name: ["podman", "podman-compose", "podman-docker"] + name: ["podman", "docker-cli", "docker-compose"] state: present -- name: Get user info for namespace users +- name: Install GnuPG tools and trusted CA bundle + ansible.builtin.apt: + name: ["gnupg", "ca-certificates"] + state: present + when: podman_compose is defined + +- name: Get podman user info for user namespace configuration ansible.builtin.getent: database: passwd key: "{{ item }}" - loop: "{{ user_namespaces }}" + loop: "{{ podman_compose.keys() | list }}" register: user_info + loop_control: + label: "{{ item }}" + when: podman_compose is defined - name: Configure /etc/subuid for rootless users ansible.builtin.lineinfile: @@ -22,6 +31,8 @@ backup: true mode: "0644" loop: "{{ user_info.results }}" + loop_control: + label: "{{ item.item }}" - name: Configure /etc/subgid for rootless users ansible.builtin.lineinfile: @@ -35,14 +46,33 @@ backup: true mode: "0644" loop: "{{ user_info.results }}" + loop_control: + label: "{{ item.item }}" -- name: Create nodocker file to disable Docker CLI emulation message - ansible.builtin.file: - path: /etc/containers/nodocker - state: touch - owner: root - group: root - mode: "0644" +- name: Enable lingering for podman compose user + ansible.builtin.command: + cmd: "loginctl enable-linger {{ item.item }}" + changed_when: false + loop: "{{ user_info.results }}" + loop_control: + label: "{{ item.item }}" + +- name: Start and enable the Podman socket + ansible.builtin.systemd: + name: podman.socket + state: started + enabled: true + scope: user + vars: + uid: "{{ item.ansible_facts.getent_passwd[item.item][1] }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ uid }}/bus" + become: true + become_user: "{{ item.item }}" + loop: "{{ user_info.results }}" + loop_control: + label: "{{ item.item }}" - name: Create global containers config directory ansible.builtin.file: @@ -58,5 +88,29 @@ events_logger = "journald" runtime = "crun" dest: /etc/containers/containers.conf - mode: "0644" backup: true + mode: "0644" + +- name: Configure Docker CLI to use rootless Podman socket + ansible.builtin.copy: + src: files/docker-host.sh + dest: /etc/profile.d/docker-host.sh + owner: root + group: root + mode: '0755' + +- name: Install git for repository cloning + ansible.builtin.apt: + name: git + state: present + when: podman_compose is defined + +- name: Deploy Podman compose projects for each user + ansible.builtin.include_tasks: deploy.yml + vars: + podman_user: "{{ compose_user.key }}" + podman_compose_config: "{{ compose_user.value }}" + loop: "{{ podman_compose | dict2items }}" + loop_control: + loop_var: compose_user + when: podman_compose is defined diff --git a/roles/podman/templates/compose-env.j2 b/roles/podman/templates/compose-env.j2 new file mode 100644 index 0000000..7906108 --- /dev/null +++ b/roles/podman/templates/compose-env.j2 @@ -0,0 +1,10 @@ +# {{ ansible_managed }} +{% if project.env is defined %} +{% for key, value in project.env.items() %} +{% if value is boolean %} +{{ key }}={{ value | lower }} +{% else %} +{{ key }}={{ value }} +{% endif %} +{% endfor %} +{% endif %} \ No newline at end of file diff --git a/roles/podman/templates/compose.service.j2 b/roles/podman/templates/compose.service.j2 new file mode 100644 index 0000000..5d7e3db --- /dev/null +++ b/roles/podman/templates/compose.service.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=%i docker compose (podman) service +Requires=podman.socket +After=podman.socket + +[Service] +Type=oneshot +RemainAfterExit=true +WorkingDirectory={{ podman_rootdir }}/%i +Environment=DOCKER_HOST=unix://%t/podman/podman.sock +ExecStart=/usr/bin/docker compose up -d --remove-orphans +ExecStop=/usr/bin/docker compose down + +[Install] +WantedBy=default.target