mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-20 11:21:49 +00:00
test(integration): migrate RunInstances multi-count expansion off live guests
RunInstances with MinCount/MaxCount=2 expands into a single per-node targeted launch (distributeInstances in gateway/ec2/instance/placement.go) before the daemon-facing NATS hop, so the real gateway router plus a stubbed daemon response is sufficient to prove it — no throwaway VM is needed to exercise the expansion logic itself. The live single-node E2E test's WaitForInstanceState/TerminateInstances sequence was teardown hygiene for the real guests it booted, not an assertion of the expansion behavior, so it is dropped rather than emulated. The migrated test additionally asserts the per-node launch request itself carries MinCount=MaxCount=2, proving the count actually propagates rather than the launch succeeding by coincidence or via the zero-daemon degenerate path in placement.go.
This commit is contained in:
@@ -76,7 +76,6 @@ create tests/e2e/single/baseline_test.go RunInstances #1
|
||||
create tests/e2e/single/datapath_test.go CreateSecurityGroup #1
|
||||
create tests/e2e/single/datapath_test.go RunInstances #1
|
||||
create tests/e2e/single/lifecycle_test.go CreateVolume #1
|
||||
create tests/e2e/single/lifecycle_test.go RunInstances #1
|
||||
create tests/e2e/single/natgw_test.go AllocateAddress #1
|
||||
create tests/e2e/single/natgw_test.go CreateNatGateway #1
|
||||
create tests/e2e/single/natgw_test.go CreateRouteTable #1
|
||||
|
||||
@@ -354,68 +354,8 @@ func runRebootInstance(t *testing.T, fix *Fixture) {
|
||||
assert.Equalf(t, preRebootIP, postRebootIP,
|
||||
"PrivateIpAddress changed across reboot: %s -> %s", preRebootIP, postRebootIP)
|
||||
|
||||
// Leave instance running — Phase 7c launches sibling instances next,
|
||||
// and Phase 8 expects the primary instance to still be up.
|
||||
}
|
||||
|
||||
// runRunInstancesMultiCount launches 2 sibling instances in a single
|
||||
// RunInstances call, waits for both to reach "running", then terminates
|
||||
// them and waits for both to reach "terminated". The primary fix.Instance
|
||||
// is untouched and remains running for Phase 8. Maps to run-e2e.sh
|
||||
// ~1238–1296.
|
||||
func runRunInstancesMultiCount(t *testing.T, fix *Fixture) {
|
||||
harness.Phase(t, "Single — RunInstances with MinCount/MaxCount > 1")
|
||||
|
||||
amiID := needAMI(t, fix)
|
||||
instType, _ := needInstanceTypeArch(t, fix)
|
||||
keyName, _ := needKeyPair(t, fix)
|
||||
|
||||
harness.Step(t, "run-instances ami=%s type=%s count=2", amiID, instType)
|
||||
out, err := fix.AWS.EC2.RunInstances(&ec2.RunInstancesInput{
|
||||
ImageId: aws.String(amiID),
|
||||
InstanceType: aws.String(instType),
|
||||
KeyName: aws.String(keyName),
|
||||
MinCount: aws.Int64(2),
|
||||
MaxCount: aws.Int64(2),
|
||||
})
|
||||
require.NoError(t, err, "run-instances --count 2")
|
||||
require.Lenf(t, out.Instances, 2,
|
||||
"expected 2 instances from run-instances, got %d", len(out.Instances))
|
||||
|
||||
multiIDs := []string{
|
||||
aws.StringValue(out.Instances[0].InstanceId),
|
||||
aws.StringValue(out.Instances[1].InstanceId),
|
||||
}
|
||||
require.NotEmpty(t, multiIDs[0], "first sibling InstanceId empty")
|
||||
require.NotEmpty(t, multiIDs[1], "second sibling InstanceId empty")
|
||||
harness.Detail(t, "sibling_1", multiIDs[0], "sibling_2", multiIDs[1])
|
||||
|
||||
// Always tear down the siblings — pre-register before any blocking
|
||||
// wait so a t.Fatal on state polling still triggers cleanup.
|
||||
terminated := false
|
||||
t.Cleanup(func() {
|
||||
if terminated {
|
||||
return
|
||||
}
|
||||
_, _ = fix.AWS.EC2.TerminateInstances(&ec2.TerminateInstancesInput{
|
||||
InstanceIds: aws.StringSlice(multiIDs),
|
||||
})
|
||||
})
|
||||
|
||||
for _, id := range multiIDs {
|
||||
harness.WaitForInstanceState(t, fix.AWS, id, "running")
|
||||
}
|
||||
|
||||
harness.Step(t, "terminate-instances %s %s", multiIDs[0], multiIDs[1])
|
||||
_, err = fix.AWS.EC2.TerminateInstances(&ec2.TerminateInstancesInput{
|
||||
InstanceIds: aws.StringSlice(multiIDs),
|
||||
})
|
||||
require.NoError(t, err, "terminate-instances")
|
||||
|
||||
for _, id := range multiIDs {
|
||||
harness.WaitForInstanceState(t, fix.AWS, id, "terminated")
|
||||
}
|
||||
terminated = true
|
||||
// Leave instance running — later phases expect the primary instance to
|
||||
// still be up.
|
||||
}
|
||||
|
||||
// waitForSSHReady probes a full SSH handshake (BatchMode + ConnectTimeout)
|
||||
|
||||
@@ -154,10 +154,6 @@ func TestRebootInstance(t *testing.T) {
|
||||
runRebootInstance(t, requireSingleNodeFixture(t))
|
||||
}
|
||||
|
||||
func TestRunInstancesMultiCount(t *testing.T) {
|
||||
runRunInstancesMultiCount(t, requireSingleNodeFixture(t))
|
||||
}
|
||||
|
||||
// TestLaunchTemplates launches a throwaway VM to verify the referenced
|
||||
// template's effective image and instance type, so it runs sequentially.
|
||||
func TestLaunchTemplates(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
//go:build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/mulgadc/spinifex/spinifex/types"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestRunInstances_MultiCountExpansion migrates the load-bearing assertion of
|
||||
// the live single-node E2E suite's runRunInstancesMultiCount
|
||||
// (spinifex/tests/e2e/single/lifecycle_test.go): a RunInstances call with
|
||||
// MinCount=MaxCount=2 returns 2 instances with non-empty InstanceIds. That
|
||||
// expansion is gateway-side logic — distributeInstances in
|
||||
// spinifex/gateway/ec2/instance/placement.go queries node capacity over NATS,
|
||||
// computes how many instances to launch, and dispatches a single targeted
|
||||
// launch per node — resolved entirely before the daemon-facing NATS hop, so a
|
||||
// static stub of the daemon side is sufficient; no real guest is needed.
|
||||
//
|
||||
// The live test's WaitForInstanceState(running)/TerminateInstances/
|
||||
// WaitForInstanceState(terminated) sequence is left behind: it is teardown
|
||||
// hygiene for a real throwaway VM, not part of what this test asserts.
|
||||
//
|
||||
// To prove the expansion is genuinely exercised rather than short-circuited by
|
||||
// the zero-daemon degenerate path (placement.go's queryNodeCapacity returning
|
||||
// no eligible nodes, which also yields InsufficientInstanceCapacity — see
|
||||
// placement.go:44), this test subscribes to the per-node launch subject
|
||||
// directly instead of using StubSubject's static responder, and asserts the
|
||||
// daemon actually received MinCount=MaxCount=2 on that targeted request.
|
||||
func TestRunInstances_MultiCountExpansion(t *testing.T) {
|
||||
gw := StartGateway(t)
|
||||
|
||||
const (
|
||||
instanceType = "t3.micro"
|
||||
nodeID = "node-1"
|
||||
)
|
||||
|
||||
// A single node reporting capacity for 2 of instanceType — enough for
|
||||
// distributeInstances to resolve MinCount=MaxCount=2 onto one node.
|
||||
statusResp := mustMarshal(t, &types.NodeStatusResponse{
|
||||
Node: nodeID,
|
||||
InstanceTypes: []types.InstanceTypeCap{
|
||||
{Name: instanceType, Available: 2},
|
||||
},
|
||||
})
|
||||
gw.StubSubject(t, "spinifex.node.status", statusResp)
|
||||
|
||||
var gotMinCount, gotMaxCount int64
|
||||
launchSubject := fmt.Sprintf("ec2.RunInstances.%s.%s", instanceType, nodeID)
|
||||
sub, err := gw.NATSConn.Subscribe(launchSubject, func(msg *nats.Msg) {
|
||||
var nodeInput ec2.RunInstancesInput
|
||||
if jsonErr := json.Unmarshal(msg.Data, &nodeInput); jsonErr != nil {
|
||||
t.Errorf("unmarshal per-node RunInstances request: %v", jsonErr)
|
||||
return
|
||||
}
|
||||
gotMinCount = aws.Int64Value(nodeInput.MinCount)
|
||||
gotMaxCount = aws.Int64Value(nodeInput.MaxCount)
|
||||
_ = msg.Respond(mustMarshal(t, &ec2.Reservation{
|
||||
ReservationId: aws.String("r-multi-count"),
|
||||
Instances: []*ec2.Instance{
|
||||
{InstanceId: aws.String("i-multi-count-1")},
|
||||
{InstanceId: aws.String("i-multi-count-2")},
|
||||
},
|
||||
}))
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sub.Unsubscribe() })
|
||||
|
||||
out, err := gw.EC2Client(t).RunInstances(&ec2.RunInstancesInput{
|
||||
ImageId: aws.String("ami-0123456789abcdef0"),
|
||||
InstanceType: aws.String(instanceType),
|
||||
MinCount: aws.Int64(2),
|
||||
MaxCount: aws.Int64(2),
|
||||
})
|
||||
require.NoError(t, err, "run-instances --count 2")
|
||||
require.Lenf(t, out.Instances, 2, "expected 2 instances from run-instances, got %d", len(out.Instances))
|
||||
require.NotEmpty(t, aws.StringValue(out.Instances[0].InstanceId), "first sibling InstanceId empty")
|
||||
require.NotEmpty(t, aws.StringValue(out.Instances[1].InstanceId), "second sibling InstanceId empty")
|
||||
|
||||
// Proves the gateway genuinely expanded the count onto the per-node
|
||||
// launch, rather than the zero-daemon path (which never reaches this
|
||||
// subject at all) or a daemon stub that ignores its input.
|
||||
require.Equal(t, int64(2), gotMinCount, "gateway must dispatch MinCount=2 on the per-node launch")
|
||||
require.Equal(t, int64(2), gotMaxCount, "gateway must dispatch MaxCount=2 on the per-node launch")
|
||||
}
|
||||
Reference in New Issue
Block a user