Repository navigation
fix(backup): back up stopped docker sandboxes by starting them for the backup #6723
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
f244cec
fix(backup): back up stopped docker sandboxes by starting them for th…
Hokonoken 9d9b84b
fix(backup): harden stopped sandbox lifecycle
prekshivyas 70e558b
test(e2e): keep stopped backup proof linear
prekshivyas 6010e93
test(e2e): prove stopped backup restore
prekshivyas 9a82e0e
docs(backup): clarify stopped sandbox coverage
prekshivyas d642832
merge: sync main into PR #6723
cv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { describe, expect, it, vi } from "vitest"; | ||
|
|
||
| vi.mock("../../state/registry", () => ({ | ||
| getSandbox: vi.fn(), | ||
| listSandboxes: vi.fn(), | ||
| })); | ||
| vi.mock("../../state/sandbox", () => ({ | ||
| backupSandboxState: vi.fn(), | ||
| })); | ||
|
|
||
| import { | ||
| backupStartedSandboxState, | ||
| returnSandboxContainerToStopped, | ||
| startStoppedSandboxContainerForBackup, | ||
| } from "./stopped-sandbox-backup"; | ||
|
|
||
| describe("startStoppedSandboxContainerForBackup", () => { | ||
| const deps = (over: Record<string, unknown> = {}) => ({ | ||
| getSandboxDriver: vi.fn().mockReturnValue("docker"), | ||
| listSandboxNames: vi.fn().mockReturnValue(["my-sb"]), | ||
| dockerPsAllNames: vi.fn().mockReturnValue("openshell-my-sb-abc123\n"), | ||
| dockerInspectStatus: vi.fn().mockReturnValue("exited"), | ||
| dockerStart: vi.fn().mockReturnValue("openshell-my-sb-abc123"), | ||
| ...over, | ||
| }); | ||
|
|
||
| it("starts an exited docker-driver container and reports its name", () => { | ||
| const d = deps(); | ||
| expect(startStoppedSandboxContainerForBackup("my-sb", d)).toEqual({ | ||
| containerName: "openshell-my-sb-abc123", | ||
| }); | ||
| expect(d.dockerStart).toHaveBeenCalledWith("openshell-my-sb-abc123"); | ||
| }); | ||
|
|
||
| it("starts a created container (onboarded but never run)", () => { | ||
| const d = deps({ dockerInspectStatus: vi.fn().mockReturnValue("created") }); | ||
| expect(startStoppedSandboxContainerForBackup("my-sb", d)).not.toBeNull(); | ||
| }); | ||
|
|
||
| it("leaves non-docker-driver sandboxes alone", () => { | ||
| const d = deps({ getSandboxDriver: vi.fn().mockReturnValue("kubernetes") }); | ||
| expect(startStoppedSandboxContainerForBackup("my-sb", d)).toBeNull(); | ||
| expect(d.dockerPsAllNames).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("returns null when no container owns the sandbox name", () => { | ||
| const d = deps({ dockerPsAllNames: vi.fn().mockReturnValue("openshell-other-x\n") }); | ||
| expect(startStoppedSandboxContainerForBackup("my-sb", d)).toBeNull(); | ||
| expect(d.dockerStart).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("leaves a running-but-not-Ready container alone (crash loop, gateway drift)", () => { | ||
| const d = deps({ dockerInspectStatus: vi.fn().mockReturnValue("running") }); | ||
| expect(startStoppedSandboxContainerForBackup("my-sb", d)).toBeNull(); | ||
| expect(d.dockerStart).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("leaves a paused container alone (#4495)", () => { | ||
| const d = deps({ dockerInspectStatus: vi.fn().mockReturnValue("paused") }); | ||
| expect(startStoppedSandboxContainerForBackup("my-sb", d)).toBeNull(); | ||
| expect(d.dockerStart).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("returns null when docker start fails", () => { | ||
| const d = deps({ dockerStart: vi.fn().mockReturnValue("") }); | ||
| expect(startStoppedSandboxContainerForBackup("my-sb", d)).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("returnSandboxContainerToStopped", () => { | ||
| it("reports success when docker stop echoes the container name", () => { | ||
| const dockerStop = vi.fn().mockReturnValue("openshell-my-sb-abc123"); | ||
| expect(returnSandboxContainerToStopped("openshell-my-sb-abc123", { dockerStop })).toBe(true); | ||
| expect(dockerStop).toHaveBeenCalledWith("openshell-my-sb-abc123"); | ||
| }); | ||
|
|
||
| it("reports failure when docker stop produces no output", () => { | ||
| const dockerStop = vi.fn().mockReturnValue(""); | ||
| expect(returnSandboxContainerToStopped("openshell-my-sb-abc123", { dockerStop })).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("backupStartedSandboxState", () => { | ||
| const ok = { | ||
| success: true, | ||
| backedUpDirs: [], | ||
| failedDirs: [], | ||
| backedUpFiles: [], | ||
| failedFiles: [], | ||
| }; | ||
| const unreachable = { ...ok, success: false, unreachable: true }; | ||
| const denied = { ...ok, success: false }; | ||
|
|
||
| it("retries while the just-started container's SSH endpoint is unreachable (#6500)", async () => { | ||
| const backup = vi | ||
| .fn() | ||
| .mockReturnValueOnce(unreachable) | ||
| .mockReturnValueOnce(unreachable) | ||
| .mockReturnValueOnce(ok); | ||
| const sleep = vi.fn().mockResolvedValue(undefined); | ||
| const result = await backupStartedSandboxState("my-sb", { | ||
| backup, | ||
| sleep, | ||
| attempts: 5, | ||
| delayMs: 1, | ||
| }); | ||
| expect(result.success).toBe(true); | ||
| expect(backup).toHaveBeenCalledTimes(3); | ||
| expect(sleep).toHaveBeenCalledTimes(2); | ||
| }); | ||
|
|
||
| it("returns a non-transport failure without retrying", async () => { | ||
| const backup = vi.fn().mockReturnValue(denied); | ||
| const sleep = vi.fn().mockResolvedValue(undefined); | ||
| const result = await backupStartedSandboxState("my-sb", { | ||
| backup, | ||
| sleep, | ||
| attempts: 5, | ||
| delayMs: 1, | ||
| }); | ||
| expect(result.success).toBe(false); | ||
| expect(backup).toHaveBeenCalledTimes(1); | ||
| expect(sleep).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("gives up after the attempt budget while still unreachable", async () => { | ||
| const backup = vi.fn().mockReturnValue(unreachable); | ||
| const sleep = vi.fn().mockResolvedValue(undefined); | ||
| const result = await backupStartedSandboxState("my-sb", { | ||
| backup, | ||
| sleep, | ||
| attempts: 3, | ||
| delayMs: 1, | ||
| }); | ||
| expect(result.unreachable).toBe(true); | ||
| expect(backup).toHaveBeenCalledTimes(3); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.