summaryrefslogtreecommitdiff
path: root/internal/manager/manager.go
diff options
context:
space:
mode:
authorJames O'Doherty <james@theodohertyfamily.com>2026-06-04 22:57:35 -0400
committerJames O'Doherty <james@theodohertyfamily.com>2026-06-04 22:57:35 -0400
commit04dca5dada8c2d971ff3b54eeedc5ab6e53a29ac (patch)
treea9890073a0eb21bc7db3aef2fcbe66cdc2fc9ceb /internal/manager/manager.go
parent66b782e261f1cd928ad6a8482788a65fb484db45 (diff)
refactor: decouple namespace operations and improve test coverage
- Introduce `namespace.Ops` interface to decouple `Manager` from system-level namespace operations, enabling easier unit testing via mocks. - Add unit tests for `internal/paths` to verify path resolution logic across different environment configurations. - Implement `EnsureBinary` helper in E2E tests to gracefully skip tests when `WG_WRAP_BIN` is not set, allowing `go test ./...` to pass in non-build environments. - Apply project-wide formatting and fix linting issues.
Diffstat (limited to 'internal/manager/manager.go')
-rw-r--r--internal/manager/manager.go68
1 files changed, 34 insertions, 34 deletions
diff --git a/internal/manager/manager.go b/internal/manager/manager.go
index 99a1a32..ffb02c0 100644
--- a/internal/manager/manager.go
+++ b/internal/manager/manager.go
@@ -9,11 +9,11 @@
// To support multiple concurrent commands on the same WireGuard tunnel without re-establishing
// connections, wg-wrap employs session-based persistent, unprivileged namespaces.
//
-// 1. Tracking: Process usage is tracked using active PID files inside the runtime base directory.
-// 2. Ref-Counting & Cleanup: Active PIDs are regularly pruned. When the last active process exits,
-// the namespace is unpinned and resources are reclaimed.
-// 3. Setns Join: When a new process is executed on an active profile, it discovers an active PID
-// and attaches itself to the existing User, Mount, and Network namespaces of the active tunnel.
+// 1. Tracking: Process usage is tracked using active PID files inside the runtime base directory.
+// 2. Ref-Counting & Cleanup: Active PIDs are regularly pruned. When the last active process exits,
+// the namespace is unpinned and resources are reclaimed.
+// 3. Setns Join: When a new process is executed on an active profile, it discovers an active PID
+// and attaches itself to the existing User, Mount, and Network namespaces of the active tunnel.
package manager
import (
@@ -30,22 +30,22 @@ import (
"git.theodohertyfamily.com/wg-wrap/pkg/wgconf"
)
-// Manager orchestrates the high-level lifecycle of WireGuard tunnels
-// and their associated network namespaces.
type Manager struct {
// PM is the path manager used to resolve configuration and runtime directories.
PM *paths.PathManager
+ // NS handles the network namespace operations.
+ NS namespace.Ops
}
-// New creates a new Manager with the given path manager.
-func New(pm *paths.PathManager) *Manager {
- return &Manager{PM: pm}
+// New creates a new Manager with the given path manager and namespace operations.
+func New(pm *paths.PathManager, ns namespace.Ops) *Manager {
+ return &Manager{PM: pm, NS: ns}
}
// Bootstrap ensures the process is running in an isolated user and network namespace.
// If an active session already exists for the profile, it joins it.
func (m *Manager) Bootstrap(cfg *config.Config) error {
- if namespace.IsIsolated() {
+ if m.NS.IsIsolated() {
return nil
}
@@ -53,28 +53,28 @@ func (m *Manager) Bootstrap(cfg *config.Config) error {
_ = os.Setenv("WG_WRAP_HOST_RUNTIME_BASE_DIR", m.PM.RuntimeBaseDir())
// Acquire startup lock to prevent concurrent bootstrap/joining races.
- lockFile, lockErr := namespace.AcquireProfileLock(m.PM, cfg.Profile)
+ lockFile, lockErr := m.NS.AcquireProfileLock(m.PM, cfg.Profile)
if lockErr == nil {
- defer namespace.ReleaseProfileLock(lockFile)
+ defer m.NS.ReleaseProfileLock(lockFile)
}
// Before bootstrapping, see if an active namespace/process for the profile exists.
- activePid, err := namespace.FindActiveProfilePid(m.PM, cfg.Profile)
+ activePid, err := m.NS.FindActiveProfilePid(m.PM, cfg.Profile)
if err == nil && activePid > 0 {
// Release the lock before executing the command to allow others to join.
- namespace.ReleaseProfileLock(lockFile)
+ m.NS.ReleaseProfileLock(lockFile)
// Register this PID before joining to prevent the race where the joining process
// hasn't registered itself yet, causing the existing process to think it's the last one.
- _ = namespace.RegisterProcess(m.PM, cfg.Profile)
+ _ = m.NS.RegisterProcess(m.PM, cfg.Profile)
- if err := namespace.BootstrapJoin(activePid); err != nil {
+ if err := m.NS.BootstrapJoin(activePid); err != nil {
return fmt.Errorf("failed to join existing namespace: %w", err)
}
return nil
}
- if err := namespace.Bootstrap(); err != nil {
+ if err := m.NS.Bootstrap(); err != nil {
return fmt.Errorf("bootstrap failed: %w", err)
}
@@ -84,25 +84,25 @@ func (m *Manager) Bootstrap(cfg *config.Config) error {
// Execute manages the full execution lifecycle inside an isolated namespace:
// lock acquisition, PID registration, tunnel initialization, command execution, and cleanup.
func (m *Manager) Execute(cfg *config.Config, verbose bool) error {
- if !namespace.IsIsolated() {
+ if !m.NS.IsIsolated() {
return fmt.Errorf("Execute called without namespace isolation")
}
// Acquire execution lock during configuration and startup inside the namespace.
- lockFile, lockErr := namespace.AcquireProfileLock(m.PM, cfg.Profile)
+ lockFile, lockErr := m.NS.AcquireProfileLock(m.PM, cfg.Profile)
var lockFileReleased bool
if lockErr == nil {
defer func() {
if !lockFileReleased {
- namespace.ReleaseProfileLock(lockFile)
+ m.NS.ReleaseProfileLock(lockFile)
}
}()
}
- if err := namespace.PruneStalePids(m.PM, cfg.Profile); err != nil {
+ if err := m.NS.PruneStalePids(m.PM, cfg.Profile); err != nil {
fmt.Fprintf(os.Stderr, "failed to prune stale pids: %v\n", err)
}
- if err := namespace.RegisterProcess(m.PM, cfg.Profile); err != nil {
+ if err := m.NS.RegisterProcess(m.PM, cfg.Profile); err != nil {
return fmt.Errorf("failed to register process: %w", err)
}
@@ -113,31 +113,31 @@ func (m *Manager) Execute(cfg *config.Config, verbose bool) error {
if lockErr == nil && !lockFileReleased {
cleanupLock = lockFile
} else {
- cleanupLock, cleanupErr = namespace.AcquireProfileLock(m.PM, cfg.Profile)
+ cleanupLock, cleanupErr = m.NS.AcquireProfileLock(m.PM, cfg.Profile)
}
if cleanupErr == nil {
- if err := namespace.UnregisterProcess(m.PM, cfg.Profile); err != nil {
+ if err := m.NS.UnregisterProcess(m.PM, cfg.Profile); err != nil {
fmt.Fprintf(os.Stderr, "failed to unregister process: %v\n", err)
}
- if err := namespace.PruneStalePids(m.PM, cfg.Profile); err != nil {
+ if err := m.NS.PruneStalePids(m.PM, cfg.Profile); err != nil {
fmt.Fprintf(os.Stderr, "failed to prune stale pids during cleanup: %v\n", err)
}
- last, lastErr := namespace.IsLastProcess(m.PM, cfg.Profile)
+ last, lastErr := m.NS.IsLastProcess(m.PM, cfg.Profile)
if lastErr == nil && last {
- if err := namespace.UnpinNamespace(m.PM, cfg.Profile); err != nil {
+ if err := m.NS.UnpinNamespace(m.PM, cfg.Profile); err != nil {
fmt.Fprintf(os.Stderr, "failed to unpin namespace: %v\n", err)
}
}
if lockErr == nil && !lockFileReleased {
lockFileReleased = true
}
- namespace.ReleaseProfileLock(cleanupLock)
+ m.NS.ReleaseProfileLock(cleanupLock)
} else {
- if err := namespace.UnregisterProcess(m.PM, cfg.Profile); err != nil {
+ if err := m.NS.UnregisterProcess(m.PM, cfg.Profile); err != nil {
fmt.Fprintf(os.Stderr, "failed to unregister process: %v\n", err)
}
}
@@ -191,7 +191,7 @@ func (m *Manager) Execute(cfg *config.Config, verbose bool) error {
}
defer tunnel.Close()
- if err := namespace.PinNamespace(m.PM, cfg.Profile); err != nil {
+ if err := m.NS.PinNamespace(m.PM, cfg.Profile); err != nil {
fmt.Fprintf(os.Stderr, "warning: failed to pin namespace: %v\n", err)
}
} else {
@@ -203,7 +203,7 @@ func (m *Manager) Execute(cfg *config.Config, verbose bool) error {
}
lockFileReleased = true
- namespace.ReleaseProfileLock(lockFile)
+ m.NS.ReleaseProfileLock(lockFile)
cmd := exec.Command(cfg.Command[0], cfg.Command[1:]...)
cmd.Stdin = os.Stdin
@@ -220,7 +220,7 @@ func (m *Manager) Execute(cfg *config.Config, verbose bool) error {
// StopProfile stops a profile session by unpinning its namespace.
func (m *Manager) StopProfile(profile string) error {
- if err := namespace.UnpinNamespace(m.PM, profile); err != nil {
+ if err := m.NS.UnpinNamespace(m.PM, profile); err != nil {
return fmt.Errorf("failed to stop profile: %w", err)
}
return nil
@@ -228,7 +228,7 @@ func (m *Manager) StopProfile(profile string) error {
// VerifyLifecycle checks for an active session for the given profile.
func (m *Manager) VerifyLifecycle(profile string) error {
- activePid, err := namespace.FindActiveProfilePid(m.PM, profile)
+ activePid, err := m.NS.FindActiveProfilePid(m.PM, profile)
if err != nil || activePid <= 0 {
return fmt.Errorf("no active session found for profile %s", profile)
}