test(integration): migrate DescribeImages round-trip off live guests

runImage's AMI describe round-trip needs no real cluster: it registers an
AMI against a hand-seeded snapshot via the real ImageServiceImpl, backed by
a new StartImageDaemonLite helper wiring RegisterImage/DescribeImages onto an
in-memory object store.
This commit is contained in:
joshsiv-mulga
2026-07-20 23:47:11 +10:00
parent dd764b3970
commit 7ce592227b
4 changed files with 97 additions and 42 deletions
-37
View File
@@ -1,37 +0,0 @@
//go:build e2e
package single
import (
"testing"
"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/require"
)
// 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 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
// AMI consumer hit the memoized ID.
amiID := needAMI(t, fix)
require.NotEmpty(t, amiID, "needAMI returned empty AMI ID")
_, arch := needInstanceTypeArch(t, fix)
harness.Detail(t, "ami", amiID, "arch", arch)
harness.Step(t, "describe-images by AMI ID (verify round-trip)")
byID, err := fix.AWS.EC2.DescribeImages(&ec2.DescribeImagesInput{
ImageIds: []*string{aws.String(amiID)},
})
require.NoErrorf(t, err, "describe-images %s", amiID)
require.Lenf(t, byID.Images, 1, "expected exactly 1 image for %s, got %d", amiID, len(byID.Images))
require.Equal(t, amiID, aws.StringValue(byID.Images[0].ImageId), "round-trip AMI ID mismatch")
require.NotEmpty(t, aws.StringValue(byID.Images[0].Name), "lookup-by-ID returned empty Name")
}
-5
View File
@@ -25,11 +25,6 @@ func TestDiscovery(t *testing.T) {
runDiscovery(t, requireSingleNodeFixture(t))
}
func TestImage(t *testing.T) {
t.Parallel()
runImage(t, requireSingleNodeFixture(t))
}
func TestAccountScoping(t *testing.T) {
t.Parallel()
runAccountScoping(t, requireSingleNodeFixture(t))
+41
View File
@@ -0,0 +1,41 @@
//go:build integration
package integration
import (
"testing"
handlers_ec2_image "github.com/mulgadc/spinifex/spinifex/handlers/ec2/image"
"github.com/mulgadc/spinifex/spinifex/objectstore"
"github.com/nats-io/nats.go"
)
// testImageBucket is the bucket name StartImageDaemonLite's ImageServiceImpl
// stores AMI config.json objects under. Backed by a MemoryObjectStore (see
// below), so — like testPredastoreBucket in daemon_lite.go — the name is
// never resolved against a real Predastore.
const testImageBucket = "integration-test-images"
// StartImageDaemonLite subscribes a real handlers_ec2_image.ImageServiceImpl
// — the same production code a live daemon runs (daemon/daemon_handlers_image.go)
// — to the ec2.DescribeImages/ec2.RegisterImage subjects, backed by a memory
// object store rather than a real Predastore. The store itself is returned
// too, so a test can seed fixture data (e.g. snapshot metadata RegisterImage
// requires) directly, without a round-trip through NATS.
//
// Deliberately separate from StartDaemonLite (like StartVolumeDaemonLite):
// only a test that actually exercises image registration/lookup should wire
// it. CreateImage/CopyImage/DeregisterImage/*ImageAttribute are not wired —
// no ported test needs them yet.
func StartImageDaemonLite(t *testing.T, gw *Gateway) (*handlers_ec2_image.ImageServiceImpl, objectstore.ObjectStore) {
t.Helper()
store := objectstore.NewMemoryObjectStore()
svc := handlers_ec2_image.NewImageServiceImplWithStore(store, testImageBucket)
nc := gw.NATSConn
sub(t, nc, "ec2.DescribeImages", func(m *nats.Msg) { dispatch(m, svc.DescribeImages) })
sub(t, nc, "ec2.RegisterImage", func(m *nats.Msg) { dispatch(m, svc.RegisterImage) })
return svc, store
}
+56
View File
@@ -0,0 +1,56 @@
//go:build integration
package integration
import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
handlers_ec2_snapshot "github.com/mulgadc/spinifex/spinifex/handlers/ec2/snapshot"
"github.com/stretchr/testify/require"
)
// TestImage ports tests/e2e/single/image_test.go's runImage describe-images
// round-trip. The live test resolved a pre-staged gold-image AMI via
// needAMI (bootstrap-install.sh's `spx admin images import`); this tier has
// no such bootstrap, so it registers its own AMI against a hand-seeded
// snapshot via the real RegisterImage path instead — same production
// ImageServiceImpl.DescribeImages contract, without depending on a live
// cluster's pre-imported gold image.
func TestImage(t *testing.T) {
t.Parallel()
gw := StartGateway(t)
_, store := StartImageDaemonLite(t, gw)
ec2Cli := gw.EC2Client(t)
const snapshotID = "snap-integration-test-image"
require.NoError(t, handlers_ec2_snapshot.WriteSnapshotConfig(store, testImageBucket, snapshotID, &handlers_ec2_snapshot.SnapshotConfig{
SnapshotID: snapshotID,
VolumeID: "vol-integration-test-image",
VolumeSize: 8,
State: "completed",
OwnerID: gw.AccountID,
}), "seed snapshot metadata")
registerOut, err := ec2Cli.RegisterImage(&ec2.RegisterImageInput{
Name: aws.String("integration-test-ami"),
RootDeviceName: aws.String("/dev/sda1"),
BlockDeviceMappings: []*ec2.BlockDeviceMapping{{
DeviceName: aws.String("/dev/sda1"),
Ebs: &ec2.EbsBlockDevice{SnapshotId: aws.String(snapshotID)},
}},
})
require.NoError(t, err, "register-image")
amiID := aws.StringValue(registerOut.ImageId)
require.NotEmpty(t, amiID, "register-image returned empty ImageId")
byID, err := ec2Cli.DescribeImages(&ec2.DescribeImagesInput{
ImageIds: []*string{aws.String(amiID)},
})
require.NoErrorf(t, err, "describe-images %s", amiID)
require.Lenf(t, byID.Images, 1, "expected exactly 1 image for %s, got %d", amiID, len(byID.Images))
require.Equal(t, amiID, aws.StringValue(byID.Images[0].ImageId), "round-trip AMI ID mismatch")
require.NotEmpty(t, aws.StringValue(byID.Images[0].Name), "lookup-by-ID returned empty Name")
}