Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions internal/steward/deployment_recovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import (
"testing"
)

func TestDeploymentAuditAllowsOnlyKnownRemoteStates(t *testing.T) {
func TestDeploymentAuditAllowsMutationOnlyFromKnownSafeStates(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)
t.Fatalf("fresh pending Route rejected verified in-sync state: %v", err)
}
if err := deploymentAuditAllowsMutation(route, AuditEvidence{Category: "undetermined"}); err == nil {
t.Fatal("pending Route accepted undetermined remote state")
Expand All @@ -22,11 +22,56 @@ func TestDeploymentAuditAllowsOnlyKnownRemoteStates(t *testing.T) {
if err := deploymentAuditAllowsMutation(route, AuditEvidence{Category: "service-missing"}); err != nil {
t.Fatalf("retry checkpoint rejected known missing service: %v", err)
}
if err := deploymentAuditAllowsMutation(route, AuditEvidence{Category: "in-sync"}); err == nil {
t.Fatal("deploying Route accepted another remote mutation after audit proved the existing deployment is in sync")
}

route.State = "deployed"
if err := deploymentAuditAllowsMutation(route, AuditEvidence{Category: "service-missing"}); err == nil {
t.Fatal("deployed Route accepted a missing remote service")
}
if err := deploymentAuditAllowsMutation(route, AuditEvidence{Category: "in-sync"}); err == nil {
t.Fatal("deployed Route accepted another remote mutation after audit proved the existing deployment is in sync")
}
}

func TestAdoptVerifiedRouteRecoversLocalStateWithoutDeployment(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)
}

exit := findServer(state.Inventory, route.ExitServer)
if exit == nil {
t.Fatal("fixture is missing Route exit Server")
}
actual := exit.Network.ExpectedEgressIPv4
evidence := AuditEvidence{
Route: route.ID,
Status: "healthy",
Category: "in-sync",
ActualEgressIPv4: &actual,
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 deployment was not adopted: %#v", result)
}
reloaded, err := LoadState(state.PrivateDir)
if err != nil {
t.Fatal(err)
}
recovered := findRoute(reloaded.Inventory, route.ID)
if recovered == nil || recovered.State != "deployed" || !recovered.Enabled {
t.Fatalf("verified remote state was not committed locally: %#v", recovered)
}
}

func TestMarkRouteDeployingPersistsRetryCheckpoint(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion internal/steward/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func deployRouteWithoutRender(ctx context.Context, state *State, routeID string)
}

func deploymentAuditAllowsMutation(route *Route, current AuditEvidence) error {
if current.Category == "in-sync" {
if current.Category == "in-sync" && route.State == "pending" {
return nil
}
if current.Category == "service-missing" && route.State != "deployed" {
Expand All @@ -85,6 +85,9 @@ func deployRoute(ctx context.Context, state *State, routeID string, skipClientVa
}

current := AuditRoute(ctx, state, routeID)
if current.Category == "in-sync" && (route.State == "deploying" || route.State == "deployed") {
return adoptVerifiedRoute(state, route, current, skipClientValidation, renderClients)
}
if err := deploymentAuditAllowsMutation(route, current); err != nil {
return nil, &operationStageError{Stage: "remote-preflight-audit", StateChanged: "remote-state-unchanged", Retry: "audit", Err: err}
}
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.2.2
2.2.3