--- - name: Deploy Planka kanban board hosts: all become: true vars: planka_stack_dir: /opt/stacks/planka planka_version: "2.0.0-rc.3" planka_port: 3000 planka_base_url: "http://{{ ansible_default_ipv4.address }}:{{ planka_port }}" planka_trust_proxy: false planka_token_expires_in: 365 planka_log_level: warn planka_db_name: planka planka_admin_email: "admin@example.com" planka_admin_password: "ChangeMe123!" planka_admin_name: "Planka Admin" planka_admin_username: "admin" # Set true to force regeneration of SECRET_KEY and INTERNAL_ACCESS_TOKEN planka_rotate_secrets: false pre_tasks: - name: Fail if planka_admin_password is default ansible.builtin.fail: msg: "planka_admin_password is still the default value. Pass a real password via -e or Semaphore extra vars." when: planka_admin_password == "ChangeMe123!" - name: Fail if planka_base_url is not set ansible.builtin.fail: msg: "planka_base_url is required. Pass it via -e planka_base_url=https://planka.example.wulf.cloud" when: planka_base_url is not defined or planka_base_url | length == 0 - name: Verify Docker is running ansible.builtin.service_facts: - name: Fail if Docker is not running ansible.builtin.fail: msg: "Docker is not running on this host." when: "'docker.service' not in ansible_facts.services or ansible_facts.services['docker.service'].state != 'running'" - name: Ensure pangolin Docker network exists community.docker.docker_network: name: pangolin driver: bridge state: present tasks: - name: Create stack directory ansible.builtin.file: path: "{{ planka_stack_dir }}" state: directory owner: root group: root mode: "0750" - name: Check if .secrets file exists (persisted secrets) ansible.builtin.stat: path: "{{ planka_stack_dir }}/.secrets" register: _secrets_stat - name: Generate new secrets ansible.builtin.shell: cmd: "echo SECRET_KEY=$(openssl rand -hex 64) && echo INTERNAL_ACCESS_TOKEN=$(openssl rand -hex 32)" register: _new_secrets changed_when: false when: not _secrets_stat.stat.exists or planka_rotate_secrets - name: Persist generated secrets ansible.builtin.copy: dest: "{{ planka_stack_dir }}/.secrets" content: "{{ _new_secrets.stdout }}" owner: root group: root mode: "0600" when: not _secrets_stat.stat.exists or planka_rotate_secrets - name: Read persisted secrets ansible.builtin.slurp: src: "{{ planka_stack_dir }}/.secrets" register: _secrets_file when: not ansible_check_mode - name: Parse secret values ansible.builtin.set_fact: planka_secret_key: "{{ (_secrets_file.content | b64decode).split('\n') | select('match', '^SECRET_KEY=') | first | regex_replace('^SECRET_KEY=', '') }}" planka_access_token: "{{ (_secrets_file.content | b64decode).split('\n') | select('match', '^INTERNAL_ACCESS_TOKEN=') | first | regex_replace('^INTERNAL_ACCESS_TOKEN=', '') }}" when: not ansible_check_mode - name: Deploy .env ansible.builtin.template: src: templates/planka.env.j2 dest: "{{ planka_stack_dir }}/.env" owner: root group: root mode: "0600" notify: Restart planka when: not ansible_check_mode - name: Deploy compose.yaml ansible.builtin.template: src: templates/planka-compose.yml.j2 dest: "{{ planka_stack_dir }}/compose.yaml" owner: root group: root mode: "0640" notify: Restart planka - name: Deploy Planka stack community.docker.docker_compose_v2: project_src: "{{ planka_stack_dir }}" pull: always build: never state: present register: _compose_result - name: Wait for Planka to respond ansible.builtin.uri: url: "http://{{ ansible_default_ipv4.address }}:{{ planka_port }}" status_code: 200 follow_redirects: safe register: _health until: _health.status == 200 retries: 12 delay: 5 ignore_errors: true - name: Deployment summary ansible.builtin.debug: msg: - "Planka deployed" - "URL: {{ planka_base_url }}" - "Admin: {{ planka_admin_email }}" - "API token: {{ planka_access_token }}" - "Stack dir: {{ planka_stack_dir }}" - "Secrets rotated: {{ planka_rotate_secrets }}" handlers: - name: Restart planka community.docker.docker_compose_v2: project_src: "{{ planka_stack_dir }}" state: present recreate: always listen: Restart planka