OK to answer my own question, it wasn't very trivial, but I got it to
work the way I wanted.
Entire playbook:
---
- name: Template directory structure
hosts: localhost
connection: local
gather_facts: no
vars:
template_src_dir: templates/app_config
template_dest_dir: /tmp/etc/app
tasks:
- name: Create directories
file:
path: "{{ template_dest_dir }}/{{ item.path }}"
state: directory
mode: "{{ item.mode }}"
with_filetree: "{{ template_src_dir }}/"
when: item.state == 'directory'
- name: Template files (explicitly skip directories in order to
use the 'src' attribute)
template:
src: '{{ item.src }}'
dest: "{{ template_dest_dir }}/{{ item.path }}"
mode: '{{ item.mode }}'
with_filetree: "{{ template_src_dir }}/"
when: item.state == 'file'
# Start orphan removal logic
- name: Find all remote content
find:
paths: "{{ template_dest_dir }}"
recurse: yes
file_type: any
register: _remote
- name: Find all local content
find:
paths: "{{ template_src_dir }}"
recurse: yes
file_type: any
register: _local
- set_fact:
# Files means anything other than a directory
_remote_files: "{{ _remote | json_query('files[?!isdir].path')
| map('regex_replace', '^' ~ template_dest_dir ~ '/', '') | list }}"
_remote_dirs: "{{ _remote | json_query('files[?isdir].path') |
map('regex_replace', '^' ~ template_dest_dir ~ '/', '') | list }}"
_local_files: "{{ _local | json_query('files[?!isdir].path') |
map('regex_replace', '^' ~ template_src_dir ~ '/', '') | list }}"
_local_dirs: "{{ _local | json_query('files[?isdir].path') |
map('regex_replace', '^' ~ template_src_dir ~ '/', '') | list }}"
- name: Clean up orphaned files
file:
path: "{{ template_dest_dir }}/{{ item }}"
state: absent
loop: "{{ _remote_files | difference(_local_files) }}"
- name: Clean up orphaned directories
file:
path: "{{ template_dest_dir }}/{{ item }}"
state: absent
loop: "{{ _remote_dirs | difference(_local_dirs) }}"
--
Dick Visser
Trust & Identity Service Operations Manager
GÉANT