diff options
| author | James O'Doherty <james@theodohertyfamily.com> | 2026-06-13 13:50:25 -0400 |
|---|---|---|
| committer | James O'Doherty <james@theodohertyfamily.com> | 2026-06-13 13:50:25 -0400 |
| commit | 5646eca119f80f8f45ebec9fcbe666ca614ebf5d (patch) | |
| tree | a785cb7f30b5a6444e208ae6717a73a758644998 /internal/namespace/preflight_test.go | |
| parent | 29621ecbd1e77e6e1a70b6b3ea8fbe3a56e47df3 (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.go | 199 |
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) + } + } + } + }) + } +} |
