refactor(e2e/single): drop phase numbers + parallel bucket #1 (#247)

- Rename 36 phaseN_X / phaseIAM_N funcs to runX.
- Drop numeric suffix from 7 TestIAMN_X wrappers (TestIAM1_UserCRUD -> TestIAMUserCRUD).
- Rewrite harness.Phase log strings "Phase N - X" -> "Single - X" to mirror multinode convention.
- Add t.Parallel() to bucket #1: 10 read-only / own-everything top-level Tests (Environment, ClusterStatsCLI, Discovery, SerialConsoleAccess, KeyPairs, Image, TagManagement, AccountScoping, VPCSubnetE2E, RouteTableValidation). Sequential bucket retains source-order singleton lifecycle (InstanceLaunch through FinalClusterStats).
This commit is contained in:
Josh Siviour
2026-05-21 22:47:26 +10:00
committed by GitHub
parent c89a24ab0c
commit 40f8c4da94
18 changed files with 291 additions and 210 deletions
+3 -3
View File
@@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/require"
)
// phase8Acct_AccountScoping ports run-e2e.sh Phase 8 EC2 Account Scoping
// runAccountScoping ports run-e2e.sh Phase 8 EC2 Account Scoping
// (lines 1915–2624). Validates that EC2 resources (instances, volumes, key
// pairs, snapshots, VPCs, IGWs, EIGWs) are isolated between tenant accounts
// created via `spx admin account create`.
@@ -41,8 +41,8 @@ import (
// Step9_GlobalResources — regions/AZs/instance types identical
// Step10_EdgeCases — empty Gamma + non-existent resource ids
// Step11_Cleanup — explicit best-effort teardown (also runs via t.Cleanup)
func phase8Acct_AccountScoping(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 8 (acct) — EC2 Account Scoping")
func runAccountScoping(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — EC2 Account Scoping")
amiID := needAMI(t, fix)
instType, _ := needInstanceTypeArch(t, fix)
+3 -3
View File
@@ -12,12 +12,12 @@ import (
"github.com/stretchr/testify/require"
)
// phase5e_CreateImage builds a custom AMI from the running instance and
// runCreateImage builds a custom AMI from the running instance and
// records the backing snapshot ID so Phase 9 cleanup can delete the
// snapshot before terminating the instance (otherwise DeleteOnTermination
// trips over the still-referenced snapshot). Maps to run-e2e.sh ~805–844.
func phase5e_CreateImage(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 5e — CreateImage Lifecycle")
func runCreateImage(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — CreateImage Lifecycle")
inst, _ := needInstance(t, fix)
instanceID := aws.StringValue(inst.InstanceId)
+3 -3
View File
@@ -26,7 +26,7 @@ systemd-run --unit=sge-http --description="Phase 8e HTTP server" \
/usr/bin/python3 -m http.server 8080 --bind 0.0.0.0
`
// phase8e_SGToSGDatapath validates the closed loop from RunInstances
// runSGToSGDatapath validates the closed loop from RunInstances
// (--security-group-ids) -> ENI -> vpcd -> OVN port group + ACL -> datapath
// drop. Creates two SGs (client / target) with an SG-to-SG ingress rule for
// tcp/8080, launches one VM in each, then exercises:
@@ -42,8 +42,8 @@ systemd-run --unit=sge-http --description="Phase 8e HTTP server" \
//
// OVN gated — skipped on dev laptops without ovn-nbctl/sudo. Maps to
// run-e2e.sh ~3188-3442.
func phase8e_SGToSGDatapath(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 8e — Security Group SG-to-SG Datapath (OVN)")
func runSGToSGDatapath(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Security Group SG-to-SG Datapath (OVN)")
harness.SkipIfNoOVN(t)
// Bootstrap every prereq up front. runSGEInstance / primaryENI use
+12 -12
View File
@@ -16,11 +16,11 @@ import (
"github.com/stretchr/testify/require"
)
// phase1_Environment verifies KVM, AWS gateway TLS reachability, daemon NATS
// runEnvironment verifies KVM, AWS gateway TLS reachability, daemon NATS
// readiness, and the basic region/AZ discovery calls. Maps to run-e2e.sh
// lines ~51–93.
func phase1_Environment(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 1 — Environment")
func runEnvironment(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Environment")
// /dev/kvm — skip rather than fatal in local-dev to match the bash
// "exit 1" only when KVM is genuinely required (CI). Local devs without
@@ -88,11 +88,11 @@ func phase1_Environment(t *testing.T, fix *Fixture) {
})
}
// phase1b_ClusterStats exercises the spx CLI cluster surface (`get nodes`,
// runClusterStatsCLI exercises the spx CLI cluster surface (`get nodes`,
// `top nodes`, `get vms`). Single-node only — multinode mode is tested by a
// different scenario. Maps to run-e2e.sh ~95–127.
func phase1b_ClusterStats(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 1b — Cluster Stats CLI")
func runClusterStatsCLI(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Cluster Stats CLI")
if fix.Env.Mode != harness.ModeSingle {
t.Skipf("Phase 1b is single-node only (mode=%s)", fix.Env.Mode)
}
@@ -114,12 +114,12 @@ func phase1b_ClusterStats(t *testing.T, fix *Fixture) {
"spx get vms should be empty before Phase 5\n%s", vms)
}
// phase2_Discovery re-asserts describe-regions / describe-availability-zones
// runDiscovery re-asserts describe-regions / describe-availability-zones
// (cheap but it's where the bash records them as a phase boundary) and picks
// the nano instance type + architecture used throughout the rest of the run.
// Maps to run-e2e.sh ~128–164.
func phase2_Discovery(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 2 — Discovery & Metadata")
func runDiscovery(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Discovery Phase 2 — Discovery & Metadata Metadata")
regions, err := fix.AWS.EC2.DescribeRegions(&ec2.DescribeRegionsInput{})
require.NoError(t, err)
@@ -137,11 +137,11 @@ func phase2_Discovery(t *testing.T, fix *Fixture) {
harness.Detail(t, "instance_type", instType, "arch", arch, "az", az)
}
// phase2b_SerialConsole flips serial-console-access on then off and verifies
// runSerialConsoleAccess flips serial-console-access on then off and verifies
// each transition with both the action-returned bool and a follow-up
// get-status round-trip. Maps to run-e2e.sh ~166–202.
func phase2b_SerialConsole(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 2b — Serial Console Access")
func runSerialConsoleAccess(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Serial Console Access")
harness.Step(t, "default state should be disabled")
if got := harness.GetSerialConsoleAccessEnabled(t, fix.AWS); got {
+21 -21
View File
@@ -92,15 +92,15 @@ const (
}`
)
// phaseIAM1_UserCRUD ports run-e2e.sh IAM Phase 1 (~1405–1459):
// runIAMUserCRUD ports run-e2e.sh IAM Phase 1 (~1405–1459):
// CreateUser, GetUser, ListUsers, UpdateUser (rename+path), DeleteUser
// plus the EntityAlreadyExists / NoSuchEntity negative paths. Each
// created user is scheduled for cleanup so a mid-phase failure can't
// poison later runs. UpdateUser exercises an out-of-band rename +
// rename-back so Phase 2+ continue to find "alice" by the well-known
// name.
func phaseIAM1_UserCRUD(t *testing.T, fix *Fixture) {
harness.Phase(t, "IAM Phase 1 — User CRUD")
func runIAMUserCRUD(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — IAM User CRUD")
// Defensive: drop leftovers from a prior failed run before recreate.
iamDeleteUserBestEffort(fix, iamUserAlice)
@@ -217,13 +217,13 @@ func phaseIAM1_UserCRUD(t *testing.T, fix *Fixture) {
})
}
// phaseIAM2_AccessKeyLifecycle ports IAM Phase 2 (~1463–1534):
// runIAMAccessKeyLifecycle ports IAM Phase 2 (~1463–1534):
// CreateAccessKey, ListAccessKeys, UpdateAccessKey (deactivate/activate),
// DeleteAccessKey plus LimitExceeded / NoSuchEntity negative paths.
// Captures alice's primary key into the Fixture so Phase 3 can sign
// requests with it.
func phaseIAM2_AccessKeyLifecycle(t *testing.T, fix *Fixture) {
harness.Phase(t, "IAM Phase 2 — Access Key Lifecycle")
func runIAMAccessKeyLifecycle(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — IAM Access Key Lifecycle")
iamEnsureAlice(t, fix)
iamEnsureBob(t, fix)
@@ -315,13 +315,13 @@ func phaseIAM2_AccessKeyLifecycle(t *testing.T, fix *Fixture) {
require.Len(t, after.AccessKeyMetadata, 1, "alice should have 1 key after delete")
}
// phaseIAM3_UserAuthentication ports IAM Phase 3 (~1538–1579):
// runIAMUserAuthentication ports IAM Phase 3 (~1538–1579):
// build a scoped AWS client with alice's key and confirm signing
// works (active key) / fails (deactivated key, bad secret, bogus ID).
// Also creates bob's key so Phase 5 enforcement / Phase 7 cleanup
// can use it.
func phaseIAM3_UserAuthentication(t *testing.T, fix *Fixture) {
harness.Phase(t, "IAM Phase 3 — User Authentication")
func runIAMUserAuthentication(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — IAM User Authentication")
// Daemon does not yet honour active-key signatures created via the IAM
// API — every scoped DescribeInstances returns 403. Skip-gate until
// the handler lands; mulga-siv-100 tracks the daemon-side work.
@@ -375,14 +375,14 @@ func phaseIAM3_UserAuthentication(t *testing.T, fix *Fixture) {
require.NoError(t, err, "root describe-instances")
}
// phaseIAM4_PolicyCRUD ports IAM Phase 4 (~1583–1698):
// runIAMPolicyCRUD ports IAM Phase 4 (~1583–1698):
// CreatePolicy (5 variants), CreatePolicyVersion / ListPolicyVersions /
// SetDefaultPolicyVersion / DeletePolicyVersion (extends bash, per
// task spec), GetPolicy, GetPolicyVersion, ListPolicies plus
// EntityAlreadyExists / MalformedPolicyDocument / NoSuchEntity errors.
// Captures the admin account ID into the Fixture for Phase 5–7.
func phaseIAM4_PolicyCRUD(t *testing.T, fix *Fixture) {
harness.Phase(t, "IAM Phase 4 — Policy CRUD")
func runIAMPolicyCRUD(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — IAM Policy CRUD")
// Defensive: drop any leftover test policies from a prior failed run.
// Detach-then-delete isn't worth the round trips here — the cleanup
@@ -553,12 +553,12 @@ func phaseIAM4_PolicyCRUD(t *testing.T, fix *Fixture) {
"expected >=5 policies, got %d (%v)", len(all.Policies), iamPolicyNames(all.Policies))
}
// phaseIAM5_PolicyAttachmentEnforcement ports IAM Phase 5 (~1702–1833):
// runIAMPolicyAttachmentEnforcement ports IAM Phase 5 (~1702–1833):
// AttachUserPolicy idempotency, ListAttachedUserPolicies, enforcement
// (default deny / explicit allow / wildcard / explicit deny / root
// bypass / prefix wildcard / FullAdmin), DetachUserPolicy.
func phaseIAM5_PolicyAttachmentEnforcement(t *testing.T, fix *Fixture) {
harness.Phase(t, "IAM Phase 5 — Policy Attachment & Enforcement")
func runIAMPolicyAttachmentEnforcement(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — IAM Policy Attachment IAM Phase 5 — Policy Attachment & Enforcement Enforcement")
// Enforcement depends on scoped-credential signing (Phase 3) which the
// daemon doesn't honour yet — mulga-siv-100. Skip-gate the whole phase
// until the upstream gap closes.
@@ -727,12 +727,12 @@ func phaseIAM5_PolicyAttachmentEnforcement(t *testing.T, fix *Fixture) {
require.NoError(t, err, "charlie iam:ListUsers after FullAdmin")
}
// phaseIAM6_PolicyLifecycle ports IAM Phase 6 (~1837–1864):
// runIAMPolicyLifecycle ports IAM Phase 6 (~1837–1864):
// detach alice's policy and confirm she loses access; deleting a
// still-attached policy must surface DeleteConflict; after detach +
// delete the policy is gone (NoSuchEntity).
func phaseIAM6_PolicyLifecycle(t *testing.T, fix *Fixture) {
harness.Phase(t, "IAM Phase 6 — Policy Lifecycle (Detach & Delete)")
func runIAMPolicyLifecycle(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — IAM Policy Lifecycle (Detach IAM Phase 6 — Policy Lifecycle (Detach & Delete) Delete)")
// DetachUserPolicy returns 404 NoSuchEntity even for attachments
// confirmed by Phase 5 — daemon's attachment ledger isn't persisted
// across the API boundary. Skip-gate; mulga-siv-100 tracks the fix.
@@ -776,15 +776,15 @@ func phaseIAM6_PolicyLifecycle(t *testing.T, fix *Fixture) {
})
}
// phaseIAM7_Cleanup ports IAM Phase 7 (~1868–1911): tear down every
// runIAMCleanup ports IAM Phase 7 (~1868–1911): tear down every
// user, key, and policy created by Phases 1–6 even if intermediate
// phases failed. Each step is best-effort so a missing resource
// (already torn down or never created) doesn't fail the cleanup pass.
// The final list-users / list-policies assertions are commented
// expectations rather than hard checks because Phase 7 may run with
// residual state from a partial Phase 1–6 failure.
func phaseIAM7_Cleanup(t *testing.T, fix *Fixture) {
harness.Phase(t, "IAM Phase 7 — Cleanup")
func runIAMCleanup(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — IAM Cleanup")
// Resolve account ID before deleting users — iamEnsureAdminAccountID
// probes via GetUser(alice), and the user is gone after the loop below.
+3 -3
View File
@@ -11,12 +11,12 @@ import (
"github.com/stretchr/testify/require"
)
// phase4_Image rediscovers the architecture-appropriate Ubuntu AMI that
// runImage rediscovers the architecture-appropriate Ubuntu AMI that
// bootstrap-install.sh imported from the pre-staged gold-image file
// (~/images/ubuntu-{26,24}.04.img → `spx admin images import --file`) and
// stashes the ID on the fixture for Phase 5+. Maps to run-e2e.sh ~233–255.
func phase4_Image(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 4 — Image Management")
func runImage(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Image Management")
// Resolve the gold-image AMI via the cluster discovery helper. The first
// caller pays the DescribeImages + state-poll cost; this and every other
+15 -15
View File
@@ -17,12 +17,12 @@ import (
"github.com/stretchr/testify/require"
)
// phase5_LaunchInstance bootstraps the suite's primary instance and asserts
// runInstanceLaunch bootstraps the suite's primary instance and asserts
// that the live row exposes the AMI / type / key the launch was given.
// Delegates the launch + memoization to needInstance so any sibling Test*
// hits the same cached row. Maps to run-e2e.sh ~257–343.
func phase5_LaunchInstance(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 5 — Instance Lifecycle")
func runInstanceLaunch(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Instance Lifecycle")
def := harness.EnsureDefaultVPC(t, fix.Harness)
require.NotEmpty(t, def.SGID, "default SG ID required")
@@ -35,11 +35,11 @@ func phase5_LaunchInstance(t *testing.T, fix *Fixture) {
"root_device", aws.StringValue(inst.RootDeviceName))
}
// phase5aPre_ClusterStats re-runs `spx get vms` now that a VM is running.
// runInstanceClusterStats re-runs `spx get vms` now that a VM is running.
// Single-node only — multinode uses a different cluster surface. Maps to
// run-e2e.sh ~345–353.
func phase5aPre_ClusterStats(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 5a-pre — Cluster Stats CLI (with running VM)")
func runInstanceClusterStats(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Cluster Stats CLI (with running VM)")
if fix.Env.Mode != harness.ModeSingle {
t.Skipf("Phase 5a-pre is single-node only (mode=%s)", fix.Env.Mode)
}
@@ -52,10 +52,10 @@ func phase5aPre_ClusterStats(t *testing.T, fix *Fixture) {
"spx get vms should list running instance %s\n%s", instanceID, vms)
}
// phase5a_Metadata round-trips DescribeInstances and asserts the basic
// runInstanceMetadata round-trips DescribeInstances and asserts the basic
// metadata fields match what RunInstances saw. Maps to run-e2e.sh ~355–379.
func phase5a_Metadata(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 5a — Instance Metadata Validation")
func runInstanceMetadata(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Instance Metadata Validation")
want, _ := needInstance(t, fix)
instanceID := aws.StringValue(want.InstanceId)
@@ -85,11 +85,11 @@ func phase5a_Metadata(t *testing.T, fix *Fixture) {
)
}
// phase5aii_SSHProbe waits for SSH to become reachable, then runs `id`,
// runSSHProbe waits for SSH to become reachable, then runs `id`,
// `lsblk`, and `hostname` against the guest to confirm the VM booted and
// presents the expected root volume. Maps to run-e2e.sh ~381–463.
func phase5aii_SSHProbe(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 5a-ii — SSH Connectivity & Volume Verification")
func runSSHProbe(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — SSH Connectivity Phase 5a-ii — SSH Connectivity & Volume Verification Volume Verification")
inst, rootVolumeID := needInstance(t, fix)
_, keyPath := needKeyPair(t, fix)
@@ -147,11 +147,11 @@ func phase5aii_SSHProbe(t *testing.T, fix *Fixture) {
}
}
// phase5aiii_ConsoleOutput verifies GetConsoleOutput round-trips the
// runConsoleOutput verifies GetConsoleOutput round-trips the
// instance ID. The payload itself is base64 cloud-init output; bash doesn't
// decode it and neither do we. Maps to run-e2e.sh ~465–478.
func phase5aiii_ConsoleOutput(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 5a-iii — Console Output")
func runConsoleOutput(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Console Output")
inst, _ := needInstance(t, fix)
instanceID := aws.StringValue(inst.InstanceId)
+3 -3
View File
@@ -17,13 +17,13 @@ import (
"golang.org/x/crypto/ssh"
)
// phase3_KeyPairs exercises CreateKeyPair / ImportKeyPair / DescribeKeyPairs /
// runKeyPairs exercises CreateKeyPair / ImportKeyPair / DescribeKeyPairs /
// DeleteKeyPair. The primary key pair is materialised via harness.EnsureKeyPair
// — it owns the create path's assertions, memoizes for downstream Phase 5+
// callers, and registers TestSingleNode-scoped cleanup. test-key-2 is created
// via import then deleted within this phase. Maps to run-e2e.sh ~204–231.
func phase3_KeyPairs(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 3 — SSH Key Management")
func runKeyPairs(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — SSH Key Management")
const key2 = "test-key-2"
+15 -15
View File
@@ -18,13 +18,13 @@ import (
"github.com/stretchr/testify/require"
)
// phase7_StopStart stops fix.Instance and waits for it to reach the
// runStopStart stops fix.Instance and waits for it to reach the
// "stopped" state, then asserts that rebooting a stopped instance is
// rejected with IncorrectInstanceState. Leaves the instance stopped so
// Phase 7a / 7b can act against the stopped state. Maps to run-e2e.sh
// ~1027–1053.
func phase7_StopStart(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 7 — Instance State Transitions (Stop)")
func runStopStart(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Instance State Transitions (Stop)")
inst, _ := needInstance(t, fix)
instanceID := aws.StringValue(inst.InstanceId)
@@ -58,13 +58,13 @@ func phase7_StopStart(t *testing.T, fix *Fixture) {
harness.WaitForInstanceState(t, fix.AWS, instanceID, "running")
}
// phase7a_AttachToStoppedError creates a fresh 10 GiB volume and attempts
// runAttachToStoppedError creates a fresh 10 GiB volume and attempts
// to attach it to the (currently stopped) primary instance. The attach
// must fail with IncorrectInstanceState. The test volume is deleted
// in-line on success and via t.Cleanup on early failure. Maps to
// run-e2e.sh ~1054–1068.
func phase7a_AttachToStoppedError(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 7a — Attach Volume to Stopped Instance (Error Path)")
func runAttachToStoppedError(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Attach Volume to Stopped Instance (Error Path)")
inst, _ := needInstance(t, fix)
instanceID := aws.StringValue(inst.InstanceId)
@@ -128,13 +128,13 @@ func phase7a_AttachToStoppedError(t *testing.T, fix *Fixture) {
cleaned = true
}
// phase7b_ModifyInstanceAttribute changes the (stopped) instance type to
// runModifyInstanceAttribute changes the (stopped) instance type to
// the same-family ".small" upsize, starts the instance, SSHes in to
// verify the new vCPU + memory budget, then stops the instance again so
// Phase 7c-pre can drive its own start/reboot cycle. Maps to run-e2e.sh
// ~1070–1177.
func phase7b_ModifyInstanceAttribute(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 7b — ModifyInstanceAttribute")
func runModifyInstanceAttribute(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — ModifyInstanceAttribute")
inst, _ := needInstance(t, fix)
instanceID := aws.StringValue(inst.InstanceId)
@@ -252,14 +252,14 @@ func phase7b_ModifyInstanceAttribute(t *testing.T, fix *Fixture) {
// Cleanup restores original type + running state for sibling Test*.
}
// phase7cPre_Reboot starts the (stopped) primary instance, captures its
// runRebootInstance starts the (stopped) primary instance, captures its
// pre-reboot private IP, issues a reboot, asserts the API never reports a
// non-running state during a short polling window, waits for SSH to come
// back, and verifies the guest actually rebooted (uptime < 120s) without
// changing its private IP. Leaves the instance running. Maps to
// run-e2e.sh ~1178–1236.
func phase7cPre_Reboot(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 7c-pre — Reboot Running Instance")
func runRebootInstance(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Reboot Running Instance")
inst, _ := needInstance(t, fix)
instanceID := aws.StringValue(inst.InstanceId)
@@ -326,13 +326,13 @@ func phase7cPre_Reboot(t *testing.T, fix *Fixture) {
// and Phase 8 expects the primary instance to still be up.
}
// phase7c_RunInstancesMultiCount launches 2 sibling instances in a single
// runRunInstancesMultiCount launches 2 sibling instances in a single
// RunInstances call, waits for both to reach "running", then terminates
// them and waits for both to reach "terminated". The primary fix.Instance
// is untouched and remains running for Phase 8. Maps to run-e2e.sh
// ~1238–1296.
func phase7c_RunInstancesMultiCount(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 7c — RunInstances with MinCount/MaxCount > 1")
func runRunInstancesMultiCount(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — RunInstances with MinCount/MaxCount > 1")
amiID := needAMI(t, fix)
instType, _ := needInstanceTypeArch(t, fix)
+3 -3
View File
@@ -16,7 +16,7 @@ import (
"github.com/stretchr/testify/require"
)
// phase8d_NATGateway ports run-e2e.sh ~3017–3161 (Phase 8d).
// runNATGateway ports run-e2e.sh ~3017–3161 (Phase 8d).
//
// Stands up its own bastion + private-instance pair in the default VPC,
// allocates a NAT Gateway in the default (public) subnet, attaches a
@@ -30,11 +30,11 @@ import (
// sibling Stage F file). Everything launched here is torn down LIFO by
// the registered t.Cleanup so a failure mid-flight still releases the
// EIP and terminates both VMs.
func phase8d_NATGateway(t *testing.T, fix *Fixture) {
func runNATGateway(t *testing.T, fix *Fixture) {
if !fix.PoolMode {
t.Skip("Phase 8d requires pool-mode networking")
}
harness.Phase(t, "Phase 8d — NAT Gateway E2E")
harness.Phase(t, "Single — NAT Gateway E2E")
amiID := needAMI(t, fix)
instType, _ := needInstanceTypeArch(t, fix)
+3 -3
View File
@@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/require"
)
// phase8_NegativeErrorPaths ports run-e2e.sh Phase 8 (lines ~1298–1399):
// runNegativeErrorPaths ports run-e2e.sh Phase 8 (lines ~1298–1399):
// every sub-test exercises a single error code on the gateway. Each case
// runs in its own t.Run so one failure doesn't poison the others; cleanup
// is defensive (calls that unexpectedly succeed get torn down).
@@ -36,8 +36,8 @@ import (
// 8p (idempotent) — DeleteKeyPair for unknown key must succeed
// 8q InvalidInstanceID.NotFound — ModifyInstanceAttribute on running inst
// 8r InvalidInstanceID.NotFound — RebootInstances on unknown id
func phase8_NegativeErrorPaths(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 8 — Negative / Error Path Tests")
func runNegativeErrorPaths(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Negative / Error Path Tests")
// Bootstrap every prereq up front so the parallel sub-tests below all
// see populated locals.
+3 -3
View File
@@ -26,12 +26,12 @@ var pingReceivedRE = regexp.MustCompile(`[1-3] (packets )?received`)
// `0 received` or `100% packet loss`. Mirrors the bash grep pattern.
var pingDroppedRE = regexp.MustCompile(`0 (packets )?received|100% packet loss`)
// phase5f_SecurityGroupEgress flips the default SG's allow-all egress rule and
// runSecurityGroupEgress flips the default SG's allow-all egress rule and
// verifies vpcd programs OVN ACLs that actually drop traffic. Egress is tested
// because in dev_networking mode ingress SSH bypasses OVN via QEMU hostfwd —
// only egress hits the ACL. Maps to run-e2e.sh ~846–928.
func phase5f_SecurityGroupEgress(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 5f — Security Group Enforcement (egress ACL)")
func runSecurityGroupEgress(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Security Group Enforcement (egress ACL)")
inst, _ := needInstance(t, fix)
_, keyPath := needKeyPair(t, fix)
+6 -6
View File
@@ -14,12 +14,12 @@ import (
"github.com/stretchr/testify/require"
)
// phase5c_SnapshotLifecycle creates, describes, copies, and deletes a
// runSnapshotLifecycle creates, describes, copies, and deletes a
// snapshot off the running instance's root volume (the only volume that's
// definitely backed by a mounted viperblock instance — create-snapshot
// requires that). Maps to run-e2e.sh ~627–786.
func phase5c_SnapshotLifecycle(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 5c — Snapshot Lifecycle")
func runSnapshotLifecycle(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Snapshot Lifecycle")
_, rootVolumeID := needInstance(t, fix)
@@ -140,14 +140,14 @@ func phase5c_SnapshotLifecycle(t *testing.T, fix *Fixture) {
harness.Detail(t, "deleted_original", snapshotID)
}
// phase5d_SnapshotBackedLaunch verifies the AMI used for the live instance
// runSnapshotBackedLaunch verifies the AMI used for the live instance
// carries a snapshot reference — proof the launch went through the
// cloneAMIToVolume → OpenFromSnapshot path. Maps to run-e2e.sh ~788–803.
//
// Bash's prose mentions verifying the predastore-side snapshot config, but
// the actual bash only checks the EC2 API. We follow the bash.
func phase5d_SnapshotBackedLaunch(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 5d — Snapshot-Backed Instance Launch")
func runSnapshotBackedLaunch(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Snapshot-Backed Instance Launch")
amiID := needAMI(t, fix)
+3 -3
View File
@@ -12,14 +12,14 @@ import (
"github.com/stretchr/testify/require"
)
// phase6_TagManagement exercises CreateTags / DescribeTags / DeleteTags
// runTagManagement exercises CreateTags / DescribeTags / DeleteTags
// against the Phase 5 instance and its root volume. Maps to run-e2e.sh
// ~930–1026. Sub-steps mirror the bash 6a–6j checks; we additionally
// assert in 6i that the value-conditional DeleteTags scoped to the
// instance does NOT touch the matching tag on the root volume, which the
// bash driver only implies via per-resource targeting.
func phase6_TagManagement(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 6 — Tag Management")
func runTagManagement(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Tag Management")
inst, rootVolumeID := needInstance(t, fix)
instanceID := aws.StringValue(inst.InstanceId)
+3 -3
View File
@@ -9,12 +9,12 @@ import (
"github.com/mulgadc/spinifex/tests/e2e/harness"
)
// phase9b_FinalClusterStats is a single-node sanity pass against `spx get
// runFinalClusterStats is a single-node sanity pass against `spx get
// vms`. Replaces the umbrella's phase9 + phase9a (deleted) — resource
// cleanup is now owned by harness.Fixture.Close, invoked from TestMain
// after every Test* in the package has run.
func phase9b_FinalClusterStats(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 9b — Final Cluster Stats")
func runFinalClusterStats(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Final Cluster Stats")
if fix.Env.Mode != harness.ModeSingle {
t.Skipf("Phase 9b is single-node only (mode=%s)", fix.Env.Mode)
}
+180 -99
View File
@@ -4,122 +4,203 @@ package single
import "testing"
// Top-level Test* entry points. Each delegates to a phaseN_X function in
// this package, passing the package-scoped Fixture singleton.
// Top-level Test* entry points. Each delegates to a runX function in the
// matching <name>_test.go file. Names are number-free (TestX) so isolated
// runs via `go test -run TestKeyPairs` are stable.
//
// Every phase function self-bootstraps any prerequisite resource it needs
// via harness.Discover* / harness.Ensure* (or the package-local need* /
// iamEnsure* helpers), so a targeted `go test -run TestX` run works in
// isolation without depending on a sibling Test* having stashed state.
// Every run* function self-bootstraps prerequisites via harness.Discover* /
// harness.Ensure* (or the package-local need* / iamEnsure* helpers), so a
// targeted `go test -run TestX` invocation works in isolation without
// relying on a sibling Test* having stashed state.
//
// Parallelism (mulga-siv-128):
//
// Bucket #1 (parallel): read-only or own-everything Tests below call
// t.Parallel(). They share the package fixture but never mutate the
// singleton EC2 instance, default VPC, or default SG.
//
// Sequential (no t.Parallel — would race the singleton VM or shared state):
// - TestInstanceLaunch : first sequential — boots singleton.
// - TestInstanceClusterStats / Metadata
// / SSHProbe / ConsoleOutput : read singleton.
// - TestVolumeLifecycle / VolumeStatus : attach to singleton.
// - TestSnapshotLifecycle /
// SnapshotBackedLaunch / CreateImage : snapshot singleton.
// - TestSecurityGroupEgress : mutates default SG.
// - TestStopStart / AttachToStoppedError
// / ModifyInstanceAttribute /
// RebootInstance / RunInstancesMultiCount : mutate singleton state.
// - TestNegativeErrorPaths : sub-tests already parallel inside.
// - TestNATGateway / TestSGToSGDatapath : share EIP pool.
// - All TestIAM* : share IAM policy state via the
// package-scoped IAM fixture; sub-tests inside each already parallel.
// - TestFinalClusterStats : final sanity gate.
// Discovery + environment.
func TestEnvironment(t *testing.T) { phase1_Environment(t, requireSingleNodeFixture(t)) }
func TestClusterStatsCLI(t *testing.T) { phase1b_ClusterStats(t, requireSingleNodeFixture(t)) }
func TestDiscovery(t *testing.T) { phase2_Discovery(t, requireSingleNodeFixture(t)) }
func TestSerialConsoleAccess(t *testing.T) { phase2b_SerialConsole(t, requireSingleNodeFixture(t)) }
// --- Bucket #1: parallel-safe ---
// Key pairs + image.
func TestKeyPairs(t *testing.T) { phase3_KeyPairs(t, requireSingleNodeFixture(t)) }
func TestImage(t *testing.T) { phase4_Image(t, requireSingleNodeFixture(t)) }
// Instance lifecycle.
func TestInstanceLaunch(t *testing.T) {
phase5_LaunchInstance(t, requireSingleNodeFixture(t))
}
func TestInstanceClusterStats(t *testing.T) {
phase5aPre_ClusterStats(t, requireSingleNodeFixture(t))
}
func TestInstanceMetadata(t *testing.T) {
phase5a_Metadata(t, requireSingleNodeFixture(t))
}
func TestSSHProbe(t *testing.T) {
phase5aii_SSHProbe(t, requireSingleNodeFixture(t))
}
func TestConsoleOutput(t *testing.T) {
phase5aiii_ConsoleOutput(t, requireSingleNodeFixture(t))
func TestEnvironment(t *testing.T) {
t.Parallel()
runEnvironment(t, requireSingleNodeFixture(t))
}
// Volume + snapshot + AMI.
func TestVolumeLifecycle(t *testing.T) {
phase5b_VolumeLifecycle(t, requireSingleNodeFixture(t))
}
func TestVolumeStatus(t *testing.T) {
phase5bii_VolumeStatus(t, requireSingleNodeFixture(t))
}
func TestSnapshotLifecycle(t *testing.T) {
phase5c_SnapshotLifecycle(t, requireSingleNodeFixture(t))
}
func TestSnapshotBackedLaunch(t *testing.T) {
phase5d_SnapshotBackedLaunch(t, requireSingleNodeFixture(t))
}
func TestCreateImage(t *testing.T) {
phase5e_CreateImage(t, requireSingleNodeFixture(t))
}
func TestSecurityGroupEgress(t *testing.T) {
phase5f_SecurityGroupEgress(t, requireSingleNodeFixture(t))
func TestClusterStatsCLI(t *testing.T) {
t.Parallel()
runClusterStatsCLI(t, requireSingleNodeFixture(t))
}
// Tags + lifecycle transitions.
func TestTagManagement(t *testing.T) { phase6_TagManagement(t, requireSingleNodeFixture(t)) }
func TestStopStart(t *testing.T) { phase7_StopStart(t, requireSingleNodeFixture(t)) }
func TestAttachToStoppedError(t *testing.T) {
phase7a_AttachToStoppedError(t, requireSingleNodeFixture(t))
}
func TestModifyInstanceAttribute(t *testing.T) {
phase7b_ModifyInstanceAttribute(t, requireSingleNodeFixture(t))
}
func TestRebootInstance(t *testing.T) {
phase7cPre_Reboot(t, requireSingleNodeFixture(t))
}
func TestRunInstancesMultiCount(t *testing.T) {
phase7c_RunInstancesMultiCount(t, requireSingleNodeFixture(t))
func TestDiscovery(t *testing.T) {
t.Parallel()
runDiscovery(t, requireSingleNodeFixture(t))
}
// Negative / error paths.
func TestNegativeErrorPaths(t *testing.T) {
phase8_NegativeErrorPaths(t, requireSingleNodeFixture(t))
func TestSerialConsoleAccess(t *testing.T) {
t.Parallel()
runSerialConsoleAccess(t, requireSingleNodeFixture(t))
}
// IAM 1–7.
func TestIAM1_UserCRUD(t *testing.T) {
phaseIAM1_UserCRUD(t, requireSingleNodeFixture(t))
}
func TestIAM2_AccessKeyLifecycle(t *testing.T) {
phaseIAM2_AccessKeyLifecycle(t, requireSingleNodeFixture(t))
}
func TestIAM3_UserAuthentication(t *testing.T) {
phaseIAM3_UserAuthentication(t, requireSingleNodeFixture(t))
}
func TestIAM4_PolicyCRUD(t *testing.T) {
phaseIAM4_PolicyCRUD(t, requireSingleNodeFixture(t))
}
func TestIAM5_PolicyAttachmentEnforcement(t *testing.T) {
phaseIAM5_PolicyAttachmentEnforcement(t, requireSingleNodeFixture(t))
}
func TestIAM6_PolicyLifecycle(t *testing.T) {
phaseIAM6_PolicyLifecycle(t, requireSingleNodeFixture(t))
}
func TestIAM7_Cleanup(t *testing.T) {
phaseIAM7_Cleanup(t, requireSingleNodeFixture(t))
func TestKeyPairs(t *testing.T) {
t.Parallel()
runKeyPairs(t, requireSingleNodeFixture(t))
}
func TestImage(t *testing.T) {
t.Parallel()
runImage(t, requireSingleNodeFixture(t))
}
func TestTagManagement(t *testing.T) {
t.Parallel()
runTagManagement(t, requireSingleNodeFixture(t))
}
// Account scoping.
func TestAccountScoping(t *testing.T) {
phase8Acct_AccountScoping(t, requireSingleNodeFixture(t))
t.Parallel()
runAccountScoping(t, requireSingleNodeFixture(t))
}
// VPC / NAT / datapath.
func TestVPCSubnetE2E(t *testing.T) {
phase8b_VPCSubnetE2E(t, requireSingleNodeFixture(t))
t.Parallel()
runVPCSubnetE2E(t, requireSingleNodeFixture(t))
}
func TestRouteTableValidation(t *testing.T) {
phase8c_RouteTableValidation(t, requireSingleNodeFixture(t))
}
func TestNATGateway(t *testing.T) { phase8d_NATGateway(t, requireSingleNodeFixture(t)) }
func TestSGToSGDatapath(t *testing.T) { phase8e_SGToSGDatapath(t, requireSingleNodeFixture(t)) }
// Final cluster sanity (replaces the umbrella's phase9b — phase9 + phase9a
// deleted; harness.Fixture.Close drains cleanups at process exit).
func TestFinalClusterStats(t *testing.T) {
phase9b_FinalClusterStats(t, requireSingleNodeFixture(t))
func TestRouteTableValidation(t *testing.T) {
t.Parallel()
runRouteTableValidation(t, requireSingleNodeFixture(t))
}
// --- Sequential: singleton VM lifecycle ---
func TestInstanceLaunch(t *testing.T) {
runInstanceLaunch(t, requireSingleNodeFixture(t))
}
func TestInstanceClusterStats(t *testing.T) {
runInstanceClusterStats(t, requireSingleNodeFixture(t))
}
func TestInstanceMetadata(t *testing.T) {
runInstanceMetadata(t, requireSingleNodeFixture(t))
}
func TestSSHProbe(t *testing.T) {
runSSHProbe(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))
}
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 TestSecurityGroupEgress(t *testing.T) {
runSecurityGroupEgress(t, requireSingleNodeFixture(t))
}
func TestStopStart(t *testing.T) {
runStopStart(t, requireSingleNodeFixture(t))
}
func TestAttachToStoppedError(t *testing.T) {
runAttachToStoppedError(t, requireSingleNodeFixture(t))
}
func TestModifyInstanceAttribute(t *testing.T) {
runModifyInstanceAttribute(t, requireSingleNodeFixture(t))
}
func TestRebootInstance(t *testing.T) {
runRebootInstance(t, requireSingleNodeFixture(t))
}
func TestRunInstancesMultiCount(t *testing.T) {
runRunInstancesMultiCount(t, requireSingleNodeFixture(t))
}
// --- Sequential: shared-state / sub-test-parallel Tests ---
func TestNegativeErrorPaths(t *testing.T) {
runNegativeErrorPaths(t, requireSingleNodeFixture(t))
}
func TestNATGateway(t *testing.T) {
runNATGateway(t, requireSingleNodeFixture(t))
}
func TestSGToSGDatapath(t *testing.T) {
runSGToSGDatapath(t, requireSingleNodeFixture(t))
}
// IAM Tests share the package-scoped IAM fixture; sub-tests inside each
// runIAM* already use t.Parallel for fan-out.
func TestIAMUserCRUD(t *testing.T) {
runIAMUserCRUD(t, requireSingleNodeFixture(t))
}
func TestIAMAccessKeyLifecycle(t *testing.T) {
runIAMAccessKeyLifecycle(t, requireSingleNodeFixture(t))
}
func TestIAMUserAuthentication(t *testing.T) {
runIAMUserAuthentication(t, requireSingleNodeFixture(t))
}
func TestIAMPolicyCRUD(t *testing.T) {
runIAMPolicyCRUD(t, requireSingleNodeFixture(t))
}
func TestIAMPolicyAttachmentEnforcement(t *testing.T) {
runIAMPolicyAttachmentEnforcement(t, requireSingleNodeFixture(t))
}
func TestIAMPolicyLifecycle(t *testing.T) {
runIAMPolicyLifecycle(t, requireSingleNodeFixture(t))
}
func TestIAMCleanup(t *testing.T) {
runIAMCleanup(t, requireSingleNodeFixture(t))
}
// TestFinalClusterStats runs as the last sequential test (parallel bucket
// runs afterwards, but those Tests are read-only / own-everything so a
// later cluster-stats snapshot would still be valid).
func TestFinalClusterStats(t *testing.T) {
runFinalClusterStats(t, requireSingleNodeFixture(t))
}
+6 -6
View File
@@ -15,11 +15,11 @@ import (
"github.com/stretchr/testify/require"
)
// phase5b_VolumeLifecycle exercises create → modify → attach → detach →
// runVolumeLifecycle exercises create → modify → attach → detach →
// delete on a fresh 10 GiB volume against the running Phase 5 instance.
// Maps to run-e2e.sh ~488–612.
func phase5b_VolumeLifecycle(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 5b — Volume Lifecycle (Attach/Detach)")
func runVolumeLifecycle(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Volume Lifecycle (Attach/Detach)")
az := needAZ(t, fix)
inst, _ := needInstance(t, fix)
@@ -133,10 +133,10 @@ func phase5b_VolumeLifecycle(t *testing.T, fix *Fixture) {
}, 2*time.Minute, 2*time.Second)
}
// phase5bii_VolumeStatus runs DescribeVolumeStatus against the root volume
// runVolumeStatus runs DescribeVolumeStatus against the root volume
// and asserts the response references it back. Maps to run-e2e.sh ~614–625.
func phase5bii_VolumeStatus(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 5b-ii — DescribeVolumeStatus")
func runVolumeStatus(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — DescribeVolumeStatus")
_, rootVolumeID := needInstance(t, fix)
+6 -6
View File
@@ -16,7 +16,7 @@ import (
"github.com/stretchr/testify/require"
)
// phase8b_VPCSubnetE2E ports run-e2e.sh Phase 8b (~2747-3496).
// runVPCSubnetE2E ports run-e2e.sh Phase 8b (~2747-3496).
//
// The bash driver layers Phase 8b on top of the *default* VPC + IGW that
// admin init prebakes, then leans on Phase 8d / 8e to share the public and
@@ -39,11 +39,11 @@ import (
// VPC graph down in the correct order: instance -> wait terminated ->
// disassociate route table -> delete route(s) -> delete route table ->
// detach IGW -> delete IGW -> delete subnets -> delete VPC.
func phase8b_VPCSubnetE2E(t *testing.T, fix *Fixture) {
func runVPCSubnetE2E(t *testing.T, fix *Fixture) {
if !fix.PoolMode {
t.Skip("Phase 8b requires pool-mode networking")
}
harness.Phase(t, "Phase 8b — VPC Public/Private Subnet E2E")
harness.Phase(t, "Single — VPC Public/Private Subnet E2E")
amiID := needAMI(t, fix)
instType, _ := needInstanceTypeArch(t, fix)
@@ -293,7 +293,7 @@ func phase8b_VPCSubnetE2E(t *testing.T, fix *Fixture) {
}
}
// phase8c_RouteTableValidation ports run-e2e.sh Phase 8c (~2626-2745).
// runRouteTableValidation ports run-e2e.sh Phase 8c (~2626-2745).
//
// Validates route-table CRUD + invariants against the *default* VPC's main
// route table (always runs — no pool-mode gating). Three sub-tests:
@@ -310,8 +310,8 @@ func phase8b_VPCSubnetE2E(t *testing.T, fix *Fixture) {
//
// Each step registers its own cleanup so a partial run still releases the
// scratch route table / association regardless of which assertion fired.
func phase8c_RouteTableValidation(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 8c — Route Table Validation")
func runRouteTableValidation(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Route Table Validation")
c := fix.AWS