Files
Julian Sommer 7b4a83ccfe fix: expect InvalidSnapshot.NotFound for cross-account DeleteSnapshot
Snapshot metadata is now keyed by owning account, so a snapshot outside
the caller's prefix is absent rather than forbidden. The e2e assertion
still encoded the pre-partitioning UnauthorizedOperation contract.
2026-09-03 15:51:27 +10:00

834 lines
30 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build e2e
package single
import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mulgadc/spinifex/tests/e2e/harness"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// runAccountScoping validates that EC2 resources (instances, volumes, key
// pairs, snapshots, VPCs, IGWs, EIGWs) are isolated between tenant accounts.
// Three accounts are created (Alpha, Beta, Gamma). All resources use scoped
// clients with static credentials. t.Cleanup tears down everything best-effort.
func runAccountScoping(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — EC2 Account Scoping")
amiID := needAMI(t, fix)
instType, _ := needInstanceTypeArch(t, fix)
az := needAZ(t, fix)
carousel := harness.NewAccountCarousel()
// State threaded across Steps; zero values are skipped by teardown.
var (
alphaInst, betaInst string
alphaVol, betaVol string
alphaSnap, betaSnap string
alphaVPC, betaVPC string
alphaSubnet, betaSubnet string
alphaIGW, betaIGW string
alphaEIGW, betaEIGW string
alphaKeyNames = []string{}
betaKeyNames = []string{}
alphaEncryptionLeftEnabled bool
betaEncryptionLeftEnabled bool
)
// t.Cleanup runs LIFO. All deletes below swallow not-found errors (idempotent).
t.Cleanup(func() {
harness.Step(t, "Phase 8 acct cleanup (deferred)")
alpha := carousel.Get("spx-team-alpha")
beta := carousel.Get("spx-team-beta")
// Terminate instances.
if alpha != nil && alphaInst != "" {
_, _ = alpha.Client.EC2.TerminateInstances(&ec2.TerminateInstancesInput{
InstanceIds: []*string{aws.String(alphaInst)},
})
}
if beta != nil && betaInst != "" {
_, _ = beta.Client.EC2.TerminateInstances(&ec2.TerminateInstancesInput{
InstanceIds: []*string{aws.String(betaInst)},
})
}
// Wait for termination so volumes / snapshots release cleanly.
if alpha != nil && alphaInst != "" {
waitTerminated(t, alpha.Client, alphaInst)
}
if beta != nil && betaInst != "" {
waitTerminated(t, beta.Client, betaInst)
}
// Snapshots.
if alpha != nil && alphaSnap != "" {
_, _ = alpha.Client.EC2.DeleteSnapshot(&ec2.DeleteSnapshotInput{
SnapshotId: aws.String(alphaSnap),
})
}
if beta != nil && betaSnap != "" {
_, _ = beta.Client.EC2.DeleteSnapshot(&ec2.DeleteSnapshotInput{
SnapshotId: aws.String(betaSnap),
})
}
// Volumes (give a moment for detach-on-terminate to settle).
time.Sleep(3 * time.Second)
if alpha != nil && alphaVol != "" {
_, _ = alpha.Client.EC2.DeleteVolume(&ec2.DeleteVolumeInput{
VolumeId: aws.String(alphaVol),
})
}
if beta != nil && betaVol != "" {
_, _ = beta.Client.EC2.DeleteVolume(&ec2.DeleteVolumeInput{
VolumeId: aws.String(betaVol),
})
}
// Key pairs.
if alpha != nil {
for _, k := range alphaKeyNames {
_, _ = alpha.Client.EC2.DeleteKeyPair(&ec2.DeleteKeyPairInput{
KeyName: aws.String(k),
})
}
}
if beta != nil {
for _, k := range betaKeyNames {
_, _ = beta.Client.EC2.DeleteKeyPair(&ec2.DeleteKeyPairInput{
KeyName: aws.String(k),
})
}
}
// EIGWs.
if alpha != nil && alphaEIGW != "" {
_, _ = alpha.Client.EC2.DeleteEgressOnlyInternetGateway(&ec2.DeleteEgressOnlyInternetGatewayInput{
EgressOnlyInternetGatewayId: aws.String(alphaEIGW),
})
}
if beta != nil && betaEIGW != "" {
_, _ = beta.Client.EC2.DeleteEgressOnlyInternetGateway(&ec2.DeleteEgressOnlyInternetGatewayInput{
EgressOnlyInternetGatewayId: aws.String(betaEIGW),
})
}
// IGWs (must detach before delete).
if alpha != nil && alphaIGW != "" {
if alphaVPC != "" {
_, _ = alpha.Client.EC2.DetachInternetGateway(&ec2.DetachInternetGatewayInput{
InternetGatewayId: aws.String(alphaIGW),
VpcId: aws.String(alphaVPC),
})
}
_, _ = alpha.Client.EC2.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{
InternetGatewayId: aws.String(alphaIGW),
})
}
if beta != nil && betaIGW != "" {
_, _ = beta.Client.EC2.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{
InternetGatewayId: aws.String(betaIGW),
})
}
// Subnets.
if alpha != nil && alphaSubnet != "" {
_, _ = alpha.Client.EC2.DeleteSubnet(&ec2.DeleteSubnetInput{
SubnetId: aws.String(alphaSubnet),
})
}
if beta != nil && betaSubnet != "" {
_, _ = beta.Client.EC2.DeleteSubnet(&ec2.DeleteSubnetInput{
SubnetId: aws.String(betaSubnet),
})
}
// VPCs.
if alpha != nil && alphaVPC != "" {
_, _ = alpha.Client.EC2.DeleteVpc(&ec2.DeleteVpcInput{
VpcId: aws.String(alphaVPC),
})
}
if beta != nil && betaVPC != "" {
_, _ = beta.Client.EC2.DeleteVpc(&ec2.DeleteVpcInput{
VpcId: aws.String(betaVPC),
})
}
// Reset EBS encryption defaults if Step 8 left them on.
if alpha != nil && alphaEncryptionLeftEnabled {
_, _ = alpha.Client.EC2.DisableEbsEncryptionByDefault(&ec2.DisableEbsEncryptionByDefaultInput{})
}
if beta != nil && betaEncryptionLeftEnabled {
_, _ = beta.Client.EC2.DisableEbsEncryptionByDefault(&ec2.DisableEbsEncryptionByDefaultInput{})
}
})
// ---------------------------------------------------------------------
// Step 1: Account Setup (bash 1928–1965)
// ---------------------------------------------------------------------
t.Run("Step1_AccountSetup", func(t *testing.T) {
harness.Step(t, "create account 'Team Alpha'")
alphaInfo := harness.SpxAdminAccountCreate(t, "Team Alpha", "")
require.NotEmpty(t, alphaInfo.AccountID, "alpha AccountID")
require.NotEmpty(t, alphaInfo.AccessKeyID, "alpha AccessKeyID")
alpha := carousel.Add(t, fix.Env, "spx-team-alpha", alphaInfo)
harness.Detail(t, "alpha_account", alphaInfo.AccountID, "alpha_key", alphaInfo.AccessKeyID)
harness.Step(t, "create account 'Team Beta'")
betaInfo := harness.SpxAdminAccountCreate(t, "Team Beta", "")
require.NotEmpty(t, betaInfo.AccountID, "beta AccountID")
require.NotEmpty(t, betaInfo.AccessKeyID, "beta AccessKeyID")
beta := carousel.Add(t, fix.Env, "spx-team-beta", betaInfo)
harness.Detail(t, "beta_account", betaInfo.AccountID, "beta_key", betaInfo.AccessKeyID)
// Auth smoke-test — bash just gates on exit 0.
_, err := alpha.Client.EC2.DescribeInstances(&ec2.DescribeInstancesInput{})
require.NoError(t, err, "alpha describe-instances (auth check)")
_, err = beta.Client.EC2.DescribeInstances(&ec2.DescribeInstancesInput{})
require.NoError(t, err, "beta describe-instances (auth check)")
})
// Every subsequent step derefs these; bail if Step 1 failed to register them.
alpha := carousel.Get("spx-team-alpha")
beta := carousel.Get("spx-team-beta")
if alpha == nil || beta == nil {
t.Fatalf("Step 1 did not register both alpha and beta carousel entries")
}
// ---------------------------------------------------------------------
// Step 2: Instance Scoping (bash 1969–2088)
// ---------------------------------------------------------------------
t.Run("Step2_InstanceScoping", func(t *testing.T) {
const alphaInstKey = "alpha-instance-key"
const betaInstKey = "beta-instance-key"
harness.Step(t, "create per-account key pairs")
_, err := alpha.Client.EC2.CreateKeyPair(&ec2.CreateKeyPairInput{KeyName: aws.String(alphaInstKey)})
require.NoError(t, err, "alpha create-key-pair")
alphaKeyNames = append(alphaKeyNames, alphaInstKey)
_, err = beta.Client.EC2.CreateKeyPair(&ec2.CreateKeyPairInput{KeyName: aws.String(betaInstKey)})
require.NoError(t, err, "beta create-key-pair")
betaKeyNames = append(betaKeyNames, betaInstKey)
harness.Step(t, "alpha run-instances")
alphaRun, err := alpha.Client.EC2.RunInstances(&ec2.RunInstancesInput{
ImageId: aws.String(amiID),
InstanceType: aws.String(instType),
KeyName: aws.String(alphaInstKey),
MinCount: aws.Int64(1),
MaxCount: aws.Int64(1),
})
require.NoError(t, err, "alpha run-instances")
require.NotEmpty(t, alphaRun.Instances, "alpha run-instances: no instances")
alphaInst = aws.StringValue(alphaRun.Instances[0].InstanceId)
require.NotEmpty(t, alphaInst, "alpha InstanceId empty")
harness.Detail(t, "alpha_instance", alphaInst)
harness.Step(t, "beta run-instances")
betaRun, err := beta.Client.EC2.RunInstances(&ec2.RunInstancesInput{
ImageId: aws.String(amiID),
InstanceType: aws.String(instType),
KeyName: aws.String(betaInstKey),
MinCount: aws.Int64(1),
MaxCount: aws.Int64(1),
})
require.NoError(t, err, "beta run-instances")
require.NotEmpty(t, betaRun.Instances, "beta run-instances: no instances")
betaInst = aws.StringValue(betaRun.Instances[0].InstanceId)
require.NotEmpty(t, betaInst, "beta InstanceId empty")
harness.Detail(t, "beta_instance", betaInst)
harness.WaitForInstanceState(t, alpha.Client, alphaInst, "running")
harness.WaitForInstanceState(t, beta.Client, betaInst, "running")
harness.Step(t, "alpha sees only own instances")
alphaIDs := describeInstanceIDs(t, alpha.Client)
assert.NotContains(t, alphaIDs, betaInst, "alpha saw beta's instance")
harness.Step(t, "beta sees only own instances")
betaIDs := describeInstanceIDs(t, beta.Client)
assert.NotContains(t, betaIDs, alphaInst, "beta saw alpha's instance")
harness.Step(t, "alpha OwnerId matches alpha account")
alphaDesc, err := alpha.Client.EC2.DescribeInstances(&ec2.DescribeInstancesInput{})
require.NoError(t, err, "alpha describe-instances")
require.NotEmpty(t, alphaDesc.Reservations, "alpha reservations empty")
assert.Equal(t, alpha.AccountID, aws.StringValue(alphaDesc.Reservations[0].OwnerId),
"alpha OwnerId mismatch")
harness.Step(t, "cross-account stop alpha->beta blocked")
harness.ExpectError(t, "InvalidInstanceID.NotFound", func() error {
_, err := alpha.Client.EC2.StopInstances(&ec2.StopInstancesInput{
InstanceIds: []*string{aws.String(betaInst)},
})
return err
})
harness.Step(t, "cross-account terminate beta->alpha blocked")
harness.ExpectError(t, "InvalidInstanceID.NotFound", func() error {
_, err := beta.Client.EC2.TerminateInstances(&ec2.TerminateInstancesInput{
InstanceIds: []*string{aws.String(alphaInst)},
})
return err
})
harness.Step(t, "cross-account reboot alpha->beta blocked")
harness.ExpectError(t, "InvalidInstanceID.NotFound", func() error {
_, err := alpha.Client.EC2.RebootInstances(&ec2.RebootInstancesInput{
InstanceIds: []*string{aws.String(betaInst)},
})
return err
})
// Stop alpha for cross-account start/modify/console assertions.
harness.Step(t, "stop alpha instance for cross-account start/modify/console tests")
_, err = alpha.Client.EC2.StopInstances(&ec2.StopInstancesInput{
InstanceIds: []*string{aws.String(alphaInst)},
})
require.NoError(t, err, "alpha stop-instances")
harness.WaitForInstanceState(t, alpha.Client, alphaInst, "stopped")
harness.Step(t, "cross-account start beta->alpha blocked")
harness.ExpectError(t, "InvalidInstanceID.NotFound", func() error {
_, err := beta.Client.EC2.StartInstances(&ec2.StartInstancesInput{
InstanceIds: []*string{aws.String(alphaInst)},
})
return err
})
harness.Step(t, "cross-account modify-instance-attribute beta->alpha blocked")
harness.ExpectError(t, "InvalidInstanceID.NotFound", func() error {
_, err := beta.Client.EC2.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{
InstanceId: aws.String(alphaInst),
InstanceType: &ec2.AttributeValue{Value: aws.String("t2.small")},
})
return err
})
harness.Step(t, "cross-account get-console-output beta->alpha blocked")
harness.ExpectError(t, "InvalidInstanceID.NotFound", func() error {
_, err := beta.Client.EC2.GetConsoleOutput(&ec2.GetConsoleOutputInput{
InstanceId: aws.String(alphaInst),
})
return err
})
// Restart alpha for Step 3 (attach test needs it running).
harness.Step(t, "restart alpha instance")
_, err = alpha.Client.EC2.StartInstances(&ec2.StartInstancesInput{
InstanceIds: []*string{aws.String(alphaInst)},
})
require.NoError(t, err, "alpha start-instances")
harness.WaitForInstanceState(t, alpha.Client, alphaInst, "running")
})
// ---------------------------------------------------------------------
// Step 3: Volume Scoping (bash 2092–2146)
// ---------------------------------------------------------------------
t.Run("Step3_VolumeScoping", func(t *testing.T) {
harness.Step(t, "alpha create-volume")
av, err := alpha.Client.EC2.CreateVolume(&ec2.CreateVolumeInput{
AvailabilityZone: aws.String(az),
Size: aws.Int64(10),
VolumeType: aws.String("gp3"),
})
require.NoError(t, err, "alpha create-volume")
alphaVol = aws.StringValue(av.VolumeId)
require.NotEmpty(t, alphaVol)
harness.Detail(t, "alpha_vol", alphaVol)
harness.Step(t, "beta create-volume")
bv, err := beta.Client.EC2.CreateVolume(&ec2.CreateVolumeInput{
AvailabilityZone: aws.String(az),
Size: aws.Int64(10),
VolumeType: aws.String("gp3"),
})
require.NoError(t, err, "beta create-volume")
betaVol = aws.StringValue(bv.VolumeId)
require.NotEmpty(t, betaVol)
harness.Detail(t, "beta_vol", betaVol)
harness.WaitForVolumeState(t, alpha.Client, alphaVol, "available")
harness.WaitForVolumeState(t, beta.Client, betaVol, "available")
harness.Step(t, "alpha sees only own volumes")
alphaVols := describeVolumeIDs(t, alpha.Client)
assert.NotContains(t, alphaVols, betaVol, "alpha saw beta's volume")
harness.Step(t, "cross-account describe-volume by id blocked")
harness.ExpectError(t, "InvalidVolume.NotFound", func() error {
_, err := alpha.Client.EC2.DescribeVolumes(&ec2.DescribeVolumesInput{
VolumeIds: []*string{aws.String(betaVol)},
})
return err
})
harness.Step(t, "cross-account delete-volume blocked")
harness.ExpectError(t, "InvalidVolume.NotFound", func() error {
_, err := beta.Client.EC2.DeleteVolume(&ec2.DeleteVolumeInput{
VolumeId: aws.String(alphaVol),
})
return err
})
harness.Step(t, "cross-account attach-volume (beta attaches alpha's vol) blocked")
harness.ExpectError(t, "InvalidVolume.NotFound", func() error {
_, err := beta.Client.EC2.AttachVolume(&ec2.AttachVolumeInput{
VolumeId: aws.String(alphaVol),
InstanceId: aws.String(betaInst),
Device: aws.String("/dev/sdf"),
})
return err
})
// Attach alpha's volume to alpha's instance, then test cross-account detach.
harness.Step(t, "alpha attach-volume to own instance")
_, err = alpha.Client.EC2.AttachVolume(&ec2.AttachVolumeInput{
VolumeId: aws.String(alphaVol),
InstanceId: aws.String(alphaInst),
Device: aws.String("/dev/sdf"),
})
require.NoError(t, err, "alpha attach-volume")
harness.WaitForVolumeState(t, alpha.Client, alphaVol, "in-use")
harness.Step(t, "cross-account detach-volume blocked")
harness.ExpectError(t, "InvalidVolume.NotFound", func() error {
_, err := beta.Client.EC2.DetachVolume(&ec2.DetachVolumeInput{
VolumeId: aws.String(alphaVol),
})
return err
})
harness.Step(t, "cross-account modify-volume blocked")
harness.ExpectError(t, "InvalidVolume.NotFound", func() error {
_, err := beta.Client.EC2.ModifyVolume(&ec2.ModifyVolumeInput{
VolumeId: aws.String(alphaVol),
Size: aws.Int64(20),
})
return err
})
// Detach so Step 11 / cleanup can delete the volume cleanly.
harness.Step(t, "alpha detach own volume")
_, err = alpha.Client.EC2.DetachVolume(&ec2.DetachVolumeInput{
VolumeId: aws.String(alphaVol),
})
require.NoError(t, err, "alpha detach-volume")
harness.WaitForVolumeState(t, alpha.Client, alphaVol, "available")
})
// ---------------------------------------------------------------------
// Step 4: Key Pair Scoping (bash 2150–2206)
// ---------------------------------------------------------------------
// Step 4 (key-pair scoping) is covered by tests/integration's
// TestAccountScoping_KeyPairs — KeyServiceImpl's ownership check is a
// plain account-prefixed S3 key ("keys/<accountID>/..."), no vm.Manager or
// real guest involved, so the live import-key-pair variant proved nothing
// beyond what the integration tier already asserts (CreateKeyPair/
// DeleteKeyPair/namespace-isolation across two real, independently minted
// accounts). ImportKeyPair isolation is not separately re-asserted there:
// it shares the same account-prefixed storage path as CreateKeyPair, so a
// second create-time isolation check would exercise identical code.
// ---------------------------------------------------------------------
// Step 5: Snapshot Scoping (bash 2210–2250)
// ---------------------------------------------------------------------
t.Run("Step5_SnapshotScoping", func(t *testing.T) {
require.NotEmpty(t, alphaVol, "Step 3 must populate alphaVol")
require.NotEmpty(t, betaVol, "Step 3 must populate betaVol")
harness.Step(t, "alpha create-snapshot")
as, err := alpha.Client.EC2.CreateSnapshot(&ec2.CreateSnapshotInput{
VolumeId: aws.String(alphaVol),
Description: aws.String("Alpha snapshot"),
})
require.NoError(t, err, "alpha create-snapshot")
alphaSnap = aws.StringValue(as.SnapshotId)
require.NotEmpty(t, alphaSnap)
harness.Detail(t, "alpha_snap", alphaSnap)
harness.Step(t, "beta create-snapshot")
bs, err := beta.Client.EC2.CreateSnapshot(&ec2.CreateSnapshotInput{
VolumeId: aws.String(betaVol),
Description: aws.String("Beta snapshot"),
})
require.NoError(t, err, "beta create-snapshot")
betaSnap = aws.StringValue(bs.SnapshotId)
require.NotEmpty(t, betaSnap)
harness.Detail(t, "beta_snap", betaSnap)
harness.Step(t, "alpha sees only own snapshots (owner-ids=self)")
alphaSnaps, err := alpha.Client.EC2.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
OwnerIds: []*string{aws.String("self")},
})
require.NoError(t, err, "alpha describe-snapshots")
var alphaIDs []string
for _, s := range alphaSnaps.Snapshots {
alphaIDs = append(alphaIDs, aws.StringValue(s.SnapshotId))
}
assert.NotContains(t, alphaIDs, betaSnap, "alpha saw beta's snapshot")
// OwnerId verification.
require.NotEmpty(t, alphaSnaps.Snapshots, "alpha snapshots empty")
assert.Equal(t, alpha.AccountID, aws.StringValue(alphaSnaps.Snapshots[0].OwnerId),
"alpha snapshot OwnerId mismatch")
// Snapshot metadata is keyed by owning account, so a snapshot outside the
// caller's prefix is absent rather than forbidden.
harness.Step(t, "cross-account delete-snapshot blocked (InvalidSnapshot.NotFound)")
harness.ExpectError(t, "InvalidSnapshot.NotFound", func() error {
_, err := beta.Client.EC2.DeleteSnapshot(&ec2.DeleteSnapshotInput{
SnapshotId: aws.String(alphaSnap),
})
return err
})
harness.Step(t, "cross-account create-snapshot of alpha's volume blocked")
harness.ExpectError(t, "InvalidVolume.NotFound", func() error {
_, err := beta.Client.EC2.CreateSnapshot(&ec2.CreateSnapshotInput{
VolumeId: aws.String(alphaVol),
Description: aws.String("stolen"),
})
return err
})
})
// Steps 6/7/8 (VPC/subnet, IGW/EIGW, and account-settings scoping) are
// covered by tests/integration's TestAccountScoping_VPCSubnet,
// TestAccountScoping_IGWEIGW and TestAccountScoping_Settings.
// VPCServiceImpl/IGWServiceImpl/EgressOnlyIGWServiceImpl/
// AccountSettingsServiceImpl all resolve ownership from a plain
// account-scoped KV key (utils.AccountKey(accountID, id) or an
// account-keyed settings record) — no vm.Manager, no real guest, no OVN
// state that only exists once a VPC has an attached instance. The live
// variants proved nothing beyond what the integration tier now asserts
// across two real, independently minted tenant accounts.
// ---------------------------------------------------------------------
// Step 9: Global Resources (bash 2439–2472)
// ---------------------------------------------------------------------
t.Run("Step9_GlobalResources", func(t *testing.T) {
harness.Step(t, "describe-regions identical across accounts")
alphaRegions := describeRegionNames(t, alpha.Client)
betaRegions := describeRegionNames(t, beta.Client)
assert.Equal(t, alphaRegions, betaRegions, "regions differ between accounts")
harness.Step(t, "describe-availability-zones identical across accounts")
alphaAZs := describeAZNames(t, alpha.Client)
betaAZs := describeAZNames(t, beta.Client)
assert.Equal(t, alphaAZs, betaAZs, "AZs differ between accounts")
harness.Step(t, "describe-instance-types identical across accounts")
alphaTypes := describeInstanceTypeNames(t, alpha.Client)
betaTypes := describeInstanceTypeNames(t, beta.Client)
assert.Equal(t, alphaTypes, betaTypes, "instance types differ between accounts")
})
// ---------------------------------------------------------------------
// Step 10: Edge Cases (bash 2476–2542)
// ---------------------------------------------------------------------
t.Run("Step10_EdgeCases", func(t *testing.T) {
harness.Step(t, "create empty Gamma account")
gammaInfo := harness.SpxAdminAccountCreate(t, "Team Gamma", "")
gamma := carousel.Add(t, fix.Env, "spx-team-gamma", gammaInfo)
harness.Detail(t, "gamma_account", gammaInfo.AccountID)
gInsts, err := gamma.Client.EC2.DescribeInstances(&ec2.DescribeInstancesInput{})
require.NoError(t, err, "gamma describe-instances")
var gInstIDs []string
for _, r := range gInsts.Reservations {
for _, i := range r.Instances {
gInstIDs = append(gInstIDs, aws.StringValue(i.InstanceId))
}
}
assert.Empty(t, gInstIDs, "gamma has instances")
gKeys := describeKeyPairNames(t, gamma.Client)
assert.Empty(t, gKeys, "gamma has key pairs")
gSnaps, err := gamma.Client.EC2.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
OwnerIds: []*string{aws.String("self")},
})
require.NoError(t, err, "gamma describe-snapshots")
assert.Empty(t, gSnaps.Snapshots, "gamma has snapshots")
// Root isolation: a key pair created on the root profile must not
// appear under alpha. Also verify root cannot see alpha's instance.
const rootKey = "root-scoping-key"
harness.Step(t, "root create-key-pair %s", rootKey)
_, err = fix.AWS.EC2.CreateKeyPair(&ec2.CreateKeyPairInput{KeyName: aws.String(rootKey)})
require.NoError(t, err, "root create-key-pair %s", rootKey)
t.Cleanup(func() {
_, _ = fix.AWS.EC2.DeleteKeyPair(&ec2.DeleteKeyPairInput{KeyName: aws.String(rootKey)})
})
alphaKeys := describeKeyPairNames(t, alpha.Client)
assert.NotContains(t, alphaKeys, rootKey, "alpha saw root's key pair")
rootInsts := describeInstanceIDs(t, fix.AWS)
assert.NotContains(t, rootInsts, alphaInst, "root saw tenant instance")
_, _ = fix.AWS.EC2.DeleteKeyPair(&ec2.DeleteKeyPairInput{KeyName: aws.String(rootKey)})
// Non-existent ids — same error code as cross-account.
harness.Step(t, "non-existent volume id -> InvalidVolume.NotFound")
harness.ExpectError(t, "InvalidVolume.NotFound", func() error {
_, err := alpha.Client.EC2.DeleteVolume(&ec2.DeleteVolumeInput{
VolumeId: aws.String("vol-00000000000000000"),
})
return err
})
harness.Step(t, "non-existent snapshot id -> InvalidSnapshot.NotFound")
harness.ExpectError(t, "InvalidSnapshot.NotFound", func() error {
_, err := alpha.Client.EC2.DeleteSnapshot(&ec2.DeleteSnapshotInput{
SnapshotId: aws.String("snap-00000000000000000"),
})
return err
})
})
// ---------------------------------------------------------------------
// Step 11: EC2 Account Scoping Cleanup (bash 2546–2624)
// ---------------------------------------------------------------------
// Explicit teardown — outer t.Cleanup also runs idempotently on failure.
t.Run("Step11_Cleanup", func(t *testing.T) {
harness.Step(t, "terminate alpha instance")
if alphaInst != "" {
_, _ = alpha.Client.EC2.TerminateInstances(&ec2.TerminateInstancesInput{
InstanceIds: []*string{aws.String(alphaInst)},
})
}
harness.Step(t, "terminate beta instance")
if betaInst != "" {
_, _ = beta.Client.EC2.TerminateInstances(&ec2.TerminateInstancesInput{
InstanceIds: []*string{aws.String(betaInst)},
})
}
if alphaInst != "" {
waitTerminated(t, alpha.Client, alphaInst)
}
if betaInst != "" {
waitTerminated(t, beta.Client, betaInst)
}
harness.Step(t, "delete snapshots")
if alphaSnap != "" {
_, _ = alpha.Client.EC2.DeleteSnapshot(&ec2.DeleteSnapshotInput{
SnapshotId: aws.String(alphaSnap),
})
alphaSnap = ""
}
if betaSnap != "" {
_, _ = beta.Client.EC2.DeleteSnapshot(&ec2.DeleteSnapshotInput{
SnapshotId: aws.String(betaSnap),
})
betaSnap = ""
}
time.Sleep(3 * time.Second)
harness.Step(t, "delete volumes")
if alphaVol != "" {
_, _ = alpha.Client.EC2.DeleteVolume(&ec2.DeleteVolumeInput{
VolumeId: aws.String(alphaVol),
})
alphaVol = ""
}
if betaVol != "" {
_, _ = beta.Client.EC2.DeleteVolume(&ec2.DeleteVolumeInput{
VolumeId: aws.String(betaVol),
})
betaVol = ""
}
harness.Step(t, "delete key pairs")
for _, k := range alphaKeyNames {
_, _ = alpha.Client.EC2.DeleteKeyPair(&ec2.DeleteKeyPairInput{KeyName: aws.String(k)})
}
alphaKeyNames = nil
for _, k := range betaKeyNames {
_, _ = beta.Client.EC2.DeleteKeyPair(&ec2.DeleteKeyPairInput{KeyName: aws.String(k)})
}
betaKeyNames = nil
harness.Step(t, "delete EIGWs")
if alphaEIGW != "" {
_, _ = alpha.Client.EC2.DeleteEgressOnlyInternetGateway(&ec2.DeleteEgressOnlyInternetGatewayInput{
EgressOnlyInternetGatewayId: aws.String(alphaEIGW),
})
alphaEIGW = ""
}
if betaEIGW != "" {
_, _ = beta.Client.EC2.DeleteEgressOnlyInternetGateway(&ec2.DeleteEgressOnlyInternetGatewayInput{
EgressOnlyInternetGatewayId: aws.String(betaEIGW),
})
betaEIGW = ""
}
harness.Step(t, "detach + delete IGWs")
if alphaIGW != "" {
if alphaVPC != "" {
_, _ = alpha.Client.EC2.DetachInternetGateway(&ec2.DetachInternetGatewayInput{
InternetGatewayId: aws.String(alphaIGW),
VpcId: aws.String(alphaVPC),
})
}
_, _ = alpha.Client.EC2.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{
InternetGatewayId: aws.String(alphaIGW),
})
alphaIGW = ""
}
if betaIGW != "" {
_, _ = beta.Client.EC2.DeleteInternetGateway(&ec2.DeleteInternetGatewayInput{
InternetGatewayId: aws.String(betaIGW),
})
betaIGW = ""
}
harness.Step(t, "delete subnets")
if alphaSubnet != "" {
_, _ = alpha.Client.EC2.DeleteSubnet(&ec2.DeleteSubnetInput{
SubnetId: aws.String(alphaSubnet),
})
alphaSubnet = ""
}
if betaSubnet != "" {
_, _ = beta.Client.EC2.DeleteSubnet(&ec2.DeleteSubnetInput{
SubnetId: aws.String(betaSubnet),
})
betaSubnet = ""
}
harness.Step(t, "delete VPCs")
if alphaVPC != "" {
_, _ = alpha.Client.EC2.DeleteVpc(&ec2.DeleteVpcInput{VpcId: aws.String(alphaVPC)})
alphaVPC = ""
}
if betaVPC != "" {
_, _ = beta.Client.EC2.DeleteVpc(&ec2.DeleteVpcInput{VpcId: aws.String(betaVPC)})
betaVPC = ""
}
alphaInst = ""
betaInst = ""
})
}
// waitTerminated polls until id reaches "terminated" or NotFound (60s cap).
func waitTerminated(t *testing.T, c *harness.AWSClient, id string) {
t.Helper()
deadline := time.Now().Add(60 * time.Second)
for time.Now().Before(deadline) {
out, err := c.EC2.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIds: []*string{aws.String(id)},
})
if err != nil {
if aerr, ok := err.(awserr.Error); ok && aerr.Code() == "InvalidInstanceID.NotFound" {
return
}
} else if len(out.Reservations) > 0 && len(out.Reservations[0].Instances) > 0 {
state := aws.StringValue(out.Reservations[0].Instances[0].State.Name)
if state == "terminated" {
return
}
}
time.Sleep(2 * time.Second)
}
// Don't fail the test from cleanup — log and move on.
t.Logf("waitTerminated: %s did not reach terminated within 60s", id)
}
// describeInstanceIDs returns all instance ids visible to c across every
// reservation. Used for cross-account visibility assertions.
func describeInstanceIDs(t *testing.T, c *harness.AWSClient) []string {
t.Helper()
out, err := c.EC2.DescribeInstances(&ec2.DescribeInstancesInput{})
require.NoError(t, err, "describe-instances")
var ids []string
for _, r := range out.Reservations {
for _, i := range r.Instances {
ids = append(ids, aws.StringValue(i.InstanceId))
}
}
return ids
}
// describeVolumeIDs returns all volume ids visible to c.
func describeVolumeIDs(t *testing.T, c *harness.AWSClient) []string {
t.Helper()
out, err := c.EC2.DescribeVolumes(&ec2.DescribeVolumesInput{})
require.NoError(t, err, "describe-volumes")
ids := make([]string, 0, len(out.Volumes))
for _, v := range out.Volumes {
ids = append(ids, aws.StringValue(v.VolumeId))
}
return ids
}
// describeKeyPairNames returns key pair names visible to c.
func describeKeyPairNames(t *testing.T, c *harness.AWSClient) []string {
t.Helper()
out, err := c.EC2.DescribeKeyPairs(&ec2.DescribeKeyPairsInput{})
require.NoError(t, err, "describe-key-pairs")
names := make([]string, 0, len(out.KeyPairs))
for _, kp := range out.KeyPairs {
names = append(names, aws.StringValue(kp.KeyName))
}
return names
}
// describeRegionNames returns region names in gateway-returned order.
func describeRegionNames(t *testing.T, c *harness.AWSClient) []string {
t.Helper()
out, err := c.EC2.DescribeRegions(&ec2.DescribeRegionsInput{})
require.NoError(t, err, "describe-regions")
names := make([]string, 0, len(out.Regions))
for _, r := range out.Regions {
names = append(names, aws.StringValue(r.RegionName))
}
return names
}
// describeAZNames returns AZ names.
func describeAZNames(t *testing.T, c *harness.AWSClient) []string {
t.Helper()
out, err := c.EC2.DescribeAvailabilityZones(&ec2.DescribeAvailabilityZonesInput{})
require.NoError(t, err, "describe-availability-zones")
names := make([]string, 0, len(out.AvailabilityZones))
for _, z := range out.AvailabilityZones {
names = append(names, aws.StringValue(z.ZoneName))
}
return names
}
// describeInstanceTypeNames returns sorted instance-type names.
func describeInstanceTypeNames(t *testing.T, c *harness.AWSClient) []string {
t.Helper()
out, err := c.EC2.DescribeInstanceTypes(&ec2.DescribeInstanceTypesInput{})
require.NoError(t, err, "describe-instance-types")
names := make([]string, 0, len(out.InstanceTypes))
for _, it := range out.InstanceTypes {
names = append(names, aws.StringValue(it.InstanceType))
}
sortStrings(names)
return names
}
// sortStrings sorts xs in place (insertion sort).
func sortStrings(xs []string) {
for i := 1; i < len(xs); i++ {
for j := i; j > 0 && xs[j-1] > xs[j]; j-- {
xs[j-1], xs[j] = xs[j], xs[j-1]
}
}
}