mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-23 04:36:40 +00:00
237e78cb7c
TestFinalClusterStats and TestInstanceLaunch assert nothing beyond what other callers of their shared helpers already imply. The multinode baseline, volume, guest-SSH, and preflight tests duplicate single-node coverage or exercise per-guest behaviour that isn't cluster-specific. TestMultinodeInstanceDistribution can't fail: three independent RunInstances calls with no placement group make a strict spread assertion flaky and a loose one meaningless. Preflight checks (/dev/kvm writability, peer SSH reachability) move into requireMultiNodeFixture so the package fixture singleton still fails fast with a clear message instead of disappearing along with the dedicated test. The remaining callers of needInstanceTrio (CrossNodeOps, CrossNodeGateway, NodeFailure, VolumeDurability) still trigger the shared trio launch on their own, so removing the distribution and volume-lifecycle tests doesn't leave it unbuilt. Also slims the manifest-lint baseline entry for the deleted multinode volume test.
159 lines
5.9 KiB
Go
159 lines
5.9 KiB
Go
//go:build e2e
|
|
|
|
package multinode
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/service/ec2"
|
|
"github.com/mulgadc/spinifex/tests/e2e/harness"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Fresh-install reachability baseline. Run before needInstanceTrio so the
|
|
// default SG/subnet/route-table are in their pristine state.
|
|
//
|
|
// - runMultinodeSameSGCrossHostComms: proves the default SG self-reference
|
|
// rule spans chassis — ICMP between two instances on different nodes
|
|
// succeeds with no added ingress rule.
|
|
|
|
// baselineLaunch launches one instance with the given SGs into subnetID,
|
|
// registers a terminate-and-wait cleanup, and returns the instance ID once "running".
|
|
func baselineLaunch(t *testing.T, fix *Fixture, amiID, instType, keyName, subnetID string, sgIDs []string) string {
|
|
t.Helper()
|
|
sgs := make([]*string, 0, len(sgIDs))
|
|
for _, id := range sgIDs {
|
|
sgs = append(sgs, aws.String(id))
|
|
}
|
|
input := &ec2.RunInstancesInput{
|
|
ImageId: aws.String(amiID),
|
|
InstanceType: aws.String(instType),
|
|
KeyName: aws.String(keyName),
|
|
SubnetId: aws.String(subnetID),
|
|
SecurityGroupIds: sgs,
|
|
MinCount: aws.Int64(1),
|
|
MaxCount: aws.Int64(1),
|
|
}
|
|
var id string
|
|
for attempt := 1; attempt <= 6; attempt++ {
|
|
out, err := fix.AWS.EC2.RunInstances(input)
|
|
if err == nil {
|
|
require.NotEmpty(t, out.Instances, "RunInstances returned no instances")
|
|
id = aws.StringValue(out.Instances[0].InstanceId)
|
|
break
|
|
}
|
|
if !strings.Contains(err.Error(), "InsufficientInstanceCapacity") {
|
|
t.Fatalf("RunInstances: %v", err)
|
|
}
|
|
t.Logf("baselineLaunch attempt %d: InsufficientInstanceCapacity, retrying", attempt)
|
|
time.Sleep(10 * time.Second)
|
|
}
|
|
require.NotEmpty(t, id, "RunInstances never succeeded")
|
|
t.Cleanup(func() {
|
|
if _, err := fix.AWS.EC2.TerminateInstances(&ec2.TerminateInstancesInput{
|
|
InstanceIds: []*string{aws.String(id)},
|
|
}); err != nil {
|
|
t.Errorf("terminate instance %s: %v", id, err)
|
|
return
|
|
}
|
|
harness.WaitForInstanceState(t, fix.AWS, id, "terminated")
|
|
})
|
|
harness.WaitForInstanceState(t, fix.AWS, id, "running")
|
|
return id
|
|
}
|
|
|
|
// instancePrivateIP returns the instance's primary VPC private IP.
|
|
func instancePrivateIP(t *testing.T, fix *Fixture, id string) string {
|
|
t.Helper()
|
|
out, err := fix.AWS.EC2.DescribeInstances(&ec2.DescribeInstancesInput{
|
|
InstanceIds: []*string{aws.String(id)},
|
|
})
|
|
require.NoError(t, err, "describe-instances %s", id)
|
|
require.NotEmpty(t, out.Reservations, "no reservations for %s", id)
|
|
require.NotEmpty(t, out.Reservations[0].Instances, "no instances for %s", id)
|
|
ip := aws.StringValue(out.Reservations[0].Instances[0].PrivateIpAddress)
|
|
require.NotEmptyf(t, ip, "instance %s has no private IP", id)
|
|
return ip
|
|
}
|
|
|
|
// sshCapture runs cmd over SSH and returns combined output + error without fataling.
|
|
func sshCapture(pem, user, host string, port int, cmd string) (string, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
out, err := harness.RunGuestSSH(ctx, harness.SSHTarget{
|
|
User: user, Host: host, Port: port, KeyPath: pem,
|
|
}, cmd)
|
|
return string(out), err
|
|
}
|
|
|
|
// runMultinodeSameSGCrossHostComms launches two instances on different nodes sharing
|
|
// the default SG and asserts ICMP-ping succeeds across hosts. ICMP is permitted only
|
|
// by the default SG self-reference rule, proving it is enforced across chassis.
|
|
func runMultinodeSameSGCrossHostComms(t *testing.T, fix *Fixture) {
|
|
harness.Phase(t, "Multinode — Baseline: same default-SG instances communicate across hosts")
|
|
|
|
vpcID, defSGID, subnetID := harness.DiscoverDefaultVPC(t, fix.AWS)
|
|
instType, arch := needInstanceTypeArch(t, fix)
|
|
amiID := needAMI(t, fix, arch)
|
|
keyName, keyPath := needKeyPair(t, fix)
|
|
|
|
// Dedicated SG for runner SSH — opens tcp/22 only; ICMP between guests
|
|
// still depends on the default SG self-ingress, keeping the signal clean.
|
|
runnerSG := harness.EnsureSG(t, fix.Harness, vpcID, "baseline-runnersg")
|
|
harness.AuthorizeSSHIngress(t, fix.AWS, runnerSG)
|
|
|
|
// Launch instances until two land on distinct nodes. Bounded so a degenerate
|
|
// single-node placement can't loop forever.
|
|
type placed struct {
|
|
id string
|
|
node string
|
|
}
|
|
var instances []placed
|
|
var srcIdx, dstIdx = -1, -1
|
|
for attempt := 0; attempt < 4 && (srcIdx < 0 || dstIdx < 0); attempt++ {
|
|
id := baselineLaunch(t, fix, amiID, instType, keyName, subnetID, []string{defSGID, runnerSG})
|
|
node := harness.InstanceHostingNode(t, fix.Cluster, id)
|
|
nodeName := ""
|
|
if node != nil {
|
|
nodeName = node.Name
|
|
}
|
|
instances = append(instances, placed{id: id, node: nodeName})
|
|
// Re-scan for a distinct-node pair.
|
|
srcIdx, dstIdx = -1, -1
|
|
for i := range instances {
|
|
for j := range instances {
|
|
if i != j && instances[i].node != "" && instances[i].node != instances[j].node {
|
|
srcIdx, dstIdx = i, j
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if srcIdx < 0 || dstIdx < 0 {
|
|
t.Skipf("could not place two instances on distinct nodes (got %v); scheduler colocated", instances)
|
|
}
|
|
src, dst := instances[srcIdx], instances[dstIdx]
|
|
harness.Detail(t, "src", src.id, "src_node", src.node, "dst", dst.id, "dst_node", dst.node)
|
|
|
|
dstPriv := instancePrivateIP(t, fix, dst.id)
|
|
|
|
// Shell into the source instance via its dedicated runner-SSH SG.
|
|
host, port := harness.GuestSSHEndpoint(t, fix.AWS, fix.Cluster, src.id)
|
|
harness.GuestSSHReady(t, host, port, "ubuntu", keyPath,
|
|
harness.WithTimeout(3*time.Minute), harness.WithPoll(3*time.Second))
|
|
|
|
harness.Step(t, "ping %s (%s) from %s across hosts via default-SG self-ingress", dst.id, dstPriv, src.id)
|
|
out, err := sshCapture(keyPath, "ubuntu", host, port,
|
|
fmt.Sprintf("ping -c 3 -W 2 %s", dstPriv))
|
|
require.NoErrorf(t, err,
|
|
"cross-host ping %s -> %s failed; default SG self-ingress not enforced across chassis\n%s",
|
|
src.id, dst.id, out)
|
|
assert.Containsf(t, out, "0% packet loss",
|
|
"cross-host ping had loss; default SG self-ingress datapath degraded\n%s", out)
|
|
}
|