integration: reuse images and remove redundant scenario setup

This commit is contained in:
Kristoffer Dalby
2026-10-07 14:29:56 +00:00
parent 47653f06fb
commit bcc71f9b57
3 changed files with 369 additions and 346 deletions
+85 -55
View File
@@ -25,9 +25,10 @@ import (
// - bypass: no-bypass talks to the server over gRPC; bypass opens the
// database directly.
//
// Each row spins up its own scenario because policy_mode is fixed at boot
// via `HEADSCALE_POLICY_MODE`. The two users + two nodes give the tests
// block real `user@` aliases to resolve against.
// Each policy mode shares one scenario across its sequential checks because
// policy_mode is fixed at boot. The two users + two nodes give the tests
// block real `user@` aliases to resolve against. Check inputs are separate
// from the live policy, which must remain unchanged after every check.
func TestPolicyCheckCommand(t *testing.T) {
IntegrationSkip(t)
@@ -80,44 +81,56 @@ func TestPolicyCheckCommand(t *testing.T) {
type row struct {
name string
policyMode string
fixture fixture
bypass bool
wantErr string
wantStdout string
}
modes := []string{"file", "database"} //nolint:goconst // axis labels match HEADSCALE_POLICY_MODE values
modes := []types.PolicyMode{types.PolicyModeFile, types.PolicyModeDB}
bypasses := []bool{false, true}
rows := make([]row, 0, len(modes)*len(fixtures)*len(bypasses))
rows := make([]row, 0, len(fixtures)*len(bypasses))
for _, mode := range modes {
for _, f := range fixtures {
for _, bypass := range bypasses {
suffix := "no-bypass"
if bypass {
suffix = "bypass"
}
r := row{
name: mode + "-" + f.name + "-" + suffix,
policyMode: mode,
fixture: f,
bypass: bypass,
wantStdout: "Policy is valid",
}
if f.name == "acl-plus-failing-tests" {
r.wantErr = "test(s) failed"
r.wantStdout = ""
}
rows = append(rows, r)
for _, f := range fixtures {
for _, bypass := range bypasses {
suffix := "no-bypass"
if bypass {
suffix = "bypass"
}
r := row{
name: f.name + "-" + suffix,
fixture: f,
bypass: bypass,
wantStdout: "Policy is valid",
}
if f.name == "acl-plus-failing-tests" {
r.wantErr = "test(s) failed"
r.wantStdout = ""
}
rows = append(rows, r)
}
}
for _, tt := range rows {
t.Run(tt.name, func(t *testing.T) {
// Use a live policy distinct from every check fixture so an accidental
// policy update is observable, including for the ACL-only fixture.
livePolicy := policyv2.Policy{
ACLs: []policyv2.ACL{
{
Action: policyv2.ActionAccept,
Sources: []policyv2.Alias{wildcard()},
Destinations: []policyv2.AliasWithPorts{
aliasWithPorts(wildcard(), tailcfg.PortRangeAny),
},
},
},
}
livePolicyBytes, err := json.Marshal(livePolicy)
require.NoError(t, err)
for _, mode := range modes {
t.Run(string(mode), func(t *testing.T) {
spec := ScenarioSpec{
NodesPerUser: 1,
Users: []string{"user1", "user2"}, //nolint:goconst // matches usernamep("user1@")/("user2@") above
@@ -126,45 +139,62 @@ func TestPolicyCheckCommand(t *testing.T) {
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
t.Cleanup(func() { scenario.ShutdownAssertNoPanics(t) })
err = scenario.CreateHeadscaleEnv(
[]tsic.Option{},
hsic.WithTestName("cli-policycheck"),
hsic.WithConfigEnv(map[string]string{
"HEADSCALE_POLICY_MODE": tt.policyMode, //nolint:goconst // env var name from hscontrol/types/config.go
}),
hsic.WithPolicyMode(mode),
hsic.WithACLPolicy(&livePolicy),
)
require.NoError(t, err)
headscale, err := scenario.Headscale()
require.NoError(t, err)
pBytes, err := json.Marshal(tt.fixture.policy)
require.NoError(t, err)
require.EventuallyWithT(t, func(c *assert.CollectT) {
stdout, err := headscale.Execute([]string{"headscale", "policy", "get"})
assert.NoError(c, err)
assert.JSONEq(c, string(livePolicyBytes), stdout)
}, integrationutil.StatusReadyTimeout, integrationutil.FastPoll, "live policy should be loaded before checks")
policyFilePath := "/etc/headscale/policy.json" //nolint:goconst // standard headscale policy path
err = headscale.WriteFile(policyFilePath, pBytes)
require.NoError(t, err)
for _, tt := range rows {
t.Run(tt.name, func(t *testing.T) {
pBytes, err := json.Marshal(tt.fixture.policy)
require.NoError(t, err)
cmd := []string{"headscale", "policy", "check", "-f", policyFilePath} //nolint:goconst // CLI invocation
if tt.bypass {
// --force suppresses the "is the server running?"
// confirmation prompt so the command can run
// non-interactively under the test harness.
cmd = append(cmd, "--bypass-server-and-access-database-directly", "--force")
policyFilePath := "/etc/headscale/policy-check-" + tt.name + ".json"
err = headscale.WriteFile(policyFilePath, pBytes)
require.NoError(t, err)
t.Cleanup(func() {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
stdout, err := headscale.Execute([]string{"headscale", "policy", "get"})
assert.NoError(c, err)
assert.JSONEq(c, string(livePolicyBytes), stdout)
}, integrationutil.StatusReadyTimeout, integrationutil.FastPoll, "policy check must not alter the live policy")
})
cmd := []string{"headscale", "policy", "check", "-f", policyFilePath} //nolint:goconst // CLI invocation
if tt.bypass {
// --force suppresses the "is the server running?"
// confirmation prompt so the command can run
// non-interactively under the test harness.
cmd = append(cmd, "--bypass-server-and-access-database-directly", "--force")
}
stdout, err := headscale.Execute(cmd)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
require.Contains(t, stdout, tt.wantStdout)
})
}
stdout, err := headscale.Execute(cmd)
if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
require.Contains(t, stdout, tt.wantStdout)
})
}
}