diff --git a/spinifex/network/reconcile/apply.go b/spinifex/network/reconcile/apply.go index 43fa44770..f383b01fe 100644 --- a/spinifex/network/reconcile/apply.go +++ b/spinifex/network/reconcile/apply.go @@ -762,6 +762,14 @@ func (r *reconciler) ensureGuestPortDatapath(ctx context.Context, spec policy.EI "retry_in_ms", otelsetup.Millis(time.Until(until))) return } + // No LSP, nothing to bind: a recompute cannot conjure one, so probing would + // burn the whole deadline. applyPorts refuses a port whose SG policy is + // unprogrammable, which lands here for every public-IP guest it skipped. + if _, err := r.ovn.GetLogicalSwitchPort(ctx, lspName); err != nil { + slog.Warn("reconcile/apply: guest LSP absent; skipping datapath probe", + "vpc_id", vpcID, "lsp", lspName, "eni_id", eniIDFromPort(lspName), "err", err) + return + } deadline := time.Now().Add(guestPortDatapathTimeout) nudged := false misses := 0 diff --git a/spinifex/network/reconcile/apply_gateway_claim_test.go b/spinifex/network/reconcile/apply_gateway_claim_test.go index 515e0fd46..d7bccec68 100644 --- a/spinifex/network/reconcile/apply_gateway_claim_test.go +++ b/spinifex/network/reconcile/apply_gateway_claim_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/mulgadc/spinifex/spinifex/network/ovn/mock" "github.com/mulgadc/spinifex/spinifex/network/ovn/nbdb" "github.com/mulgadc/spinifex/spinifex/network/policy" ) @@ -130,6 +131,23 @@ func withFastGuestPortBounds(t *testing.T) { t.Cleanup(func() { guestPortDatapathTimeout, guestPortDatapathInterval = to, iv }) } +// ovnWithGuestLSP returns a mock OVN client carrying lspName, the precondition +// ensureGuestPortDatapath now checks before it starts nudging. +func ovnWithGuestLSP(t *testing.T, lspName string) *mock.Client { + t.Helper() + m := mock.New() + if err := m.Connect(context.Background()); err != nil { + t.Fatalf("mock Connect: %v", err) + } + if err := m.CreateLogicalSwitch(context.Background(), &nbdb.LogicalSwitch{Name: "ls-test"}); err != nil { + t.Fatalf("CreateLogicalSwitch: %v", err) + } + if err := m.CreateLogicalSwitchPort(context.Background(), "ls-test", &nbdb.LogicalSwitchPort{Name: lspName}); err != nil { + t.Fatalf("CreateLogicalSwitchPort: %v", err) + } + return m +} + func withFastDatapathBounds(t *testing.T) { t.Helper() to, iv := gatewayDatapathTimeout, gatewayDatapathInterval @@ -402,10 +420,32 @@ func TestEnsureGuestPortDatapath_EmptyLSPIsNoop(t *testing.T) { } } +// applyPorts refuses a port whose SG policy is unprogrammable, so applyEIPs then +// runs for a public-IP guest with no LSP. A recompute cannot bind a port that was +// never created, so probing it would burn the whole deadline for nothing. +func TestEnsureGuestPortDatapath_AbsentLSPSkipsProbe(t *testing.T) { + withFastGuestPortBounds(t) + f := &fakeClaimVerifier{guestUpAfter: -1} + r := &reconciler{gwClaim: f, ovn: ovnWithGuestLSP(t, "port-eni-other")} + + start := time.Now() + r.ensureGuestPortDatapath(context.Background(), policy.EIPSpec{VPCID: "vpc-a", PortName: "port-eni-1"}) + + if elapsed := time.Since(start); elapsed >= guestPortDatapathTimeout { + t.Errorf("elapsed = %s, want well under the %s deadline", elapsed, guestPortDatapathTimeout) + } + if f.guestChecks != 0 { + t.Errorf("guestChecks = %d, want 0 (no LSP to probe)", f.guestChecks) + } + if f.nudges != 0 { + t.Errorf("nudges = %d, want 0 (a recompute cannot create an LSP)", f.nudges) + } +} + func TestEnsureGuestPortDatapath_UpNoNudge(t *testing.T) { withFastGuestPortBounds(t) f := &fakeClaimVerifier{guestUpAfter: 0} - r := &reconciler{gwClaim: f} + r := &reconciler{gwClaim: f, ovn: ovnWithGuestLSP(t, "port-eni-1")} r.ensureGuestPortDatapath(context.Background(), policy.EIPSpec{VPCID: "vpc-a", PortName: "port-eni-1"}) @@ -424,7 +464,7 @@ func TestEnsureGuestPortDatapath_NudgeThenConverge(t *testing.T) { withFastGuestPortBounds(t) // Down until one recompute nudge, then up. f := &fakeClaimVerifier{guestUpAfter: 1} - r := &reconciler{gwClaim: f} + r := &reconciler{gwClaim: f, ovn: ovnWithGuestLSP(t, "port-eni-1")} r.ensureGuestPortDatapath(context.Background(), policy.EIPSpec{VPCID: "vpc-a", PortName: "port-eni-1"}) @@ -436,7 +476,7 @@ func TestEnsureGuestPortDatapath_NudgeThenConverge(t *testing.T) { func TestEnsureGuestPortDatapath_NeverConvergesRecomputesEachMiss(t *testing.T) { withFastGuestPortBounds(t) f := &fakeClaimVerifier{guestUpAfter: -1} // never up - r := &reconciler{gwClaim: f} + r := &reconciler{gwClaim: f, ovn: ovnWithGuestLSP(t, "port-eni-1")} done := make(chan struct{}) go func() { @@ -462,7 +502,7 @@ func TestEnsureGuestPortDatapath_NeverConvergesRecomputesEachMiss(t *testing.T) func TestEnsureGuestPortDatapath_ProbeErrorBailsOut(t *testing.T) { withFastGuestPortBounds(t) f := &fakeClaimVerifier{guestErr: errors.New("ovn-sbctl down")} - r := &reconciler{gwClaim: f} + r := &reconciler{gwClaim: f, ovn: ovnWithGuestLSP(t, "port-eni-1")} r.ensureGuestPortDatapath(context.Background(), policy.EIPSpec{VPCID: "vpc-a", PortName: "port-eni-1"}) @@ -478,7 +518,7 @@ func TestEnsureGuestPortDatapath_ContextCancelStops(t *testing.T) { t.Cleanup(func() { guestPortDatapathTimeout, guestPortDatapathInterval = to, iv }) f := &fakeClaimVerifier{guestUpAfter: -1} - r := &reconciler{gwClaim: f} + r := &reconciler{gwClaim: f, ovn: ovnWithGuestLSP(t, "port-eni-1")} ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) @@ -550,7 +590,7 @@ func TestEnsureGuestPortDatapath_WedgedSBEscalatesResetOnce(t *testing.T) { withFastGuestPortBounds(t) withSmallSBResetThreshold(t) f := &fakeClaimVerifier{guestUpAfter: -1, sbNotConnected: true} - r := &reconciler{gwClaim: f} + r := &reconciler{gwClaim: f, ovn: ovnWithGuestLSP(t, "port-eni-1")} runToDeadline(t, func() { r.ensureGuestPortDatapath(context.Background(), policy.EIPSpec{VPCID: "vpc-a", PortName: "port-eni-1"}) @@ -565,7 +605,7 @@ func TestEnsureGuestPortDatapath_ConnectedSBNeverResets(t *testing.T) { withFastGuestPortBounds(t) withSmallSBResetThreshold(t) f := &fakeClaimVerifier{guestUpAfter: -1, sbNotConnected: false} - r := &reconciler{gwClaim: f} + r := &reconciler{gwClaim: f, ovn: ovnWithGuestLSP(t, "port-eni-1")} runToDeadline(t, func() { r.ensureGuestPortDatapath(context.Background(), policy.EIPSpec{VPCID: "vpc-a", PortName: "port-eni-1"}) diff --git a/spinifex/network/reconcile/apply_guest_port_backoff_test.go b/spinifex/network/reconcile/apply_guest_port_backoff_test.go index 0c2e360e1..66cec46f3 100644 --- a/spinifex/network/reconcile/apply_guest_port_backoff_test.go +++ b/spinifex/network/reconcile/apply_guest_port_backoff_test.go @@ -29,7 +29,7 @@ func TestEnsureGuestPortDatapath_BacksOffAfterFailure(t *testing.T) { withFastGuestPortBackoff(t, time.Minute, time.Hour) f := &fakeClaimVerifier{guestUpAfter: -1} // never up - r := &reconciler{gwClaim: f} + r := &reconciler{gwClaim: f, ovn: ovnWithGuestLSP(t, "port-eni-1")} spec := policy.EIPSpec{VPCID: "vpc-a", PortName: "port-eni-1"} r.ensureGuestPortDatapath(context.Background(), spec) @@ -72,7 +72,7 @@ func TestEnsureGuestPortDatapath_BackoffClearsOnConverge(t *testing.T) { withFastGuestPortBackoff(t, time.Millisecond, time.Millisecond) f := &fakeClaimVerifier{guestUpAfter: -1} - r := &reconciler{gwClaim: f} + r := &reconciler{gwClaim: f, ovn: ovnWithGuestLSP(t, "port-eni-1")} spec := policy.EIPSpec{VPCID: "vpc-a", PortName: "port-eni-1"} r.ensureGuestPortDatapath(context.Background(), spec)