Files
spinifex/tests/e2e/single/volume_test.go
T
Josh Siviour 40f8c4da94 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).
2026-05-21 22:47:26 +10:00

164 lines
5.9 KiB
Go
Raw 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 (
"errors"
"fmt"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"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"
)
// 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 runVolumeLifecycle(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — Volume Lifecycle (Attach/Detach)")
az := needAZ(t, fix)
inst, _ := needInstance(t, fix)
instanceID := aws.StringValue(inst.InstanceId)
// This phase creates + deletes a standalone test volume; do NOT route
// through harness.EnsureVolume because the fixture's terminate-on-cleanup
// would later try to re-delete a volume this test has just deleted in-line.
harness.Step(t, "create-volume size=10 az=%s", az)
createOut, err := fix.AWS.EC2.CreateVolume(&ec2.CreateVolumeInput{
AvailabilityZone: aws.String(az),
Size: aws.Int64(10),
})
require.NoError(t, err, "create-volume")
volumeID := aws.StringValue(createOut.VolumeId)
require.NotEmpty(t, volumeID, "CreateVolume returned empty VolumeId")
harness.Detail(t, "volume", volumeID)
// Best-effort cleanup if a later assertion fails before the in-line delete.
t.Cleanup(func() {
_, _ = fix.AWS.EC2.DeleteVolume(&ec2.DeleteVolumeInput{VolumeId: aws.String(volumeID)})
})
harness.WaitForVolumeState(t, fix.AWS, volumeID, "available", harness.WithPoll(500*time.Millisecond))
const newSize int64 = 20
harness.Step(t, "modify-volume size=%d", newSize)
_, err = fix.AWS.EC2.ModifyVolume(&ec2.ModifyVolumeInput{
VolumeId: aws.String(volumeID),
Size: aws.Int64(newSize),
})
require.NoError(t, err, "modify-volume")
// Bash polls Volumes[0].Size directly (not Modifications[].State) — replicate.
// Resize is slower than state transitions; allow 5 minutes.
harness.EventuallyErr(t, func() error {
out, err := fix.AWS.EC2.DescribeVolumes(&ec2.DescribeVolumesInput{
VolumeIds: []*string{aws.String(volumeID)},
})
if err != nil {
return fmt.Errorf("describe-volumes: %w", err)
}
if len(out.Volumes) == 0 {
return fmt.Errorf("%s not found", volumeID)
}
if got := aws.Int64Value(out.Volumes[0].Size); got != newSize {
return fmt.Errorf("%s size=%d want=%d", volumeID, got, newSize)
}
return nil
}, 5*time.Minute, 5*time.Second)
harness.Detail(t, "resized_gib", newSize)
harness.Step(t, "attach-volume %s -> %s as /dev/sdf", volumeID, instanceID)
_, err = fix.AWS.EC2.AttachVolume(&ec2.AttachVolumeInput{
VolumeId: aws.String(volumeID),
InstanceId: aws.String(instanceID),
Device: aws.String("/dev/sdf"),
})
require.NoError(t, err, "attach-volume")
harness.WaitForVolumeState(t, fix.AWS, volumeID, ec2.VolumeStateInUse, harness.WithPoll(500*time.Millisecond))
// Once the volume is in-use, the attachment record should be populated.
descAttached, err := fix.AWS.EC2.DescribeVolumes(&ec2.DescribeVolumesInput{
VolumeIds: []*string{aws.String(volumeID)},
})
require.NoError(t, err, "describe-volumes (attached)")
require.NotEmpty(t, descAttached.Volumes[0].Attachments, "no Attachments after attach-volume")
att := descAttached.Volumes[0].Attachments[0]
assert.Equal(t, ec2.VolumeAttachmentStateAttached, aws.StringValue(att.State),
"attachment State should be %q", ec2.VolumeAttachmentStateAttached)
assert.Equal(t, instanceID, aws.StringValue(att.InstanceId), "Attachment.InstanceId mismatch")
// Bash omits --instance-id to exercise the gateway's resolution path.
harness.Step(t, "detach-volume %s", volumeID)
_, err = fix.AWS.EC2.DetachVolume(&ec2.DetachVolumeInput{
VolumeId: aws.String(volumeID),
})
require.NoError(t, err, "detach-volume")
harness.WaitForVolumeState(t, fix.AWS, volumeID, "available", harness.WithPoll(500*time.Millisecond))
harness.Step(t, "delete-volume %s", volumeID)
_, err = fix.AWS.EC2.DeleteVolume(&ec2.DeleteVolumeInput{
VolumeId: aws.String(volumeID),
})
require.NoError(t, err, "delete-volume")
// Bash treats describe-volume's first non-zero exit OR empty/None result
// as proof of deletion. We assert on InvalidVolume.NotFound specifically —
// any other error or a successful describe returning a non-deleted state
// surfaces the bug instead of being papered over.
harness.EventuallyErr(t, func() error {
out, err := fix.AWS.EC2.DescribeVolumes(&ec2.DescribeVolumesInput{
VolumeIds: []*string{aws.String(volumeID)},
})
if err != nil {
if harness.ErrorCodeIs(err, "InvalidVolume.NotFound") {
return nil
}
return fmt.Errorf("describe-volumes: %w", err)
}
if len(out.Volumes) == 0 {
return nil
}
if state := aws.StringValue(out.Volumes[0].State); state == "deleted" {
return nil
} else {
return errors.New("volume still present: state=" + state)
}
}, 2*time.Minute, 2*time.Second)
}
// runVolumeStatus runs DescribeVolumeStatus against the root volume
// and asserts the response references it back. Maps to run-e2e.sh ~614–625.
func runVolumeStatus(t *testing.T, fix *Fixture) {
harness.Phase(t, "Single — DescribeVolumeStatus")
_, rootVolumeID := needInstance(t, fix)
out, err := fix.AWS.EC2.DescribeVolumeStatus(&ec2.DescribeVolumeStatusInput{
VolumeIds: []*string{aws.String(rootVolumeID)},
})
require.NoError(t, err, "describe-volume-status %s", rootVolumeID)
require.NotEmpty(t, out.VolumeStatuses, "no VolumeStatuses returned")
var matched bool
var status string
for _, vs := range out.VolumeStatuses {
if aws.StringValue(vs.VolumeId) == rootVolumeID {
matched = true
if vs.VolumeStatus != nil {
status = aws.StringValue(vs.VolumeStatus.Status)
}
break
}
}
assert.Truef(t, matched, "DescribeVolumeStatus did not return %s; got %d entries",
rootVolumeID, len(out.VolumeStatuses))
harness.Detail(t, "volume", rootVolumeID, "status", status)
}