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.
This commit is contained in:
Jack Carter
2026-04-17 10:28:50 +02:00
parent 843d349012
commit 02e2c94ddb
3 changed files with 33 additions and 8 deletions
+26 -3
View File
@@ -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:
+1 -1
View File
@@ -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
@@ -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 }}"