Skip to content

Commit b12c9a4

Browse files
JAORMXclaude
andcommitted
Add user namespace preflight check to VM creation
Surface a clear error at startup when unprivileged user namespaces are disabled (kernel.unprivileged_userns_clone=0) instead of failing with a cryptic EPERM during VM creation. Uses propolis's UserNamespaceCheck via WithPreflightChecks, which appends to the default checker. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e2e482b commit b12c9a4

4 files changed

Lines changed: 71 additions & 0 deletions

File tree

pkg/infra/vm/preflight_linux.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//go:build linux
5+
6+
package vm
7+
8+
import "github.com/stacklok/propolis/preflight"
9+
10+
// extraPreflightChecks returns additional preflight checks for Linux hosts.
11+
// Currently this adds a user namespace availability check, which catches
12+
// kernel.unprivileged_userns_clone=0 before VM creation fails with EPERM.
13+
func extraPreflightChecks() []preflight.Check {
14+
return []preflight.Check{preflight.UserNamespaceCheck()}
15+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//go:build linux
5+
6+
package vm
7+
8+
import (
9+
"testing"
10+
)
11+
12+
func TestExtraPreflightChecks(t *testing.T) {
13+
t.Parallel()
14+
15+
checks := extraPreflightChecks()
16+
17+
t.Run("returns exactly one check", func(t *testing.T) {
18+
t.Parallel()
19+
if got := len(checks); got != 1 {
20+
t.Fatalf("expected 1 check, got %d", got)
21+
}
22+
})
23+
24+
t.Run("check is named userns", func(t *testing.T) {
25+
t.Parallel()
26+
if got := checks[0].Name; got != "userns" {
27+
t.Fatalf("expected check name %q, got %q", "userns", got)
28+
}
29+
})
30+
31+
t.Run("check is required", func(t *testing.T) {
32+
t.Parallel()
33+
if !checks[0].Required {
34+
t.Fatal("expected userns check to be required")
35+
}
36+
})
37+
}

pkg/infra/vm/preflight_other.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//go:build !linux
5+
6+
package vm
7+
8+
import "github.com/stacklok/propolis/preflight"
9+
10+
// extraPreflightChecks returns nil on non-Linux platforms where user
11+
// namespace checks are not applicable.
12+
func extraPreflightChecks() []preflight.Check {
13+
return nil
14+
}

pkg/infra/vm/propolis.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ func (p *PropolisProvider) CreateVM(ctx context.Context, env *environment.Enviro
149149
propolis.WithLogLevel(p.logLevel))
150150
}
151151

152+
if checks := extraPreflightChecks(); len(checks) > 0 {
153+
propolisOpts = append(propolisOpts,
154+
propolis.WithPreflightChecks(checks...))
155+
}
156+
152157
propolisOpts = append(propolisOpts, propolis.WithBackend(
153158
libkrun.NewBackend(p.buildBackendOpts(opts)...),
154159
))

0 commit comments

Comments
 (0)