From 4db3c5f048fad241b5610cfcba3e5e8a7ec723f6 Mon Sep 17 00:00:00 2001 From: joshsiv-mulga Date: Tue, 21 Jul 2026 00:24:47 +1000 Subject: [PATCH] test(integration): cover RunInstances launch-template expansion Proves version selection (omitted Version resolves to the template default, explicit Version selects that version) and field precedence (explicit RunInstances parameters override the template, unset fields inherit it) through the real gateway. --- .../run_instances_launchtemplate_test.go | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 tests/integration/run_instances_launchtemplate_test.go diff --git a/tests/integration/run_instances_launchtemplate_test.go b/tests/integration/run_instances_launchtemplate_test.go new file mode 100644 index 000000000..799379e6d --- /dev/null +++ b/tests/integration/run_instances_launchtemplate_test.go @@ -0,0 +1,171 @@ +//go:build integration + +package integration + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "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" +) + +// captureLaunchTemplateNodeInput wires a per-node launch responder that +// forwards each decoded RunInstancesInput onto the returned channel before +// replying, so a test can inspect exactly what expandLaunchTemplate +// (handlers/ec2/launchtemplate/expand.go, called from RunInstances.go) merged +// onto the request the gateway actually dispatched. +func captureLaunchTemplateNodeInput(t *testing.T, gw *Gateway, instanceType, nodeID string) <-chan *ec2.RunInstancesInput { + t.Helper() + ch := make(chan *ec2.RunInstancesInput, 4) + subject := fmt.Sprintf("ec2.RunInstances.%s.%s", instanceType, nodeID) + sub, err := gw.NATSConn.Subscribe(subject, func(msg *nats.Msg) { + var nodeInput ec2.RunInstancesInput + if err := json.Unmarshal(msg.Data, &nodeInput); err != nil { + t.Errorf("unmarshal per-node RunInstances request: %v", err) + return + } + ch <- &nodeInput + _ = msg.Respond(mustMarshal(t, &ec2.Reservation{ + ReservationId: aws.String("r-lt"), + Instances: []*ec2.Instance{{InstanceId: aws.String("i-lt")}}, + })) + }) + require.NoError(t, err) + t.Cleanup(func() { _ = sub.Unsubscribe() }) + return ch +} + +// awaitLaunchTemplateNodeInput reads one captured RunInstancesInput or fails +// the test, so a hung dispatch (e.g. expansion never reaching the per-node +// subject) surfaces as a fast, clear failure instead of the SDK's own timeout. +func awaitLaunchTemplateNodeInput(t *testing.T, ch <-chan *ec2.RunInstancesInput) *ec2.RunInstancesInput { + t.Helper() + select { + case got := <-ch: + return got + case <-time.After(3 * time.Second): + t.Fatal("timed out waiting for per-node RunInstances dispatch") + return nil + } +} + +// TestRunInstances_LaunchTemplateVersionSelection proves ExpandRunInstances +// resolves the referenced launch template version before validation: an +// omitted Version resolves to the template's default (version 1, set at +// creation), while an explicit Version selects that specific version's data. +func TestRunInstances_LaunchTemplateVersionSelection(t *testing.T) { + gw := StartGateway(t) + StartLaunchTemplateDaemonLite(t, gw) + + const ( + templateName = "lt-version-selection" + instanceType = "t3.micro" + nodeID = "lt-node" + amiV1 = "ami-0000000000000001" + amiV2 = "ami-0000000000000002" + ) + + gw.StubSubject(t, "spinifex.node.status", mustMarshal(t, &types.NodeStatusResponse{ + Node: nodeID, + InstanceTypes: []types.InstanceTypeCap{{Name: instanceType, Available: 5}}, + })) + nodeCh := captureLaunchTemplateNodeInput(t, gw, instanceType, nodeID) + + _, err := gw.EC2Client(t).CreateLaunchTemplate(&ec2.CreateLaunchTemplateInput{ + LaunchTemplateName: aws.String(templateName), + LaunchTemplateData: &ec2.RequestLaunchTemplateData{ + ImageId: aws.String(amiV1), + InstanceType: aws.String(instanceType), + KeyName: aws.String("template-key-v1"), + }, + }) + require.NoError(t, err, "create-launch-template") + + _, err = gw.EC2Client(t).CreateLaunchTemplateVersion(&ec2.CreateLaunchTemplateVersionInput{ + LaunchTemplateName: aws.String(templateName), + LaunchTemplateData: &ec2.RequestLaunchTemplateData{ + ImageId: aws.String(amiV2), + InstanceType: aws.String(instanceType), + KeyName: aws.String("template-key-v2"), + }, + }) + require.NoError(t, err, "create-launch-template-version 2") + + // Version omitted -> $Default, which CreateLaunchTemplateVersion never + // moved off version 1. + _, err = gw.EC2Client(t).RunInstances(&ec2.RunInstancesInput{ + MinCount: aws.Int64(1), + MaxCount: aws.Int64(1), + LaunchTemplate: &ec2.LaunchTemplateSpecification{ + LaunchTemplateName: aws.String(templateName), + }, + }) + require.NoError(t, err, "run-instances with default launch-template version") + gotDefault := awaitLaunchTemplateNodeInput(t, nodeCh) + require.Equal(t, amiV1, aws.StringValue(gotDefault.ImageId), "omitted Version must resolve to the template default (v1)") + + // Explicit Version="2" must resolve the second version's data instead. + _, err = gw.EC2Client(t).RunInstances(&ec2.RunInstancesInput{ + MinCount: aws.Int64(1), + MaxCount: aws.Int64(1), + LaunchTemplate: &ec2.LaunchTemplateSpecification{ + LaunchTemplateName: aws.String(templateName), + Version: aws.String("2"), + }, + }) + require.NoError(t, err, "run-instances with explicit launch-template version 2") + gotV2 := awaitLaunchTemplateNodeInput(t, nodeCh) + require.Equal(t, amiV2, aws.StringValue(gotV2.ImageId), "explicit Version=2 must resolve version 2's data") +} + +// TestRunInstances_LaunchTemplateFieldPrecedence proves mergeRunInstancesInput +// overlays direct RunInstances parameters onto the template-derived base: +// a field left nil on the direct request inherits the template, but a field +// explicitly set on the direct request wins over the template's value. +func TestRunInstances_LaunchTemplateFieldPrecedence(t *testing.T) { + gw := StartGateway(t) + StartLaunchTemplateDaemonLite(t, gw) + + const ( + templateName = "lt-field-precedence" + instanceType = "t3.micro" + nodeID = "lt-node" + templateAMI = "ami-0000000000000fff" + ) + + gw.StubSubject(t, "spinifex.node.status", mustMarshal(t, &types.NodeStatusResponse{ + Node: nodeID, + InstanceTypes: []types.InstanceTypeCap{{Name: instanceType, Available: 5}}, + })) + nodeCh := captureLaunchTemplateNodeInput(t, gw, instanceType, nodeID) + + _, err := gw.EC2Client(t).CreateLaunchTemplate(&ec2.CreateLaunchTemplateInput{ + LaunchTemplateName: aws.String(templateName), + LaunchTemplateData: &ec2.RequestLaunchTemplateData{ + ImageId: aws.String(templateAMI), + InstanceType: aws.String(instanceType), + KeyName: aws.String("template-key"), + }, + }) + require.NoError(t, err, "create-launch-template") + + _, err = gw.EC2Client(t).RunInstances(&ec2.RunInstancesInput{ + MinCount: aws.Int64(1), + MaxCount: aws.Int64(1), + KeyName: aws.String("explicit-key"), + LaunchTemplate: &ec2.LaunchTemplateSpecification{ + LaunchTemplateName: aws.String(templateName), + }, + }) + require.NoError(t, err, "run-instances overriding KeyName") + + got := awaitLaunchTemplateNodeInput(t, nodeCh) + require.Equal(t, "explicit-key", aws.StringValue(got.KeyName), "an explicit direct-request field must override the template's value") + require.Equal(t, templateAMI, aws.StringValue(got.ImageId), "a field left unset on the direct request must inherit the template") +}