Files
spinifex/tests/integration/run_instances_clienttoken_test.go
T

106 lines
4.3 KiB
Go

//go:build integration
package integration
import (
"fmt"
"sync/atomic"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
types "github.com/mulgadc/spinifex/contracts/cluster/v1"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/require"
)
// clientTokenRun numbers each invocation of the test below so its ClientToken
// values are fresh on every pass; see the test's doc comment.
var clientTokenRun atomic.Int64
// TestRunInstances_ClientTokenIdempotency proves ClientToken dedup
// (domains/ec2/awsapi/instance/RunInstances.go's ClientTokenStore wrapping) runs for
// real through the full gateway: a replayed token must not reach the daemon a
// second time, and a different token must launch again.
//
// getClientTokenStore (clienttoken.go) binds its JetStream KV bucket via a
// process-wide sync.Once, so the very first test in this package's binary
// that supplies a ClientToken permanently decides which test's NATS account
// backs the store for the rest of the run. No other test can collide with
// this one — every key is namespaced by accountID+token, and these token
// values appear nowhere else in the suite — but THIS test collides with
// itself under -count=N: the store outlives each pass, so a fixed token would
// make the second pass's opening launch look like a replay of the first
// pass's. Hence the per-invocation suffix.
func TestRunInstances_ClientTokenIdempotency(t *testing.T) {
gw := StartGateway(t)
run := clientTokenRun.Add(1)
const (
instanceType = "t3.micro"
nodeID = "ct-node"
)
statusResp := mustMarshal(t, &types.NodeStatusResponse{
Node: nodeID,
InstanceTypes: []types.InstanceTypeCap{
{Name: instanceType, Available: 10},
},
})
gw.StubSubject(t, "spinifex.node.status", statusResp)
// dispatchCount records how many times the daemon-side per-node launch
// subject actually fired, so a replayed ClientToken that (incorrectly)
// dispatched again is caught even though the SDK call still succeeds.
var dispatchCount atomic.Int64
launchSubject := fmt.Sprintf("ec2.RunInstances.%s.%s", instanceType, nodeID)
sub, err := gw.NATSConn.Subscribe(launchSubject, func(msg *nats.Msg) {
n := dispatchCount.Add(1)
_ = msg.Respond(mustMarshal(t, &ec2.Reservation{
ReservationId: aws.String(fmt.Sprintf("r-ct-%d", n)),
Instances: []*ec2.Instance{
{InstanceId: aws.String(fmt.Sprintf("i-ct-%d", n))},
},
}))
})
require.NoError(t, err)
t.Cleanup(func() { _ = sub.Unsubscribe() })
baseInput := func(token string) *ec2.RunInstancesInput {
return &ec2.RunInstancesInput{
ImageId: aws.String("ami-0123456789abcdef0"),
InstanceType: aws.String(instanceType),
MinCount: aws.Int64(1),
MaxCount: aws.Int64(1),
ClientToken: aws.String(token),
}
}
tokenA := fmt.Sprintf("integration-test-clienttoken-a-%d", run)
res1, err := gw.EC2Client(t).RunInstances(baseInput(tokenA))
require.NoError(t, err, "first launch with token A")
require.Len(t, res1.Instances, 1)
require.Equal(t, int64(1), dispatchCount.Load(), "first call must dispatch to the daemon once")
firstInstanceID := aws.StringValue(res1.Instances[0].InstanceId)
firstReservationID := aws.StringValue(res1.ReservationId)
// Replay: same token, same params. Must return the identical reservation
// without a second daemon dispatch.
res2, err := gw.EC2Client(t).RunInstances(baseInput(tokenA))
require.NoError(t, err, "replay with token A")
require.Equal(t, int64(1), dispatchCount.Load(), "replaying the same token must not dispatch again")
require.Equal(t, firstReservationID, aws.StringValue(res2.ReservationId), "replay must return the original reservation")
require.Len(t, res2.Instances, 1)
require.Equal(t, firstInstanceID, aws.StringValue(res2.Instances[0].InstanceId), "replay must return the original instance")
// A different token for the same account must launch again, independently.
tokenB := fmt.Sprintf("integration-test-clienttoken-b-%d", run)
res3, err := gw.EC2Client(t).RunInstances(baseInput(tokenB))
require.NoError(t, err, "launch with token B")
require.Equal(t, int64(2), dispatchCount.Load(), "a different token must dispatch a fresh launch")
require.Len(t, res3.Instances, 1)
require.NotEqual(t, firstInstanceID, aws.StringValue(res3.Instances[0].InstanceId),
"a different token must not replay token A's instance")
}