summaryrefslogtreecommitdiff
path: root/internal/namespace/preflight_test.go
diff options
context:
space:
mode:
authorJames O'Doherty <james@theodohertyfamily.com>2026-06-13 13:50:25 -0400
committerJames O'Doherty <james@theodohertyfamily.com>2026-06-13 13:50:25 -0400
commit5646eca119f80f8f45ebec9fcbe666ca614ebf5d (patch)
treea785cb7f30b5a6444e208ae6717a73a758644998 /internal/namespace/preflight_test.go
parent29621ecbd1e77e6e1a70b6b3ea8fbe3a56e47df3 (diff)
feat: implement system preflight checks and health diagnostics
Introduced a tiered system verification mechanism to improve reliability and provide actionable feedback to users, avoiding false positives in the critical execution path. Key changes: - Implement `CheckSystemRequirements` for critical, non-ambiguous requirements (e.g., TUN device availability) to ensure fatal environment issues are caught immediately during bootstrap. - Implement a user-facing `healthcheck` command that provides comprehensive diagnostics and actionable configuration hints for common misconfigurations (e.g., unprivileged user namespaces, subuid/subgid mappings, and kernel sysctls). - Refactor the `FileSystem` interface to support full mockability, allowing for exhaustive unit testing of diagnostic logic. - Add comprehensive unit tests in `internal/namespace/preflight_test.go` covering various Linux distributions, privilege levels, and hardware availability scenarios. - Ensure code quality through formatting, static analysis (golangci-lint), and validation of all existing unit, integration, and E2E tests.
Diffstat (limited to 'internal/namespace/preflight_test.go')
-rw-r--r--internal/namespace/preflight_test.go199
1 files changed, 199 insertions, 0 deletions
diff --git a/internal/namespace/preflight_test.go b/internal/namespace/preflight_test.go
new file mode 100644
index 0000000..49790f2
--- /dev/null
+++ b/internal/namespace/preflight_test.go
@@ -0,0 +1,199 @@
+package namespace
+
+import (
+ "os"
+ "testing"
+ "time"
+)
+
+type mockFileInfo struct {
+ name string
+}
+
+func (m *mockFileInfo) Name() string { return m.name }
+func (m *mockFileInfo) Size() int64 { return 0 }
+func (m *mockFileInfo) Mode() os.FileMode { return 0 }
+func (m *mockFileInfo) ModTime() time.Time { return time.Now() }
+func (m *mockFileInfo) IsDir() bool { return false }
+func (m *mockFileInfo) Sys() any { return nil }
+
+type mockFS struct {
+ files map[string][]byte
+ canOpen map[string]bool
+}
+
+func (m *mockFS) Stat(name string) (os.FileInfo, error) {
+ if _, ok := m.files[name]; ok {
+ return &mockFileInfo{name: name}, nil
+ }
+ return nil, os.ErrNotExist
+}
+
+func (m *mockFS) MkdirAll(path string, perm os.FileMode) error { return nil }
+func (m *mockFS) CreateTemp(dir, pattern string) (*os.File, error) { return nil, nil }
+func (m *mockFS) MkdirTemp(dir, pattern string) (string, error) { return "", nil }
+func (m *mockFS) Remove(name string) error { return nil }
+
+func (m *mockFS) ReadFile(name string) ([]byte, error) {
+ if data, ok := m.files[name]; ok {
+ return data, nil
+ }
+ return nil, os.ErrNotExist
+}
+
+func (m *mockFS) Open(name string) (*os.File, error) {
+ if m.canOpen[name] {
+ f, _ := os.CreateTemp("", "mock-file")
+ return f, nil
+ }
+ return nil, os.ErrPermission
+}
+
+func TestCheckSystemRequirements(t *testing.T) {
+ tests := []struct {
+ name string
+ files map[string][]byte
+ canOpen map[string]bool
+ wantErr bool
+ }{
+ {
+ name: "all requirements met",
+ files: map[string][]byte{
+ "/dev/net/tun": {},
+ },
+ canOpen: map[string]bool{
+ "/dev/net/tun": true,
+ },
+ wantErr: false,
+ },
+ {
+ name: "tun device missing",
+ files: map[string][]byte{},
+ canOpen: map[string]bool{},
+ wantErr: true,
+ },
+ {
+ name: "tun device not accessible",
+ files: map[string][]byte{
+ "/dev/net/tun": {},
+ },
+ canOpen: map[string]bool{
+ "/dev/net/tun": false,
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ fs := &mockFS{files: tt.files, canOpen: tt.canOpen}
+ p := NewPreflight(fs)
+ err := p.CheckSystemRequirements()
+ if (err != nil) != tt.wantErr {
+ t.Errorf("CheckSystemRequirements() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
+
+func TestRunHealthCheck(t *testing.T) {
+ tests := []struct {
+ name string
+ files map[string][]byte
+ canOpen map[string]bool
+ uid int
+ username string
+ wantPass map[string]bool
+ }{
+ {
+ name: "healthy non-root user on Debian",
+ files: map[string][]byte{
+ "/dev/net/tun": {},
+ "/proc/sys/kernel/unprivileged_userns_clone": []byte("1"),
+ "/proc/sys/user/max_user_namespaces": []byte("1024"),
+ "/etc/subuid": []byte("james:100000:60000"),
+ },
+ canOpen: map[string]bool{
+ "/dev/net/tun": true,
+ },
+ uid: 1000,
+ username: "james",
+ wantPass: map[string]bool{
+ "TUN Device Accessibility": true,
+ "Unprivileged User Namespaces (Debian/Ubuntu)": true,
+ "User Namespace Limit": true,
+ "SubUID/SubGID Mappings": true,
+ },
+ },
+ {
+ name: "healthy root user",
+ files: map[string][]byte{
+ "/dev/net/tun": {},
+ },
+ canOpen: map[string]bool{
+ "/dev/net/tun": true,
+ },
+ uid: 0,
+ username: "root",
+ wantPass: map[string]bool{
+ "TUN Device Accessibility": true,
+ "SubUID/SubGID Mappings": true, // Should be bypassed
+ },
+ },
+ {
+ name: "broken config",
+ files: map[string][]byte{
+ "/dev/net/tun": {},
+ "/proc/sys/kernel/unprivileged_userns_clone": []byte("0"),
+ "/proc/sys/user/max_user_namespaces": []byte("0"),
+ "/etc/subuid": []byte("someone-else:100000:60000"),
+ },
+ canOpen: map[string]bool{
+ "/dev/net/tun": true,
+ },
+ uid: 1000,
+ username: "james",
+ wantPass: map[string]bool{
+ "TUN Device Accessibility": true,
+ "Unprivileged User Namespaces (Debian/Ubuntu)": false,
+ "User Namespace Limit": false,
+ "SubUID/SubGID Mappings": false,
+ },
+ },
+ {
+ name: "non-debian healthy",
+ files: map[string][]byte{
+ "/dev/net/tun": {},
+ "/proc/sys/user/max_user_namespaces": []byte("1024"),
+ "/etc/subuid": []byte("james:100000:60000"),
+ },
+ canOpen: map[string]bool{
+ "/dev/net/tun": true,
+ },
+ uid: 1000,
+ username: "james",
+ wantPass: map[string]bool{
+ "TUN Device Accessibility": true,
+ "Unprivileged User Namespaces (Debian/Ubuntu)": true, // Not applicable is considered passed
+ "User Namespace Limit": true,
+ "SubUID/SubGID Mappings": true,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ fs := &mockFS{files: tt.files, canOpen: tt.canOpen}
+ p := NewPreflight(fs)
+ results := p.RunHealthCheck(tt.uid, tt.username)
+
+ for _, res := range results {
+ if pass, ok := tt.wantPass[res.Name]; ok {
+ if res.Passed != pass {
+ t.Errorf("Requirement %s: passed = %v, want %v (Msg: %s)", res.Name, res.Passed, pass, res.Message)
+ }
+ }
+ }
+ })
+ }
+}