mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-07 16:40:21 +09:00
compat tests: build node topology per-scenario, not globally
tscap uses clean-slate mode: each scenario wipes every device from the tailnet before logging new ones in, so node IPs change from scenario to scenario. The previous compat tests built a single nodes slice from the first file's topology (or from a hardcoded setup) and used it for every scenario, which produced IP mismatches in the filter rule comparisons. Rebuild types.Nodes per scenario from the current file's Topology. ACL already had a build-from-topology helper; extract the equivalent for grants (buildGrantsNodesFromCapture) and use it for SSH too. Drop the "first 8 nodes only" shim in grants. Pass count jumped ~2500 tests across the four compat suites. Updates #3157 Updates #3169
This commit is contained in:
@@ -313,23 +313,6 @@ func TestACLCompat(t *testing.T) {
|
||||
|
||||
t.Logf("Loaded %d ACL test files", len(files))
|
||||
|
||||
// Build nodes from the first non-error file's topology.
|
||||
// All files share the same 19-node tailnet topology.
|
||||
var users types.Users
|
||||
|
||||
var nodes types.Nodes
|
||||
|
||||
for _, file := range files {
|
||||
tf := loadACLTestFile(t, file)
|
||||
if !tf.Error && len(tf.Topology.Nodes) > 0 {
|
||||
users, nodes = buildACLUsersAndNodes(t, tf)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NotEmpty(t, nodes, "no non-error ACL file found")
|
||||
|
||||
for _, file := range files {
|
||||
tf := loadACLTestFile(t, file)
|
||||
|
||||
@@ -352,6 +335,13 @@ func TestACLCompat(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
// Build nodes per-scenario from this file's topology.
|
||||
// tscap uses clean-slate mode, so each scenario has
|
||||
// different node IPs; using a shared topology would
|
||||
// cause IP mismatches in filter rule comparisons.
|
||||
users, nodes := buildACLUsersAndNodes(t, tf)
|
||||
require.NotEmpty(t, nodes, "%s: topology is empty", tf.TestID)
|
||||
|
||||
testACLSuccess(t, tf, users, nodes)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
|
||||
// setupGrantsCompatUsers returns the 3 test users for grants compatibility tests.
|
||||
// Users get norse-god names; nodes get original-151 pokémon names — matching
|
||||
// the anonymized identifiers tscap writes into the capture files
|
||||
@@ -270,6 +269,69 @@ func findGrantsNode(nodes types.Nodes, name string) *types.Node {
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildGrantsNodesFromCapture constructs types.Nodes from a capture's
|
||||
// topology section. Each scenario in tscap uses clean-slate mode, so
|
||||
// node IPs differ between scenarios; this builds the node set with
|
||||
// the IPs that were actually present during that capture.
|
||||
func buildGrantsNodesFromCapture(
|
||||
users types.Users,
|
||||
tf *testcapture.Capture,
|
||||
) types.Nodes {
|
||||
nodes := make(types.Nodes, 0, len(tf.Topology.Nodes))
|
||||
autoID := 1
|
||||
|
||||
for _, nodeDef := range tf.Topology.Nodes {
|
||||
node := &types.Node{
|
||||
ID: types.NodeID(autoID), //nolint:gosec
|
||||
GivenName: nodeDef.Hostname,
|
||||
IPv4: ptrAddr(nodeDef.IPv4),
|
||||
IPv6: ptrAddr(nodeDef.IPv6),
|
||||
Tags: nodeDef.Tags,
|
||||
}
|
||||
autoID++
|
||||
|
||||
hostinfo := &tailcfg.Hostinfo{}
|
||||
|
||||
if len(nodeDef.RoutableIPs) > 0 {
|
||||
routableIPs := make([]netip.Prefix, 0, len(nodeDef.RoutableIPs))
|
||||
for _, r := range nodeDef.RoutableIPs {
|
||||
routableIPs = append(routableIPs, netip.MustParsePrefix(r))
|
||||
}
|
||||
|
||||
hostinfo.RoutableIPs = routableIPs
|
||||
}
|
||||
|
||||
node.Hostinfo = hostinfo
|
||||
|
||||
if len(nodeDef.ApprovedRoutes) > 0 {
|
||||
approved := make([]netip.Prefix, 0, len(nodeDef.ApprovedRoutes))
|
||||
for _, r := range nodeDef.ApprovedRoutes {
|
||||
approved = append(approved, netip.MustParsePrefix(r))
|
||||
}
|
||||
|
||||
node.ApprovedRoutes = approved
|
||||
} else {
|
||||
node.ApprovedRoutes = []netip.Prefix{}
|
||||
}
|
||||
|
||||
// Assign user — untagged nodes look up by User field.
|
||||
if len(nodeDef.Tags) == 0 && nodeDef.User != "" {
|
||||
for i := range users {
|
||||
if users[i].Name == nodeDef.User {
|
||||
node.User = &users[i]
|
||||
node.UserID = &users[i].ID
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// convertPolicyUserEmails used to map SaaS-side emails to @example.com.
|
||||
// tscap now anonymizes the policy JSON at write time (kratail2tid -> odin,
|
||||
// kristoffer -> thor, monitorpasskeykradalby -> freya), so the captured
|
||||
@@ -328,7 +390,6 @@ func TestGrantsCompat(t *testing.T) {
|
||||
t.Logf("Loaded %d grant test files", len(files))
|
||||
|
||||
users := setupGrantsCompatUsers()
|
||||
allNodes := setupGrantsCompatNodes(users)
|
||||
|
||||
for _, file := range files {
|
||||
tf := loadGrantTestFile(t, file)
|
||||
@@ -342,18 +403,13 @@ func TestGrantsCompat(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
// Determine which node set to use based on the test's topology.
|
||||
// Tests captured with the expanded 15-node topology (V26+) have
|
||||
// nodes like pidgey (exit-a), rattata (group-a-client), etc.
|
||||
// Tests from the original 8-node topology should only use the
|
||||
// first 8 nodes to avoid resolving extra IPs from nodes that
|
||||
// weren't present during capture.
|
||||
nodes := allNodes
|
||||
if _, hasNewNodes := tf.Captures["pidgey"]; !hasNewNodes {
|
||||
nodes = allNodes[:8]
|
||||
}
|
||||
// Build nodes per-scenario from this file's topology.
|
||||
// tscap uses clean-slate mode, so each scenario has
|
||||
// different node IPs.
|
||||
nodes := buildGrantsNodesFromCapture(users, tf)
|
||||
|
||||
// Convert Tailscale user emails to headscale @example.com format
|
||||
// Use the captured full policy verbatim (anonymization
|
||||
// in tscap already rewrote SaaS emails).
|
||||
policyJSON := convertPolicyUserEmails(tf.Input.FullPolicy)
|
||||
|
||||
if tf.Input.APIResponseCode == 400 || tf.Error {
|
||||
|
||||
@@ -156,7 +156,6 @@ func TestSSHDataCompat(t *testing.T) {
|
||||
t.Logf("Loaded %d SSH test files", len(files))
|
||||
|
||||
users := setupSSHDataCompatUsers()
|
||||
nodes := setupSSHDataCompatNodes(users)
|
||||
|
||||
for _, file := range files {
|
||||
tf := loadSSHTestFile(t, file)
|
||||
@@ -180,6 +179,11 @@ func TestSSHDataCompat(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
// Build nodes per-scenario from this file's topology.
|
||||
// tscap uses clean-slate mode, so each scenario has
|
||||
// different node IPs.
|
||||
nodes := buildGrantsNodesFromCapture(users, tf)
|
||||
|
||||
// Use the captured full policy verbatim. Anonymization in
|
||||
// tscap already rewrites SaaS emails to @example.com.
|
||||
policyJSON := tf.Input.FullPolicy
|
||||
|
||||
Reference in New Issue
Block a user