//go:build e2e package single import ( "crypto/rand" "crypto/rsa" "fmt" "os" "strconv" "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" "golang.org/x/crypto/ssh" ) // Package-local bootstrappers wrapping harness.Discover*/Ensure* so each Test* // can self-bootstrap its prerequisites. Results are memoized by harness.Fixture. // needAZ returns the discovered default availability zone. func needAZ(t *testing.T, fix *Fixture) string { t.Helper() return harness.DiscoverDefaultAZ(t, fix.Harness) } // needInstanceTypeArch returns the nano instance type and its architecture. func needInstanceTypeArch(t *testing.T, fix *Fixture) (instanceType, arch string) { t.Helper() return harness.DiscoverNanoInstanceType(t, fix.Harness) } // needAMI returns the discovered architecture-appropriate Ubuntu AMI ID. func needAMI(t *testing.T, fix *Fixture) string { t.Helper() _, arch := needInstanceTypeArch(t, fix) return harness.DiscoverUbuntuAMI(t, fix.Harness, arch) } // needKeyPair returns the memoized primary EC2 key pair name + PEM path. func needKeyPair(t *testing.T, fix *Fixture) (name, pemPath string) { t.Helper() return harness.EnsureKeyPair(t, fix.Harness) } // customAMIName is the well-known AMI name used by CreateImage tests. const customAMIName = "e2e-custom-ami" // needCustomAMI returns the memoized CreateImage-built custom AMI ID, // building it (via harness.EnsureAMI) on first call. Bootstraps the source // instance first. func needCustomAMI(t *testing.T, fix *Fixture) string { t.Helper() inst, _ := needInstance(t, fix) return ensureCustomAMI(t, fix, aws.StringValue(inst.InstanceId), customAMIName, "E2E test custom image") } // ensureCustomAMI is the underlying create path used by needCustomAMI and // the phase5e direct call. Routes through harness.EnsureAMI so the AMI is // torn down at fixture cleanup. func ensureCustomAMI(t *testing.T, fix *Fixture, sourceInstanceID, name, desc string) string { t.Helper() return harness.EnsureAMI(t, fix.Harness, harness.AMISource{ CreateFrom: &harness.AMICreateSpec{ SourceInstanceID: sourceInstanceID, Name: name, Description: desc, NoReboot: true, }, }) } // needInstance bootstraps the suite's primary running instance and returns // the *ec2.Instance plus its root volume ID. Idempotent via harness.EnsureInstance. func needInstance(t *testing.T, fix *Fixture) (inst *ec2.Instance, rootVolumeID string) { t.Helper() instType, _ := needInstanceTypeArch(t, fix) keyName, _ := needKeyPair(t, fix) ami := needAMI(t, fix) vpc := harness.EnsureDefaultVPC(t, fix.Harness) require.NotEmpty(t, vpc.SGID, "default SG ID required") harness.AuthorizeSSHIngress(t, fix.AWS, vpc.SGID) instanceID := harness.EnsureInstance(t, fix.Harness, harness.InstanceSpec{ AMIID: ami, InstanceType: instType, KeyName: keyName, SubnetID: vpc.SubnetID, SGID: vpc.SGID, }) require.NotEmpty(t, instanceID, "EnsureInstance returned empty InstanceId") descOut, err := fix.AWS.EC2.DescribeInstances(&ec2.DescribeInstancesInput{ InstanceIds: []*string{aws.String(instanceID)}, }) require.NoError(t, err, "describe-instances %s", instanceID) require.NotEmpty(t, descOut.Reservations, "no reservations for %s", instanceID) require.NotEmpty(t, descOut.Reservations[0].Instances, "no instances for %s", instanceID) inst = descOut.Reservations[0].Instances[0] // Match BDM by RootDeviceName; fall back to first BDM if empty. rootDev := aws.StringValue(inst.RootDeviceName) for _, bdm := range inst.BlockDeviceMappings { if rootDev != "" && aws.StringValue(bdm.DeviceName) != rootDev { continue } if bdm.Ebs != nil { rootVolumeID = aws.StringValue(bdm.Ebs.VolumeId) break } } if rootVolumeID == "" && len(inst.BlockDeviceMappings) > 0 && inst.BlockDeviceMappings[0].Ebs != nil { rootVolumeID = aws.StringValue(inst.BlockDeviceMappings[0].Ebs.VolumeId) } require.NotEmpty(t, rootVolumeID, "could not resolve root volume from BlockDeviceMappings") return inst, rootVolumeID } // envPositiveIntOr returns the named environment variable parsed as a // positive int, or def when unset. Mirrors storagegrowth's helper of the // same shape: a malformed or non-positive value is a configuration error and // panics rather than silently falling back, so a mistyped round count can't // quietly run the wrong number of rounds and get attributed to the default. func envPositiveIntOr(key string, def int) int { v := os.Getenv(key) if v == "" { return def } n, err := strconv.Atoi(v) if err != nil || n < 1 { panic(fmt.Sprintf("%s=%q is not a positive integer", key, v)) } return n } // generateImportPubKey returns an OpenSSH-formatted public key (matching the // `ssh-keygen -t rsa` output the bash script feeds into import-key-pair). // 2048-bit to match the bash key length. func generateImportPubKey(t *testing.T) []byte { t.Helper() priv, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err, "generate RSA key") pub, err := ssh.NewPublicKey(&priv.PublicKey) require.NoError(t, err, "ssh.NewPublicKey") return ssh.MarshalAuthorizedKey(pub) }