Files
spinifex/tests/e2e/single/tests_test.go
T
Ben Duncan 7ddcac53bb feat: block outbound SMTP egress via platform OVN ACL (#1001)
* feat: block outbound SMTP egress via platform OVN ACL

Add a platform egress ACL that drops outbound TCP to 25/465/587 for public
destinations on every guest, matching AWS's default outbound-SMTP block. The
control is global by default with an operator-held per-VPC exemption; tenants
cannot open the ports via a security group because the block sits above the
tenant-allow priority band. Private destinations are always exempt.

Config exposes blocked_ports_wan (nil defaults to [25,465,587], [] disables)
and egress_block_exempt_vpcs. Adds unit tests for the ACL builder, the SG
manager wiring and the config resolution, plus an e2e that digs gmail.com MX
and confirms a 5s SMTP connect is blocked while other WAN egress works.

* docs: note the multi-node sync + restart requirement for the egress block
2026-09-04 16:45:26 -05:00

152 lines
5.1 KiB
Go

//go:build e2e
package single
import "testing"
// Top-level Test* entry points. Each delegates to a runX function in the
// matching <name>_test.go file. Every run* self-bootstraps prerequisites so
// `go test -run TestX` works in isolation.
//
// Parallel bucket (t.Parallel): read-only or own-everything tests that never
// mutate the singleton instance, default VPC, or default SG.
// Sequential: tests that boot, mutate, or snapshot the singleton VM; IAM tests
// that share package-scoped IAM state.
// --- Bucket #1: parallel-safe ---
func TestEnvironment(t *testing.T) {
t.Parallel()
runEnvironment(t, requireSingleNodeFixture(t))
}
func TestDiscovery(t *testing.T) {
t.Parallel()
runDiscovery(t, requireSingleNodeFixture(t))
}
func TestAccountScoping(t *testing.T) {
t.Parallel()
runAccountScoping(t, requireSingleNodeFixture(t))
}
// --- Sequential: singleton VM lifecycle ---
// TestClusterStatsCLI exercises the spx cluster CLI; its get-vms baseline is
// concurrency-tolerant (other suites may have VMs on the node).
func TestClusterStatsCLI(t *testing.T) {
runClusterStatsCLI(t, requireSingleNodeFixture(t))
}
// TestSGReachabilityPolicy and TestVPCEgressPaths own all mutable resources
// and run before the singleton launch.
//
// TestSGReachabilityPolicy merges the former TestDefaultSGReachabilityBaseline,
// TestGuestDNSResolution, and TestSecurityGroupEgress around one shared guest
// (see runSGReachabilityPolicy for the stage breakdown and gating rationale).
func TestSGReachabilityPolicy(t *testing.T) {
runSGReachabilityPolicy(t, requireSingleNodeFixture(t))
}
// TestVPCEgressPaths merges the former TestVPCSubnetE2E,
// TestNewVPCEgressBaseline, TestNATGateway, and TestInstanceEIP around one
// scenario-owned VPC and its two guests (see runVPCEgressPaths for the stage
// breakdown and gating rationale).
func TestVPCEgressPaths(t *testing.T) {
runVPCEgressPaths(t, requireSingleNodeFixture(t))
}
// TestWANEgressSMTPBlock boots one public guest and asserts the platform
// egress ACL drops outbound SMTP (25/465/587) to a public destination while
// ordinary WAN egress still works — the AWS-parity default block.
func TestWANEgressSMTPBlock(t *testing.T) {
runWANEgressSMTPBlock(t, requireSingleNodeFixture(t))
}
func TestInstanceClusterStats(t *testing.T) {
runInstanceClusterStats(t, requireSingleNodeFixture(t))
}
func TestInstanceMetadata(t *testing.T) {
runInstanceMetadata(t, requireSingleNodeFixture(t))
}
func TestConsoleOutput(t *testing.T) {
runConsoleOutput(t, requireSingleNodeFixture(t))
}
func TestVolumeLifecycle(t *testing.T) {
runVolumeLifecycle(t, requireSingleNodeFixture(t))
}
func TestVolumeStatus(t *testing.T) {
runVolumeStatus(t, requireSingleNodeFixture(t))
}
// TestEarlyRebootLiveness owns a fresh guest so the reset lands as soon as
// cloud-init.target is observable rather than against the old singleton.
func TestEarlyRebootLiveness(t *testing.T) {
runEarlyRebootLiveness(t, requireSingleNodeFixture(t))
}
// TestGuestChurnDurability merges guest churn around one shared instance and
// data-integrity sentinel. Sequential: it leaves the singleton running.
func TestGuestChurnDurability(t *testing.T) {
runGuestChurnDurability(t, requireSingleNodeFixture(t))
}
func TestSnapshotLifecycle(t *testing.T) {
runSnapshotLifecycle(t, requireSingleNodeFixture(t))
}
func TestSnapshotBackedLaunch(t *testing.T) {
runSnapshotBackedLaunch(t, requireSingleNodeFixture(t))
}
func TestCreateImage(t *testing.T) {
runCreateImage(t, requireSingleNodeFixture(t))
}
func TestStopStart(t *testing.T) {
runStopStart(t, requireSingleNodeFixture(t))
}
func TestAttachToStoppedError(t *testing.T) {
runAttachToStoppedError(t, requireSingleNodeFixture(t))
}
// TestSpotInstanceLifecycle drives the two spot terminal transitions that need
// a real backing VM: cancel-keeps-running and terminate-drives-the-real-close
// chain. Sequential: it consumes node capacity for its own short-lived VMs
// like the other singleton-lifecycle tests.
func TestSpotInstanceLifecycle(t *testing.T) {
runSpotInstanceLifecycle(t, requireSingleNodeFixture(t))
}
// TestLaunchTemplateBoot launches one instance from a launch template alone
// and asserts the template's fields reached the real VM. Sequential: it
// consumes node capacity for its own two short-lived VMs.
func TestLaunchTemplateBoot(t *testing.T) {
runLaunchTemplateBoot(t, requireSingleNodeFixture(t))
}
// TestInstanceStatus drives DescribeInstanceStatus and the real SDK
// instance-status-ok waiter. Sequential: it launches, stops and terminates its
// own short-lived guest, which the initializing → ok transition requires.
func TestInstanceStatus(t *testing.T) {
runInstanceStatus(t, requireSingleNodeFixture(t))
}
// --- Sequential: shared-state / sub-test-parallel Tests ---
func TestNegativeErrorPaths(t *testing.T) {
runNegativeErrorPaths(t, requireSingleNodeFixture(t))
}
// TestSGPolicyDatapath merges the former TestSameSGComms and
// TestSGToSGDatapath around one shared client/target pair (see
// runSGPolicyDatapath for the stage breakdown and gating rationale).
func TestSGPolicyDatapath(t *testing.T) {
runSGPolicyDatapath(t, requireSingleNodeFixture(t))
}