From a53160eb5b4f3c153e4d1b4696fa64ee32583132 Mon Sep 17 00:00:00 2001 From: squarepots <46488165+squarepots@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:53:47 +0800 Subject: [PATCH 1/5] Adopt verified remote routes without redeploying Signed-off-by: squarepots <46488165+squarepots@users.noreply.github.com> --- internal/steward/execution.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/internal/steward/execution.go b/internal/steward/execution.go index 91a7460..7c5f6ed 100644 --- a/internal/steward/execution.go +++ b/internal/steward/execution.go @@ -58,14 +58,14 @@ func deployRouteWithoutRender(ctx context.Context, state *State, routeID string) return deployRoute(ctx, state, routeID, true, false) } -func deploymentAuditAllowsMutation(route *Route, current AuditEvidence) error { +func deploymentAuditDecision(route *Route, current AuditEvidence) (string, error) { if current.Category == "in-sync" { - return nil + return "adopt", nil } if current.Category == "service-missing" && route.State != "deployed" { - return nil + return "deploy", nil } - return errors.New("remote Route state is not safe to overwrite") + return "", errors.New("remote Route state is not safe to overwrite") } func markRouteDeploying(state *State, routeID string) error { @@ -85,9 +85,13 @@ func deployRoute(ctx context.Context, state *State, routeID string, skipClientVa } current := AuditRoute(ctx, state, routeID) - if err := deploymentAuditAllowsMutation(route, current); err != nil { + decision, err := deploymentAuditDecision(route, current) + if err != nil { return nil, &operationStageError{Stage: "remote-preflight-audit", StateChanged: "remote-state-unchanged", Retry: "audit", Err: err} } + if decision == "adopt" { + return adoptVerifiedRoute(state, route, current, skipClientValidation, renderClients) + } if err := markRouteDeploying(state, routeID); err != nil { return nil, &operationStageError{Stage: "deployment-intent", StateChanged: "remote-state-unchanged", Retry: "deploy-route", Err: err} } From e8b0454f1de87dc387c2ea813ce928f70b1c6ee7 Mon Sep 17 00:00:00 2001 From: squarepots <46488165+squarepots@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:53:59 +0800 Subject: [PATCH 2/5] Test verified route adoption recovery Signed-off-by: squarepots <46488165+squarepots@users.noreply.github.com> --- internal/steward/deployment_recovery_test.go | 59 +++++++++++++++----- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/internal/steward/deployment_recovery_test.go b/internal/steward/deployment_recovery_test.go index 261bfaa..ee37aaa 100644 --- a/internal/steward/deployment_recovery_test.go +++ b/internal/steward/deployment_recovery_test.go @@ -6,26 +6,59 @@ import ( "testing" ) -func TestDeploymentAuditAllowsOnlyKnownRemoteStates(t *testing.T) { +func TestDeploymentAuditDecisionUsesVerifiedRemoteState(t *testing.T) { route := &Route{State: "pending"} - if err := deploymentAuditAllowsMutation(route, AuditEvidence{Category: "service-missing"}); err != nil { - t.Fatalf("fresh pending Route rejected service-missing state: %v", err) - } - if err := deploymentAuditAllowsMutation(route, AuditEvidence{Category: "in-sync"}); err != nil { - t.Fatalf("pending Route rejected verified in-sync state: %v", err) + + decision, err := deploymentAuditDecision(route, AuditEvidence{Category: "in-sync"}) + if err != nil || decision != "adopt" { + t.Fatalf("verified in-sync Route should be adopted without remote mutation: decision=%q err=%v", decision, err) } - if err := deploymentAuditAllowsMutation(route, AuditEvidence{Category: "undetermined"}); err == nil { - t.Fatal("pending Route accepted undetermined remote state") + + decision, err = deploymentAuditDecision(route, AuditEvidence{Category: "service-missing"}) + if err != nil || decision != "deploy" { + t.Fatalf("known missing service should allow deployment: decision=%q err=%v", decision, err) } - route.State = "deploying" - if err := deploymentAuditAllowsMutation(route, AuditEvidence{Category: "service-missing"}); err != nil { - t.Fatalf("retry checkpoint rejected known missing service: %v", err) + if decision, err = deploymentAuditDecision(route, AuditEvidence{Category: "undetermined"}); err == nil || decision != "" { + t.Fatalf("undetermined remote state should block mutation: decision=%q err=%v", decision, err) } route.State = "deployed" - if err := deploymentAuditAllowsMutation(route, AuditEvidence{Category: "service-missing"}); err == nil { - t.Fatal("deployed Route accepted a missing remote service") + if decision, err = deploymentAuditDecision(route, AuditEvidence{Category: "service-missing"}); err == nil || decision != "" { + t.Fatalf("deployed Route with missing service should block mutation: decision=%q err=%v", decision, err) + } +} + +func TestAdoptVerifiedRouteRestoresLocalState(t *testing.T) { + state, route := healthFixture(t, "direct", false) + current := findRoute(state.Inventory, route.ID) + current.Enabled = false + current.State = "deploying" + if err := state.Save(false); err != nil { + t.Fatal(err) + } + + evidence := AuditEvidence{ + Route: route.ID, + Status: "healthy", + Category: "in-sync", + ActualEgressIPv4: stringPointer(findServer(state.Inventory, route.ExitServer).Network.ExpectedEgressIPv4), + EgressMatchesDeclaredExit: true, + } + result, err := adoptVerifiedRoute(state, current, evidence, true, false) + if err != nil { + t.Fatal(err) + } + if result["state"] != "deployed" || result["enabled"] != true { + t.Fatalf("verified Route was not adopted: %#v", result) + } + reloaded, err := LoadState(state.PrivateDir) + if err != nil { + t.Fatal(err) + } + adopted := findRoute(reloaded.Inventory, route.ID) + if adopted == nil || adopted.State != "deployed" || !adopted.Enabled { + t.Fatalf("verified Route was not persisted locally: %#v", adopted) } } From 2f1a9fdf226bee1e68a46f32c37ac9159283d7b4 Mon Sep 17 00:00:00 2001 From: squarepots <46488165+squarepots@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:54:04 +0800 Subject: [PATCH 3/5] Bump patch version for deployment recovery Signed-off-by: squarepots <46488165+squarepots@users.noreply.github.com> --- version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.txt b/version.txt index b1b25a5..5859406 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.2.2 +2.2.3 From 92f6aa2ee1fcff3e65716d96a28b921ca54572dc Mon Sep 17 00:00:00 2001 From: squarepots <46488165+squarepots@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:55:58 +0800 Subject: [PATCH 4/5] Limit verified adoption to recovery states Signed-off-by: squarepots <46488165+squarepots@users.noreply.github.com> --- internal/steward/execution.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/steward/execution.go b/internal/steward/execution.go index 7c5f6ed..691d7f5 100644 --- a/internal/steward/execution.go +++ b/internal/steward/execution.go @@ -60,7 +60,10 @@ func deployRouteWithoutRender(ctx context.Context, state *State, routeID string) func deploymentAuditDecision(route *Route, current AuditEvidence) (string, error) { if current.Category == "in-sync" { - return "adopt", nil + if route.State == "deploying" || route.State == "deployed" { + return "adopt", nil + } + return "deploy", nil } if current.Category == "service-missing" && route.State != "deployed" { return "deploy", nil From 4f08affa135e0ebbc94e2c18cca754738aa052ac Mon Sep 17 00:00:00 2001 From: squarepots <46488165+squarepots@users.noreply.github.com> Date: Fri, 18 Sep 2026 02:56:10 +0800 Subject: [PATCH 5/5] Cover fresh deploy and retry adoption decisions Signed-off-by: squarepots <46488165+squarepots@users.noreply.github.com> --- internal/steward/deployment_recovery_test.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/internal/steward/deployment_recovery_test.go b/internal/steward/deployment_recovery_test.go index ee37aaa..a00dc60 100644 --- a/internal/steward/deployment_recovery_test.go +++ b/internal/steward/deployment_recovery_test.go @@ -10,10 +10,23 @@ func TestDeploymentAuditDecisionUsesVerifiedRemoteState(t *testing.T) { route := &Route{State: "pending"} decision, err := deploymentAuditDecision(route, AuditEvidence{Category: "in-sync"}) + if err != nil || decision != "deploy" { + t.Fatalf("fresh pending Route should still perform its initial deployment: decision=%q err=%v", decision, err) + } + + route.State = "deploying" + decision, err = deploymentAuditDecision(route, AuditEvidence{Category: "in-sync"}) + if err != nil || decision != "adopt" { + t.Fatalf("verified in-sync retry should be adopted without remote mutation: decision=%q err=%v", decision, err) + } + + route.State = "deployed" + decision, err = deploymentAuditDecision(route, AuditEvidence{Category: "in-sync"}) if err != nil || decision != "adopt" { - t.Fatalf("verified in-sync Route should be adopted without remote mutation: decision=%q err=%v", decision, err) + t.Fatalf("already deployed in-sync Route should be adopted without remote mutation: decision=%q err=%v", decision, err) } + route.State = "pending" decision, err = deploymentAuditDecision(route, AuditEvidence{Category: "service-missing"}) if err != nil || decision != "deploy" { t.Fatalf("known missing service should allow deployment: decision=%q err=%v", decision, err)