Skip to content
Open
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
30 changes: 29 additions & 1 deletion cmd/vcluster/cmd/snapshot/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"os"

"github.com/loft-sh/vcluster/pkg/config"
"github.com/loft-sh/vcluster/pkg/constants"
"github.com/loft-sh/vcluster/pkg/snapshot"
standaloneutil "github.com/loft-sh/vcluster/pkg/util/standalone"
"github.com/spf13/cobra"
)

Expand All @@ -17,8 +19,34 @@ func NewRestoreCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "restore",
Short: "Restore a vCluster",
Args: cobra.NoArgs,
Long: `Restores the vCluster backing store from a snapshot. This is an internal
command: it must only run while the vCluster control plane is down, and it is
normally invoked by the vcluster CLI or by the snapshot pod.

On a standalone host, use "vcluster restore --standalone <snapshot-url>" (the
vcluster CLI) instead of calling this command directly: the CLI stops
vcluster.service, runs this command, and restarts the service afterwards.
Direct invocations while vcluster.service is active are refused to protect
the backing store.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
// A restore rewrites the backing store and must never run while the
// standalone control plane is up: it would race the live kine/etcd,
// and the unit's Restart=always re-spawns a crashed server
// mid-restore. The vcluster CLI ("vcluster restore --standalone")
// stops and restarts the service around this command; refuse direct
// invocations that skipped that envelope. On hosts without systemd
// (e.g. the in-pod restore path) this reports not-active and the
// restore proceeds.
if standaloneutil.IsServiceActive() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocking — [coverage gap] This guard (lines 41–48) is the fix's core in-code protection, but its wiring is untested in this repo. TestIsServiceActive exercises only the unexported isServiceActive(run) helper; no test reaches this RunE handler. So a negated condition (if !standaloneutil.IsServiceActive()), a bad import alias, or a refactor that drops the branch would ship on a green build and silently re-open the corruption this PR closes (a restore racing the live kine/etcd while the unit's Restart=always respawns mid-restore).

The red-first e2e in vcluster-pro and the helper unit test both help, but neither gates this repo's CI — a regression here is only caught after a downstream dependency bump.

Consider making the probe injectable and adding a small unit test on the wiring:

// package-level seam
var isServiceActive = standaloneutil.IsServiceActive

then a table/fake test that stubs it true and asserts RunE returns the refusal error (naming the service + remediation), plus a false case that proceeds past the guard. This matches the repo's standing expectation that new snapshot/restore branch logic carries a table/fake-client unit test.

return fmt.Errorf(
"refusing to restore: %s.service is active on this host; use the vcluster CLI (%q), which stops and restarts the service around the restore, or stop it first with %q",
constants.VClusterStandaloneSystemdServiceName,
"vcluster restore --standalone <snapshot-url>",
"systemctl stop "+constants.VClusterStandaloneSystemdServiceName,
)
}

vConfig, err := config.LoadConfig(os.Getenv("VCLUSTER_NAME"))
if err != nil {
return err
Expand Down
2 changes: 2 additions & 0 deletions cmd/vclusterctl/cmd/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ vcluster restore my-vcluster container:///data/my-local-snapshot.tar.gz
vcluster restore my-vcluster ./my-snapshot.tar.gz --driver docker
# Restore with a different name
vcluster restore my-new-name ./my-snapshot.tar.gz --driver docker
# Restore the standalone vCluster on this host (stops and restarts vcluster.service)
vcluster restore --standalone s3://my-bucket/my-bucket-key
#######################################################
`,
Args: func(cobraCmd *cobra.Command, args []string) error {
Expand Down
19 changes: 19 additions & 0 deletions pkg/util/standalone/service_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,25 @@ func NewServiceManager() (ServiceManager, error) {
return newServiceManager(defaultSystemctlRunner)
}

// IsServiceActive reports whether the standalone vCluster systemd unit is
// currently active on this host. It reports false wherever systemd cannot
// answer affirmatively (non-Linux, no systemctl binary, systemd not running,
// unit stopped or not installed), so callers can use it as a guard that only
// engages on a standalone host with a running control plane.
func IsServiceActive() bool {
if runtime.GOOS != "linux" {
return false
}
return isServiceActive(defaultSystemctlRunner)
}

func isServiceActive(run systemctlRunner) bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider (adjacent, out of diff) — This new isServiceActive is a clean, reusable probe, but a near-duplicate inline check already exists at pkg/cli/find/find.go:837:

out, err := exec.Command("systemctl", "is-active", constants.VClusterStandaloneSystemdServiceName).Output()
if err == nil && strings.TrimSpace(string(out)) == "active" { status = StatusRunning }

find.go already imports standaloneutil, so that call site could adopt standaloneutil.IsServiceActive() and leave a single implementation of the is-active probe. Follow-up only — it's outside this PR's diff, so no change required here.

// "systemctl is-active" exits zero only when the unit is active; a missing
// systemctl binary, an unreachable systemd, a stopped unit, and an unknown
// unit all exit non-zero and therefore report not-active.
return run("is-active", "--quiet", constants.VClusterStandaloneSystemdServiceName) == nil
}

func newServiceManager(run systemctlRunner) (ServiceManager, error) {
if runtime.GOOS != "linux" {
return nil, fmt.Errorf("systemd manager is only supported on Linux (current OS: %s)", runtime.GOOS)
Expand Down
25 changes: 25 additions & 0 deletions pkg/util/standalone/service_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package standalone
import (
"errors"
"runtime"
"slices"
"testing"

"github.com/loft-sh/vcluster/pkg/constants"
)

func fakeRunner(catErr, stopErr, startErr error) systemctlRunner {
Expand Down Expand Up @@ -74,6 +77,28 @@ func TestServiceManager_StopStart(t *testing.T) {
}
}

// TestIsServiceActive verifies the is-active probe: an affirmative systemctl
// exit reports active, any failure (stopped unit, unknown unit, no systemd)
// reports not-active. isServiceActive has no GOOS gate, so this runs anywhere.
func TestIsServiceActive(t *testing.T) {
var gotArgs []string
active := isServiceActive(func(args ...string) error {
gotArgs = args
return nil
})
if !active {
t.Fatal("expected active when systemctl is-active succeeds")
}
wantArgs := []string{"is-active", "--quiet", constants.VClusterStandaloneSystemdServiceName}
if !slices.Equal(gotArgs, wantArgs) {
t.Fatalf("systemctl args = %v, want %v", gotArgs, wantArgs)
}

if isServiceActive(func(...string) error { return errors.New("inactive") }) {
t.Fatal("expected not-active when systemctl is-active fails")
}
}

// TestNewServiceManager_ServiceDown verifies that NewServiceManager succeeds even
// when the service exists but is not active (the restore scenario).
func TestNewServiceManager_ServiceDown(t *testing.T) {
Expand Down
Loading