mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-20 11:21:49 +00:00
test(integration): migrate launch-template RunInstances expansion off live guests
RunInstances --launch-template resolves ImageId/InstanceType from the stored template entirely gateway-side (expandLaunchTemplate, before validation/placement/the daemon-facing NATS hop), so proving it forwards the resolved fields never needed a real guest boot. Ports the assertion to tests/integration via a new opt-in StartLaunchTemplateDaemonLite (real LaunchTemplateServiceImpl over embedded JetStream) plus a direct subscription on the per-node launch subject to capture what the gateway actually dispatched, rather than only the client-visible response. The live test keeps its CRUD/versioning lifecycle (create/modify/delete version, tag-filtered describe) — none of it touches a guest — and drops only the RunInstances-from-template + wait-for-running + terminate tail, whose only job was waiting out a real boot to read back fields already proven resolved.
This commit is contained in:
@@ -160,40 +160,13 @@ func runLaunchTemplates(t *testing.T, fix *Fixture) {
|
||||
require.Len(t, latestOut.LaunchTemplateVersions, 1)
|
||||
assert.Equal(t, int64(2), aws.Int64Value(latestOut.LaunchTemplateVersions[0].VersionNumber))
|
||||
|
||||
vpc := harness.EnsureDefaultVPC(t, fix.Harness)
|
||||
require.NotEmpty(t, vpc.SubnetID)
|
||||
require.NotEmpty(t, vpc.SGID)
|
||||
|
||||
harness.Step(t, "run-instances from default launch-template version")
|
||||
// e2e:allow-create — the instance verifies launch-template expansion in the real launch path.
|
||||
runOut, err := fix.AWS.EC2.RunInstances(&ec2.RunInstancesInput{
|
||||
LaunchTemplate: &ec2.LaunchTemplateSpecification{LaunchTemplateId: aws.String(templateID)},
|
||||
MinCount: aws.Int64(1),
|
||||
MaxCount: aws.Int64(1),
|
||||
SubnetId: aws.String(vpc.SubnetID),
|
||||
SecurityGroupIds: []*string{
|
||||
aws.String(vpc.SGID),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "run-instances with launch template")
|
||||
require.NotEmpty(t, runOut.Instances, "run-instances returned no instances")
|
||||
instanceID := aws.StringValue(runOut.Instances[0].InstanceId)
|
||||
require.NotEmpty(t, instanceID, "run-instances returned an empty instance id")
|
||||
t.Cleanup(func() {
|
||||
_, _ = fix.AWS.EC2.TerminateInstances(&ec2.TerminateInstancesInput{
|
||||
InstanceIds: []*string{aws.String(instanceID)},
|
||||
})
|
||||
})
|
||||
|
||||
instance := harness.WaitForInstanceState(t, fix.AWS, instanceID, "running")
|
||||
assert.Equal(t, amiID, aws.StringValue(instance.ImageId), "templated AMI")
|
||||
assert.Equal(t, instanceType, aws.StringValue(instance.InstanceType), "templated instance type")
|
||||
|
||||
_, err = fix.AWS.EC2.TerminateInstances(&ec2.TerminateInstancesInput{
|
||||
InstanceIds: []*string{aws.String(instanceID)},
|
||||
})
|
||||
require.NoError(t, err, "terminate launch-template instance")
|
||||
harness.WaitForInstanceState(t, fix.AWS, instanceID, "terminated")
|
||||
// RunInstances from the default launch-template version (resolving the
|
||||
// template's ImageId/InstanceType onto the per-node launch dispatch, then
|
||||
// waiting for a real boot to read them back off DescribeInstances) is
|
||||
// covered by tests/integration's
|
||||
// TestRunInstances_LaunchTemplateExpansion — that expansion is gateway-side
|
||||
// logic, fully resolved before the daemon-facing NATS hop, so a live guest
|
||||
// proves nothing beyond what the integration tier already asserts.
|
||||
|
||||
harness.Step(t, "delete-launch-template by name")
|
||||
deleteOut, err := fix.AWS.EC2.DeleteLaunchTemplate(&ec2.DeleteLaunchTemplateInput{
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//go:build integration
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/config"
|
||||
handlers_ec2_launchtemplate "github.com/mulgadc/spinifex/spinifex/handlers/ec2/launchtemplate"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// StartLaunchTemplateDaemonLite subscribes a real
|
||||
// handlers_ec2_launchtemplate.LaunchTemplateServiceImpl — the same production
|
||||
// code a live daemon runs (daemon/daemon_handlers_launchtemplate.go) — to
|
||||
// every ec2.*LaunchTemplate* subject the gateway's NATSLaunchTemplateService
|
||||
// client calls (gateway/ec2/launchtemplate/*.go, RunInstances.go's
|
||||
// expandLaunchTemplate). Unlike StubSubject's static canned JSON, this
|
||||
// exercises the real KV-backed CRUD and version-resolution logic, matching
|
||||
// how RunInstances resolves a referenced template into effective ImageId/
|
||||
// InstanceType before the per-node launch dispatch.
|
||||
//
|
||||
// Kept separate from StartDaemonLite (rather than folded into it) following
|
||||
// StartVolumeDaemonLite's precedent: only a test that actually exercises
|
||||
// launch templates should pay for wiring this service.
|
||||
func StartLaunchTemplateDaemonLite(t *testing.T, gw *Gateway) *handlers_ec2_launchtemplate.LaunchTemplateServiceImpl {
|
||||
t.Helper()
|
||||
|
||||
cfg := &config.Config{AZ: testAZ}
|
||||
svc, err := handlers_ec2_launchtemplate.NewLaunchTemplateServiceImplWithNATS(cfg, gw.NATSConn)
|
||||
require.NoError(t, err, "construct launch template service")
|
||||
|
||||
nc := gw.NATSConn
|
||||
sub(t, nc, "ec2.CreateLaunchTemplate", func(m *nats.Msg) { dispatch(m, svc.CreateLaunchTemplate) })
|
||||
sub(t, nc, "ec2.CreateLaunchTemplateVersion", func(m *nats.Msg) { dispatch(m, svc.CreateLaunchTemplateVersion) })
|
||||
sub(t, nc, "ec2.DeleteLaunchTemplate", func(m *nats.Msg) { dispatch(m, svc.DeleteLaunchTemplate) })
|
||||
sub(t, nc, "ec2.DeleteLaunchTemplateVersions", func(m *nats.Msg) { dispatch(m, svc.DeleteLaunchTemplateVersions) })
|
||||
sub(t, nc, "ec2.ModifyLaunchTemplate", func(m *nats.Msg) { dispatch(m, svc.ModifyLaunchTemplate) })
|
||||
sub(t, nc, "ec2.DescribeLaunchTemplates", func(m *nats.Msg) { dispatch(m, svc.DescribeLaunchTemplates) })
|
||||
sub(t, nc, "ec2.DescribeLaunchTemplateVersions", func(m *nats.Msg) { dispatch(m, svc.DescribeLaunchTemplateVersions) })
|
||||
|
||||
return svc
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
//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_LaunchTemplateExpansion asserts the launch-template-facing
|
||||
// slice of RunInstances: CreateLaunchTemplate returns a real template id and
|
||||
// DefaultVersionNumber=1 (LaunchTemplateServiceImpl, wired via
|
||||
// StartLaunchTemplateDaemonLite — real KV-backed logic, not a static stub),
|
||||
// and RunInstances with only a LaunchTemplateSpecification resolves the
|
||||
// template's ImageId/InstanceType and forwards them to the per-node launch —
|
||||
// gateway/ec2/instance/RunInstances.go's expandLaunchTemplate, which calls
|
||||
// handlers_ec2_launchtemplate.ExpandRunInstances over NATS before validation,
|
||||
// routing, or placement ever see the request. That resolution is complete
|
||||
// before the daemon-facing NATS hop, so a real guest is not needed to prove it
|
||||
// happened — only that the per-node request actually carries the resolved
|
||||
// fields rather than the direct RunInstancesInput ones (which are absent
|
||||
// here: ImageId/InstanceType are supplied only via the template).
|
||||
//
|
||||
// The live test's EnsureDefaultVPC/RunInstances/WaitForInstanceState(running)/
|
||||
// Terminate/WaitForInstanceState(terminated) sequence is left behind: once the
|
||||
// per-node request is known to carry the resolved AMI and instance type, its
|
||||
// only remaining job was waiting out a real guest boot to read them back off
|
||||
// DescribeInstances — teardown hygiene for a real throwaway VM, not a
|
||||
// distinct assertion. The template CRUD/versioning lifecycle (create version,
|
||||
// modify default version, delete a version, tag-filtered describe) never
|
||||
// touched a guest and is unaffected — it stays in
|
||||
// tests/e2e/single/launchtemplate_test.go.
|
||||
func TestRunInstances_LaunchTemplateExpansion(t *testing.T) {
|
||||
gw := StartGateway(t)
|
||||
StartLaunchTemplateDaemonLite(t, gw)
|
||||
|
||||
const (
|
||||
amiID = "ami-0123456789abcdef0"
|
||||
instanceType = "t3.micro"
|
||||
nodeID = "node-1"
|
||||
)
|
||||
|
||||
client := gw.EC2Client(t)
|
||||
|
||||
createOut, err := client.CreateLaunchTemplate(&ec2.CreateLaunchTemplateInput{
|
||||
LaunchTemplateName: aws.String("integration-lt-run-instances"),
|
||||
LaunchTemplateData: &ec2.RequestLaunchTemplateData{
|
||||
ImageId: aws.String(amiID),
|
||||
InstanceType: aws.String(instanceType),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "create-launch-template")
|
||||
require.NotNil(t, createOut.LaunchTemplate, "create-launch-template returned no template")
|
||||
templateID := aws.StringValue(createOut.LaunchTemplate.LaunchTemplateId)
|
||||
require.NotEmpty(t, templateID, "create-launch-template returned an empty id")
|
||||
require.Equal(t, int64(1), aws.Int64Value(createOut.LaunchTemplate.DefaultVersionNumber))
|
||||
|
||||
// A single node reporting capacity for instanceType — enough for
|
||||
// distributeInstances to resolve MinCount=MaxCount=1 onto one node.
|
||||
statusResp := mustMarshal(t, &types.NodeStatusResponse{
|
||||
Node: nodeID,
|
||||
InstanceTypes: []types.InstanceTypeCap{
|
||||
{Name: instanceType, Available: 1},
|
||||
},
|
||||
})
|
||||
gw.StubSubject(t, "spinifex.node.status", statusResp)
|
||||
|
||||
// Subscribe directly to the per-node launch subject (rather than
|
||||
// StubSubject's static responder) so the captured ImageId/InstanceType
|
||||
// prove the gateway forwarded the template-resolved values, not the
|
||||
// degenerate zero-daemon path (also InsufficientInstanceCapacity — see
|
||||
// placement.go:44) and not a stub that ignores its input.
|
||||
var gotImageID, gotInstanceType string
|
||||
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
|
||||
}
|
||||
gotImageID = aws.StringValue(nodeInput.ImageId)
|
||||
gotInstanceType = aws.StringValue(nodeInput.InstanceType)
|
||||
_ = msg.Respond(mustMarshal(t, &ec2.Reservation{
|
||||
ReservationId: aws.String("r-launch-template"),
|
||||
Instances: []*ec2.Instance{
|
||||
{InstanceId: aws.String("i-launch-template-1")},
|
||||
},
|
||||
}))
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = sub.Unsubscribe() })
|
||||
|
||||
// Deliberately no ImageId/InstanceType here — only the template reference.
|
||||
// If expandLaunchTemplate did not run, ValidateRunInstancesInput would
|
||||
// reject the request for a missing ImageId/InstanceType before ever
|
||||
// reaching the per-node subject.
|
||||
out, err := client.RunInstances(&ec2.RunInstancesInput{
|
||||
LaunchTemplate: &ec2.LaunchTemplateSpecification{LaunchTemplateId: aws.String(templateID)},
|
||||
MinCount: aws.Int64(1),
|
||||
MaxCount: aws.Int64(1),
|
||||
})
|
||||
require.NoError(t, err, "run-instances --launch-template")
|
||||
require.Lenf(t, out.Instances, 1, "expected 1 instance from run-instances, got %d", len(out.Instances))
|
||||
require.NotEmpty(t, aws.StringValue(out.Instances[0].InstanceId), "InstanceId empty")
|
||||
|
||||
require.Equal(t, amiID, gotImageID, "gateway must resolve the template's ImageId onto the per-node launch")
|
||||
require.Equal(t, instanceType, gotInstanceType, "gateway must resolve the template's InstanceType onto the per-node launch")
|
||||
}
|
||||
Reference in New Issue
Block a user