mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
7b4a83ccfe
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.
834 lines
30 KiB
Go
834 lines
30 KiB
Go
//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]
|
||
}
|
||
}
|
||
}
|