fix(e2e): drop catalog-download import path from phase4_Image

`spx admin images import --name ubuntu-26.04-x86_64` with wantErr=true
was meant to detect that bootstrap-install.sh already imported the AMI
and short-circuit on the duplicate exit code. After the v6 gold-image
bump preloaded `~/images/ubuntu-26.04.img` and bootstrap-install.sh
switched to `--file` for the import, two things broke at once:

  - The catalog `--name` path no longer hits a duplicate guard for the
    --file-imported AMI, so the import re-downloads ~857MB and either
    succeeds (failing the wantErr=true assertion) or wedges the job
    timeout while the catalog server crawls along at 16KB/s.
  - `--file` from the test won't work either: the test binary runs as
    `tf-user` over SSH, while bootstrap-install.sh placed the image at
    `/home/spinifex/images/...` (mode 0700 home dir).

bootstrap-install.sh is already responsible for staging the AMI. The
test's job is verifying DescribeImages round-trips by-name and by-ID —
not re-exercising import. Trust the bootstrap-staged AMI and stop
calling import.

Keeps the 26.04→24.04 dual-detect for the v3 gold image transition;
drop the 24.04 candidate when no envs remain on v3.

Refs mulga-siv-115 (caught by the EC2 cold-boot fast-path E2E run on
feat/ec2-coldboot-fastpath).
This commit is contained in:
joshsiv-mulga
2026-05-20 08:13:49 +10:00
parent 6d38fcad5c
commit 10eb38a863
+27 -77
View File
@@ -3,7 +3,6 @@
package single package single
import ( import (
"regexp"
"testing" "testing"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
@@ -12,102 +11,53 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// amiIDRE matches "ami-<hex>" anywhere in spx import output. The CLI prints // phase4_Image rediscovers the architecture-appropriate Ubuntu AMI that
// the new AMI ID on a "✅ Image import complete. Image-ID (AMI): ami-..." // bootstrap-install.sh imported from the pre-staged gold-image file
// line; capturing the ID alone keeps us tolerant of the surrounding format // (~/images/ubuntu-{26,24}.04.img → `spx admin images import --file`) and
// changing (the bash script uses a separate describe-images call entirely // stashes the ID on the fixture for Phase 5+. Maps to run-e2e.sh ~233–255.
// because it didn't trust the CLI output).
var amiIDRE = regexp.MustCompile(`ami-[0-9a-f]+`)
// phase4_Image imports (or rediscovers) the architecture-appropriate Ubuntu
// AMI staged by bootstrap-install.sh 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) { func phase4_Image(t *testing.T, fix *Fixture) {
harness.Phase(t, "Phase 4 — Image Management") harness.Phase(t, "Phase 4 — Image Management")
require.NotEmpty(t, fix.Arch, "Phase 2 must populate fix.Arch before Phase 4") require.NotEmpty(t, fix.Arch, "Phase 2 must populate fix.Arch before Phase 4")
// Try the current catalog entry (26.04 / resolute) first, fall back to // Bootstrap-install.sh has already imported the staged ubuntu cloud
// 24.04 (noble) for clusters still on the v3 gold image. Drop the // image via `--file` (v6+ gold stages ubuntu-26.04, v3 stages
// fallback once 24.04 is gone from spinifex/utils/images.go. // ubuntu-24.04). DescribeImages by canonical name is enough — running
// `spx admin images import --name` here triggers an 800MB+ catalog
// download that outlasts the job timeout, and `--file` would need a
// path readable by the test-binary user (different from the user
// bootstrap-install.sh ran as). Drop the 24.04 candidate once the v3
// gold image is fully retired.
candidates := []string{ candidates := []string{
"ubuntu-26.04-" + fix.Arch, "ami-ubuntu-26.04-" + fix.Arch,
"ubuntu-24.04-" + fix.Arch, "ami-ubuntu-24.04-" + fix.Arch,
} }
var imgName, out string var resolvedName, resolvedID string
for _, name := range candidates { for _, name := range candidates {
harness.Detail(t, "image_candidate", name, "arch", fix.Arch) harness.Detail(t, "image_candidate", name, "arch", fix.Arch)
harness.Step(t, "spx admin images import --name %s", name) harness.Step(t, "describe-images filter name=%s", name)
// wantErr=true: bootstrap-install.sh has already imported the out, err := fix.AWS.EC2.DescribeImages(&ec2.DescribeImagesInput{
// AMI in most environments, so the import returns non-zero on
// duplicate. We don't care about the exit code — we care about
// either parsing an AMI ID or successfully filtering
// DescribeImages by the canonical ami-<name> tag afterwards.
out = harness.SpxRun(t, true, "admin", "images", "import", "--name", name)
imgName = name
// If DescribeImages can find this name, stop trying further
// candidates — bootstrap staged this version.
probe, err := fix.AWS.EC2.DescribeImages(&ec2.DescribeImagesInput{
Filters: []*ec2.Filter{ Filters: []*ec2.Filter{
{Name: aws.String("name"), Values: []*string{aws.String("ami-" + name)}}, {Name: aws.String("name"), Values: []*string{aws.String(name)}},
}, },
}) })
if err == nil && len(probe.Images) > 0 { if err == nil && len(out.Images) > 0 {
resolvedName = name
resolvedID = aws.StringValue(out.Images[0].ImageId)
break break
} }
} }
harness.Detail(t, "image", imgName) require.NotEmptyf(t, resolvedID,
"no Ubuntu AMI found via DescribeImages — bootstrap-install.sh did not stage one (tried: %v)", candidates)
// Try to parse the AMI ID directly out of the CLI's success line. require.NotEmpty(t, resolvedName, "resolved name went empty")
amiID := amiIDRE.FindString(out) harness.Detail(t, "image", resolvedName, "ami", resolvedID)
if amiID != "" {
harness.Detail(t, "spx_parsed_ami", amiID)
} else {
harness.Step(t, "spx import did not yield AMI ID; falling back to describe-images")
}
// Either confirm the parsed ID or look it up by name.
// Bash filters on `Name=name,Values=ami-${IMAGE_NAME}` — the leading
// `ami-` prefix is part of the AMI's *name* in spinifex (admin.go:487),
// not the AMI ID. Replicate exactly.
filterName := "ami-" + imgName
describeOut, err := fix.AWS.EC2.DescribeImages(&ec2.DescribeImagesInput{
Filters: []*ec2.Filter{
{Name: aws.String("name"), Values: []*string{aws.String(filterName)}},
},
})
require.NoErrorf(t, err, "describe-images filter name=%s", filterName)
require.NotEmptyf(t, describeOut.Images,
"no AMI matched name=%s — bootstrap-install.sh did not stage it and the import failed:\n%s",
filterName, truncate(out, 2000))
resolvedID := aws.StringValue(describeOut.Images[0].ImageId)
require.NotEmpty(t, resolvedID, "describe-images returned blank ImageId")
if amiID != "" && amiID != resolvedID {
// Parsed an AMI ID from spx output but it doesn't match what
// describe-images by name returns — surface the mismatch loudly
// rather than silently preferring one source over the other.
t.Fatalf("spx output AMI %s != describe-images AMI %s for name %s",
amiID, resolvedID, filterName)
}
fix.AMIID = resolvedID fix.AMIID = resolvedID
harness.Step(t, "describe-images by AMI ID (verify exactly one match)") harness.Step(t, "describe-images by AMI ID (verify round-trip)")
byID, err := fix.AWS.EC2.DescribeImages(&ec2.DescribeImagesInput{ byID, err := fix.AWS.EC2.DescribeImages(&ec2.DescribeImagesInput{
ImageIds: []*string{aws.String(fix.AMIID)}, ImageIds: []*string{aws.String(fix.AMIID)},
}) })
require.NoErrorf(t, err, "describe-images %s", fix.AMIID) require.NoErrorf(t, err, "describe-images %s", fix.AMIID)
require.Lenf(t, byID.Images, 1, "expected exactly 1 image for %s, got %d", fix.AMIID, len(byID.Images)) require.Lenf(t, byID.Images, 1, "expected exactly 1 image for %s, got %d", fix.AMIID, len(byID.Images))
require.Equal(t, fix.AMIID, aws.StringValue(byID.Images[0].ImageId), "round-trip AMI ID mismatch") require.Equal(t, fix.AMIID, aws.StringValue(byID.Images[0].ImageId), "round-trip AMI ID mismatch")
require.Equal(t, resolvedName, aws.StringValue(byID.Images[0].Name), "lookup-by-ID returned a different Name than lookup-by-Name")
harness.Detail(t, "ami", fix.AMIID, "name", aws.StringValue(byID.Images[0].Name))
}
// truncate is used to keep failure messages from dumping arbitrarily large
// spx CLI output verbatim into the test log.
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "...[truncated]"
} }