diff --git a/tests/e2e/manifest-lint/baseline.txt b/tests/e2e/manifest-lint/baseline.txt index 33c416cd8..d859be4d3 100644 --- a/tests/e2e/manifest-lint/baseline.txt +++ b/tests/e2e/manifest-lint/baseline.txt @@ -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 diff --git a/tests/e2e/single/lifecycle_test.go b/tests/e2e/single/lifecycle_test.go index d528db116..88efe6b46 100644 --- a/tests/e2e/single/lifecycle_test.go +++ b/tests/e2e/single/lifecycle_test.go @@ -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) diff --git a/tests/e2e/single/tests_test.go b/tests/e2e/single/tests_test.go index f7c688f04..ebbc40d9a 100644 --- a/tests/e2e/single/tests_test.go +++ b/tests/e2e/single/tests_test.go @@ -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) { diff --git a/tests/integration/run_instances_test.go b/tests/integration/run_instances_test.go new file mode 100644 index 000000000..c578d06b7 --- /dev/null +++ b/tests/integration/run_instances_test.go @@ -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") +}