From 79001007ea301c21816bbef7c105cad926d43b9a Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 17 Oct 2026 12:18:44 +0000 Subject: [PATCH] policy/v2: compare compat captures through the policy manager ACL, grant and SSH captures now also check FilterForNode and SSHPolicy, which add the global merge and caches the per-node compile skips. --- .../v2/tailscale_acl_data_compat_test.go | 17 ++++++++++ .../policy/v2/tailscale_grants_compat_test.go | 17 ++++++++++ .../v2/tailscale_ssh_data_compat_test.go | 33 +++++++++++++++---- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/hscontrol/policy/v2/tailscale_acl_data_compat_test.go b/hscontrol/policy/v2/tailscale_acl_data_compat_test.go index 031d1cca7..9b0c2dc45 100644 --- a/hscontrol/policy/v2/tailscale_acl_data_compat_test.go +++ b/hscontrol/policy/v2/tailscale_acl_data_compat_test.go @@ -343,6 +343,11 @@ func testACLSuccess( tf.TestID, ) + // The manager adds the global merge and its caches on top of the + // per-node compile; nodes get their filter from it. + pm, err := NewPolicyManager(policyJSON, users, nodes.ViewSlice()) + require.NoError(t, err, "%s: policy manager should load the policy", tf.TestID) + for nodeName, capture := range tf.Captures { t.Run(nodeName, func(t *testing.T) { node := findNodeByGivenName(nodes, nodeName) @@ -386,6 +391,18 @@ func testACLSuccess( diff, ) } + + pmRules, err := pm.FilterForNode(node.View()) + require.NoError(t, err) + + if diff := cmp.Diff(wantRules, pmRules, opts...); diff != "" { + t.Errorf( + "%s/%s: policy manager filter rules mismatch (-want +got):\n%s", + tf.TestID, + nodeName, + diff, + ) + } }) } } diff --git a/hscontrol/policy/v2/tailscale_grants_compat_test.go b/hscontrol/policy/v2/tailscale_grants_compat_test.go index 2ef4b2bba..f1d957d94 100644 --- a/hscontrol/policy/v2/tailscale_grants_compat_test.go +++ b/hscontrol/policy/v2/tailscale_grants_compat_test.go @@ -292,6 +292,11 @@ func testGrantSuccess( err = pol.validate() require.NoError(t, err, "%s: policy should validate successfully", tf.TestID) + // The manager adds the global merge and its caches on top of the + // per-node compile; nodes get their filter from it. + pm, err := NewPolicyManager(policyJSON, users, nodes.ViewSlice()) + require.NoError(t, err, "%s: policy manager should load the policy", tf.TestID) + for nodeName, capture := range tf.Captures { t.Run(nodeName, func(t *testing.T) { node := findGrantsNode(nodes, nodeName) @@ -323,6 +328,18 @@ func testGrantSuccess( diff, ) } + + pmRules, err := pm.FilterForNode(node.View()) + require.NoError(t, err) + + if diff := cmp.Diff(wantRules, pmRules, opts...); diff != "" { + t.Errorf( + "%s/%s: policy manager filter rules mismatch (-tailscale +headscale):\n%s", + tf.TestID, + nodeName, + diff, + ) + } }) } } diff --git a/hscontrol/policy/v2/tailscale_ssh_data_compat_test.go b/hscontrol/policy/v2/tailscale_ssh_data_compat_test.go index 3e7f8f8a0..935399659 100644 --- a/hscontrol/policy/v2/tailscale_ssh_data_compat_test.go +++ b/hscontrol/policy/v2/tailscale_ssh_data_compat_test.go @@ -233,6 +233,10 @@ func TestSSHDataCompat(t *testing.T) { tf.Input.FullPolicy, ) + // Nodes get their SSH policy through the manager's cache. + pm, err := NewPolicyManager(policyJSON, users, nodes.ViewSlice()) + require.NoError(t, err, "%s: policy manager should load the policy", tf.TestID) + for nodeName, capture := range tf.Captures { t.Run(nodeName, func(t *testing.T) { node := findNodeByGivenName(nodes, nodeName) @@ -282,15 +286,30 @@ func TestSSHDataCompat(t *testing.T) { ) } + pmSSH, err := pm.SSHPolicy("https://unused", node.View()) + require.NoError(t, err) + + if diff := cmp.Diff(wantSSH, pmSSH, opts...); diff != "" { + t.Errorf( + "%s/%s: policy manager SSH policy mismatch (-tailscale +headscale):\n%s", + tf.TestID, + nodeName, + diff, + ) + } + // EquateEmpty hides "rules":null vs "rules":[]; // pin the captured shape separately. - if gotSSH != nil && capture.Netmap != nil && - capture.Netmap.SSHPolicy != nil { - assert.Equalf(t, - capture.Netmap.SSHPolicy.Rules == nil, - gotSSH.Rules == nil, - "%s/%s: rules null-vs-[] mismatch", tf.TestID, nodeName, - ) + if capture.Netmap != nil && capture.Netmap.SSHPolicy != nil { + for _, got := range []*tailcfg.SSHPolicy{gotSSH, pmSSH} { + if got != nil { + assert.Equalf(t, + capture.Netmap.SSHPolicy.Rules == nil, + got.Rules == nil, + "%s/%s: rules null-vs-[] mismatch", tf.TestID, nodeName, + ) + } + } } // Separate presence check: the fields ignored by