Files
spinifex/tests/integration/vpcd_lite.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

89 lines
2.9 KiB
Go

//go:build integration
package integration
import (
"context"
"testing"
"time"
"github.com/mulgadc/spinifex/spinifex/network/ovn"
"github.com/mulgadc/spinifex/spinifex/network/subscribers"
vpcdfixture "github.com/mulgadc/spinifex/tests/fixtures/vpcd"
"github.com/stretchr/testify/require"
)
// VPCDLite is a real vpcd event consumer over a real in-process OVN
// Northbound DB. OVN is the connected client, for reading NB rows back.
type VPCDLite struct {
OVN ovn.Client
}
// StartVPCDLite subscribes a real network/subscribers.Subscriber — the same
// production type a live vpcd runs — to every vpc.* lifecycle topic, backed by
// managers driving a genuine in-process OVN NB DB.
//
// This closes the one seam the tier otherwise leaves open. StartDaemonLite
// already runs the real VPCServiceImpl for the AWS-facing half, so an API call
// is authenticated, validated and persisted for real; but the vpc.* events it
// publishes had no consumer, so the SG topics were answered by a canned ack
// and the fire-and-forget topology topics were dropped. With this wired, an
// SDK call is asserted all the way through to OVN NB rows.
//
// Call BEFORE StartDaemonLite, and pass WithRealVPCD to it — see the ordering
// and double-responder notes on that option.
//
// What is deliberately absent: ovn-controller, so nothing translates NB to SB
// to OpenFlow, and no OVS bridge, tap, netns or nftables rule exists. Correct
// NB rows do not imply correct flows; that remains the live tier's claim.
func StartVPCDLite(t *testing.T, gw *Gateway) *VPCDLite {
t.Helper()
fixture := vpcdfixture.Start(t)
subscriber, err := subscribers.New(fixture.Subscribers)
require.NoError(t, err, "construct vpcd subscriber")
subs, err := subscriber.Subscribe(gw.NATSConn)
require.NoError(t, err, "subscribe vpcd topics")
t.Cleanup(func() {
for _, s := range subs {
_ = s.Unsubscribe()
}
})
return &VPCDLite{OVN: fixture.OVN}
}
// nbPollInterval/nbPollTimeout bound the wait in awaitNB. Topology events are
// fire-and-forget (utils.PublishEvent), so there is no reply to synchronise
// on: the only honest assertion is to poll until the row appears. The timeout
// is generous relative to an in-process OVSDB round-trip, since it is only
// ever paid in full by a genuine failure.
const (
nbPollInterval = 10 * time.Millisecond
nbPollTimeout = 5 * time.Second
)
// awaitNB polls check until it returns nil, failing the test with the last
// error once nbPollTimeout elapses. Use it for anything driven by a
// fire-and-forget vpc.* event; the synchronous SG topics need no polling.
func awaitNB(t *testing.T, what string, check func(ctx context.Context) error) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), nbPollTimeout)
defer cancel()
var lastErr error
for {
if lastErr = check(ctx); lastErr == nil {
return
}
select {
case <-ctx.Done():
t.Fatalf("timed out waiting for %s: %v", what, lastErr)
case <-time.After(nbPollInterval):
}
}
}