From 02e2c94ddb4eac9d6ceace45752a1d16c7d143c8 Mon Sep 17 00:00:00 2001 From: Jack Carter <128555021+SunsetDrifter@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:28:50 +0200 Subject: [PATCH] fix: resolve peer-type source/destination resources on roundtrip PR#31 fixed export silently dropping peer-based policy sources, but collapsed them into sources: [peer_name]. The configure role resolver only maps sources against group_ids, so peer names fell through unresolved. The netbird_policy module then sent literal peer names to the API, which silently accepted them and wiped sourceResource -- turning the exported YAML into a policy with no source at all (reported as changed: true). Changes: - Export template emits source_resource: {name, type: peer} for peer-type resources (keeps host/domain/subnet as {id, type}). - _resolve_policy in netbird_resolve accepts peer_ids and converts {name, type: peer} to {id, type: peer}; non-peer refs pass through. - configure/tasks/main.yml passes peer_ids to the policy resolve call (the map is already built on line 214 for networks). Roundtrip verified on live NetBird: sourceResource preserved; apply reports ok (idempotent) instead of false changed: true. --- plugins/filter/netbird_resolve.py | 29 +++++++++++++++++-- roles/configure/tasks/main.yml | 2 +- .../export/access_control/policies.yml.j2 | 10 ++++--- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/plugins/filter/netbird_resolve.py b/plugins/filter/netbird_resolve.py index 865d5b6..53ecb16 100644 --- a/plugins/filter/netbird_resolve.py +++ b/plugins/filter/netbird_resolve.py @@ -23,8 +23,23 @@ def _resolve_setup_key(sk, group_ids): return result -def _resolve_policy(policy, group_ids, posture_check_ids): - """Resolve a single policy's group and posture check references.""" +def _resolve_resource_ref(resource, peer_ids): + """Resolve {name, type: peer} to {id, type: peer} using peer_ids map. + + Non-peer resources (host/domain/subnet) pass through unchanged since their + IDs in exported YAML are already concrete API IDs. + """ + if not isinstance(resource, dict): + return resource + if resource.get('type') == 'peer' and 'name' in resource and 'id' not in resource: + name = resource['name'] + return {'id': peer_ids.get(name, name), 'type': 'peer'} + return resource + + +def _resolve_policy(policy, group_ids, posture_check_ids, peer_ids=None): + """Resolve a single policy's group, posture check, and peer references.""" + peer_ids = peer_ids or {} result = dict(policy) if 'source_posture_checks' in policy: @@ -42,6 +57,14 @@ def _resolve_policy(policy, group_ids, posture_check_ids): resolved_rule['destinations'] = _resolve_names( rule.get('destinations', []), group_ids ) + if rule.get('source_resource') is not None: + resolved_rule['source_resource'] = _resolve_resource_ref( + rule['source_resource'], peer_ids + ) + if rule.get('destination_resource') is not None: + resolved_rule['destination_resource'] = _resolve_resource_ref( + rule['destination_resource'], peer_ids + ) resolved_rules.append(resolved_rule) result['rules'] = resolved_rules @@ -106,7 +129,7 @@ def netbird_resolve_ids(resource_list, resource_type, **kwargs): if resource_type == 'setup_key': result.append(_resolve_setup_key(item, group_ids)) elif resource_type == 'policy': - result.append(_resolve_policy(item, group_ids, posture_check_ids)) + result.append(_resolve_policy(item, group_ids, posture_check_ids, peer_ids)) elif resource_type == 'network': result.append(_resolve_network(item, group_ids, peer_ids)) else: diff --git a/roles/configure/tasks/main.yml b/roles/configure/tasks/main.yml index 8749aa6..66659d9 100644 --- a/roles/configure/tasks/main.yml +++ b/roles/configure/tasks/main.yml @@ -232,7 +232,7 @@ - name: Resolve group/peer/posture-check names to IDs ansible.builtin.set_fact: - _resolved_policies: "{{ netbird_policies | default([]) | community.ansible_netbird.netbird_resolve_ids('policy', group_ids=group_ids, posture_check_ids=posture_check_ids) }}" + _resolved_policies: "{{ netbird_policies | default([]) | community.ansible_netbird.netbird_resolve_ids('policy', group_ids=group_ids, posture_check_ids=posture_check_ids, peer_ids=peer_ids) }}" _resolved_networks: "{{ netbird_networks | default([]) | community.ansible_netbird.netbird_resolve_ids('network', group_ids=group_ids, peer_ids=peer_ids) }}" when: commit | bool diff --git a/roles/export/templates/export/access_control/policies.yml.j2 b/roles/export/templates/export/access_control/policies.yml.j2 index d5b0a78..e941ad4 100644 --- a/roles/export/templates/export/access_control/policies.yml.j2 +++ b/roles/export/templates/export/access_control/policies.yml.j2 @@ -48,8 +48,9 @@ netbird_policies: {% endif %} {% endfor %} {% elif _src_res is not none and _src_res is mapping and _src_res.type == 'peer' %} - sources: - - {{ peer_id_map.get(_src_res.id, _src_res.id) }} + source_resource: + name: "{{ peer_id_map.get(_src_res.id, _src_res.id) }}" + type: "peer" {% elif _src_res is not none and _src_res is mapping %} source_resource: id: "{{ _src_res.id }}" @@ -66,8 +67,9 @@ netbird_policies: {% endif %} {% endfor %} {% elif _dst_res is not none and _dst_res is mapping and _dst_res.type == 'peer' %} - destinations: - - {{ peer_id_map.get(_dst_res.id, _dst_res.id) }} + destination_resource: + name: "{{ peer_id_map.get(_dst_res.id, _dst_res.id) }}" + type: "peer" {% elif _dst_res is not none and _dst_res is mapping %} destination_resource: id: "{{ _dst_res.id }}"