Files
spinifex/tests/integration/vpc_topology_test.go
T
Josh Siviour 62e7f983ee test: assert the gateway->vpcd->OVN crossing in the integration tier (#572)
* test: assert the gateway->vpcd->OVN crossing in the integration tier

The tier already ran the real VPCServiceImpl for the AWS-facing half, but
nothing consumed the vpc.* events it published: the SG topics were answered
by a canned {"success":true} and the fire-and-forget topology topics were
dropped. Both sides of that wire were tested only against stubs, so the join
was asserted nowhere below the live e2e tier.

Wire a real subscribers.Subscriber over a real in-process OVN Northbound DB
(ovntest.StartNB), opt-in per test so only tests that assert OVN state pay
the startup cost. StartDaemonLite gains WithRealVPCD to suppress its stubs,
since a stub and the subscriber both answering one request-reply would race.

Scope is the crossing, not NB row shapes — those stay with the reconcile
scenario tests. Everything host-dependent (ovn-controller, OVS, taps, netns,
nftables, packet paths) remains the live tier's claim.

* fix: make the integration tier re-runnable and quiet the ensure probe

Two defects the vpcd/OVN work surfaced.

The tier was green at -count=1 and red package-wide at -count=2. Test
accounts were keyed by t.Name(), which is unique among tests but repeats
across passes in one TestMain, so a second pass inherited the first pass's
JetStream namespace — including an ECR signing key sealed with a master key
StartGateway regenerates per call. Key each account off a counter instead.
TestRunInstances_ClientTokenIdempotency then collided with itself for the
same reason: its dedup store outlives a pass, so its fixed ClientToken made
the second pass's opening launch look like a replay. Suffix the tokens per
invocation.

ensureNamedRowOps' wait-op is built to fail fast when the row it guards
already exists, which is the ordinary idempotent path, but transactOps
reported it as "OVSDB operation failed" at ERROR. Every re-ensure emitted
one, drowning real faults in the log sink. Classify that one result as the
probe outcome it is and log it at Debug.

* test: cover transactOps' failed-result paths

The classifier split one log line into two branches and neither was
exercised: transactOps sat at 31% because EnsureLogicalRouter's cache
lookup absorbs a re-ensure before it ever transacts, so no unit test
reached a failed operation result.

Drive both kinds directly against a real NB — the guard probe finding an
existing row, and two same-named inserts violating the schema's name
index — since only the second is a fault and they now log at different
levels. transactOps goes to 94%; the remainder is the transport-error
return, which needs a broken connection to reach.
2026-07-22 18:17:24 +10:00

118 lines
3.9 KiB
Go

//go:build integration
package integration
import (
"context"
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mulgadc/spinifex/spinifex/network/topology"
"github.com/stretchr/testify/require"
)
// startVPCDStack wires the full API-to-OVN chain: the real gateway router, a
// real vpcd event consumer over a real OVN NB DB, and the real VPC service
// impl answering the ec2.* subjects.
//
// Order matters. StartVPCDLite runs first because StartDaemonLite's
// EnsureDefaultVPC synchronously requests vpc.create-sg, which now has a real
// responder rather than a canned ack.
func startVPCDStack(t *testing.T) (*VPCDLite, *ec2.EC2) {
t.Helper()
gw := StartGateway(t)
vpcd := StartVPCDLite(t, gw)
StartDaemonLite(t, gw, WithRealVPCD())
return vpcd, gw.EC2Client(t)
}
// TestVPCD_VPCLifecycleReachesOVN asserts CreateVpc/DeleteVpc travel the whole
// chain — SigV4 request, gateway, ec2.CreateVpc, VPCServiceImpl, vpc.create,
// subscriber, topology manager, OVSDB transaction — and land as a logical
// router carrying the VPC's identity.
//
// Row contents beyond identity are network/reconcile's scenario tests to
// assert; what is unique here is that the wire between the two services
// carries the event at all.
func TestVPCD_VPCLifecycleReachesOVN(t *testing.T) {
vpcd, c := startVPCDStack(t)
vpcID := createScratchVPC(t, c, "10.42.0.0/16")
router := topology.VPCRouter(vpcID)
awaitNB(t, "logical router "+router, func(ctx context.Context) error {
lr, err := vpcd.OVN.GetLogicalRouter(ctx, router)
if err != nil {
return err
}
if got := lr.ExternalIDs["spinifex:vpc_id"]; got != vpcID {
return fmt.Errorf("spinifex:vpc_id = %q, want %q", got, vpcID)
}
if got := lr.ExternalIDs["spinifex:cidr"]; got != "10.42.0.0/16" {
return fmt.Errorf("spinifex:cidr = %q, want 10.42.0.0/16", got)
}
return nil
})
_, err := c.DeleteVpc(&ec2.DeleteVpcInput{VpcId: aws.String(vpcID)})
require.NoError(t, err, "delete-vpc %s", vpcID)
awaitNB(t, "logical router "+router+" removed", func(ctx context.Context) error {
if _, err := vpcd.OVN.GetLogicalRouter(ctx, router); err == nil {
return fmt.Errorf("logical router %s still present", router)
}
return nil
})
}
// TestVPCD_SubnetLifecycleReachesOVN asserts CreateSubnet/DeleteSubnet reach
// OVN as a logical switch attached to the owning VPC's router.
func TestVPCD_SubnetLifecycleReachesOVN(t *testing.T) {
vpcd, c := startVPCDStack(t)
vpcID := createScratchVPC(t, c, "10.43.0.0/16")
out, err := c.CreateSubnet(&ec2.CreateSubnetInput{
VpcId: aws.String(vpcID),
CidrBlock: aws.String("10.43.1.0/24"),
})
require.NoError(t, err, "create-subnet in %s", vpcID)
require.NotNil(t, out.Subnet, "create-subnet returned nil Subnet")
subnetID := aws.StringValue(out.Subnet.SubnetId)
require.NotEmpty(t, subnetID, "SubnetId empty")
sw := topology.SubnetSwitch(subnetID)
awaitNB(t, "logical switch "+sw, func(ctx context.Context) error {
ls, err := vpcd.OVN.GetLogicalSwitch(ctx, sw)
if err != nil {
return err
}
if got := ls.ExternalIDs["spinifex:subnet_id"]; got != subnetID {
return fmt.Errorf("spinifex:subnet_id = %q, want %q", got, subnetID)
}
return nil
})
// The subnet's default gateway terminates on an LRP of the VPC router, so
// its presence is what proves the switch was joined to the right VPC
// rather than merely created.
lrp := topology.SubnetRouterPort(subnetID)
awaitNB(t, "logical router port "+lrp, func(ctx context.Context) error {
_, err := vpcd.OVN.GetLogicalRouterPort(ctx, lrp)
return err
})
_, err = c.DeleteSubnet(&ec2.DeleteSubnetInput{SubnetId: aws.String(subnetID)})
require.NoError(t, err, "delete-subnet %s", subnetID)
awaitNB(t, "logical switch "+sw+" removed", func(ctx context.Context) error {
if _, err := vpcd.OVN.GetLogicalSwitch(ctx, sw); err == nil {
return fmt.Errorf("logical switch %s still present", sw)
}
return nil
})
}