From 1adebbdba1d149b9638b04d894d38f02f82712b7 Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 2 Feb 2026 05:17:12 +0000 Subject: [PATCH] chore(io): migrate pkg/cli and pkg/container to io.Local abstraction Continue io.Medium migration for the remaining packages: - pkg/cli/daemon.go: PIDFile Acquire/Release now use io.Local.Read, Delete, and Write for managing daemon PID files. - pkg/container/state.go: LoadState and SaveState use io.Local for JSON state persistence. EnsureLogsDir uses io.Local.EnsureDir. - pkg/container/templates.go: Template loading and directory scanning now use io.Local.IsFile, IsDir, Read, and List. - pkg/container/linuxkit.go: Image validation uses io.Local.IsFile, log file check uses io.Local.IsFile. Streaming log file creation (os.Create) remains unchanged as io.Local doesn't support streaming. Closes #105, closes #107 Co-Authored-By: Claude Opus 4.5 --- pkg/cli/daemon.go | 29 ++++++++++++++++------------- pkg/container/linuxkit.go | 33 ++++++++++++++++----------------- pkg/container/state.go | 21 ++++++++++++++------- pkg/container/templates.go | 18 ++++++++++-------- 4 files changed, 56 insertions(+), 45 deletions(-) diff --git a/pkg/cli/daemon.go b/pkg/cli/daemon.go index 8599eb56..bcee03c1 100644 --- a/pkg/cli/daemon.go +++ b/pkg/cli/daemon.go @@ -13,6 +13,7 @@ import ( "syscall" "time" + "github.com/host-uk/core/pkg/io" "golang.org/x/term" ) @@ -88,9 +89,14 @@ func (p *PIDFile) Acquire() error { p.mu.Lock() defer p.mu.Unlock() + absPath, err := filepath.Abs(p.path) + if err != nil { + return fmt.Errorf("failed to resolve PID file path: %w", err) + } + // Check if PID file exists - if data, err := os.ReadFile(p.path); err == nil { - pid, err := strconv.Atoi(string(data)) + if content, err := io.Local.Read(absPath); err == nil { + pid, err := strconv.Atoi(content) if err == nil && pid > 0 { // Check if process is still running if process, err := os.FindProcess(pid); err == nil { @@ -100,19 +106,12 @@ func (p *PIDFile) Acquire() error { } } // Stale PID file, remove it - _ = os.Remove(p.path) + _ = io.Local.Delete(absPath) } - // Ensure directory exists - if dir := filepath.Dir(p.path); dir != "." { - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("failed to create PID directory: %w", err) - } - } - - // Write current PID + // Write current PID (io.Local.Write creates parent directories automatically) pid := os.Getpid() - if err := os.WriteFile(p.path, []byte(strconv.Itoa(pid)), 0644); err != nil { + if err := io.Local.Write(absPath, strconv.Itoa(pid)); err != nil { return fmt.Errorf("failed to write PID file: %w", err) } @@ -123,7 +122,11 @@ func (p *PIDFile) Acquire() error { func (p *PIDFile) Release() error { p.mu.Lock() defer p.mu.Unlock() - return os.Remove(p.path) + absPath, err := filepath.Abs(p.path) + if err != nil { + return err + } + return io.Local.Delete(absPath) } // Path returns the PID file path. diff --git a/pkg/container/linuxkit.go b/pkg/container/linuxkit.go index 25c1ca18..e85f9c1a 100644 --- a/pkg/container/linuxkit.go +++ b/pkg/container/linuxkit.go @@ -4,11 +4,13 @@ import ( "bufio" "context" "fmt" - "io" + goio "io" "os" "os/exec" "syscall" "time" + + "github.com/host-uk/core/pkg/io" ) // LinuxKitManager implements the Manager interface for LinuxKit VMs. @@ -51,7 +53,7 @@ func NewLinuxKitManagerWithHypervisor(state *State, hypervisor Hypervisor) *Linu // Run starts a new LinuxKit VM from the given image. func (m *LinuxKitManager) Run(ctx context.Context, image string, opts RunOptions) (*Container, error) { // Validate image exists - if _, err := os.Stat(image); err != nil { + if !io.Local.IsFile(image) { return nil, fmt.Errorf("image not found: %s", image) } @@ -190,12 +192,12 @@ func (m *LinuxKitManager) Run(ctx context.Context, image string, opts RunOptions // Copy output to both log and stdout go func() { - mw := io.MultiWriter(logFile, os.Stdout) - _, _ = io.Copy(mw, stdout) + mw := goio.MultiWriter(logFile, os.Stdout) + _, _ = goio.Copy(mw, stdout) }() go func() { - mw := io.MultiWriter(logFile, os.Stderr) - _, _ = io.Copy(mw, stderr) + mw := goio.MultiWriter(logFile, os.Stderr) + _, _ = goio.Copy(mw, stderr) }() // Wait for the process to complete @@ -310,7 +312,7 @@ func isProcessRunning(pid int) bool { } // Logs returns a reader for the container's log output. -func (m *LinuxKitManager) Logs(ctx context.Context, id string, follow bool) (io.ReadCloser, error) { +func (m *LinuxKitManager) Logs(ctx context.Context, id string, follow bool) (goio.ReadCloser, error) { _, ok := m.state.Get(id) if !ok { return nil, fmt.Errorf("container not found: %s", id) @@ -321,11 +323,8 @@ func (m *LinuxKitManager) Logs(ctx context.Context, id string, follow bool) (io. return nil, fmt.Errorf("failed to determine log path: %w", err) } - if _, err := os.Stat(logPath); err != nil { - if os.IsNotExist(err) { - return nil, fmt.Errorf("no logs available for container: %s", id) - } - return nil, err + if !io.Local.IsFile(logPath) { + return nil, fmt.Errorf("no logs available for container: %s", id) } if !follow { @@ -337,7 +336,7 @@ func (m *LinuxKitManager) Logs(ctx context.Context, id string, follow bool) (io. return newFollowReader(ctx, logPath) } -// followReader implements io.ReadCloser for following log files. +// followReader implements goio.ReadCloser for following log files. type followReader struct { file *os.File ctx context.Context @@ -352,7 +351,7 @@ func newFollowReader(ctx context.Context, path string) (*followReader, error) { } // Seek to end - _, _ = file.Seek(0, io.SeekEnd) + _, _ = file.Seek(0, goio.SeekEnd) ctx, cancel := context.WithCancel(ctx) @@ -368,7 +367,7 @@ func (f *followReader) Read(p []byte) (int, error) { for { select { case <-f.ctx.Done(): - return 0, io.EOF + return 0, goio.EOF default: } @@ -376,14 +375,14 @@ func (f *followReader) Read(p []byte) (int, error) { if n > 0 { return n, nil } - if err != nil && err != io.EOF { + if err != nil && err != goio.EOF { return 0, err } // No data available, wait a bit and try again select { case <-f.ctx.Done(): - return 0, io.EOF + return 0, goio.EOF case <-time.After(100 * time.Millisecond): // Reset reader to pick up new data f.reader.Reset(f.file) diff --git a/pkg/container/state.go b/pkg/container/state.go index b8d98b97..e99bb051 100644 --- a/pkg/container/state.go +++ b/pkg/container/state.go @@ -5,6 +5,8 @@ import ( "os" "path/filepath" "sync" + + "github.com/host-uk/core/pkg/io" ) // State manages persistent container state. @@ -56,7 +58,12 @@ func NewState(filePath string) *State { func LoadState(filePath string) (*State, error) { state := NewState(filePath) - data, err := os.ReadFile(filePath) + absPath, err := filepath.Abs(filePath) + if err != nil { + return nil, err + } + + content, err := io.Local.Read(absPath) if err != nil { if os.IsNotExist(err) { return state, nil @@ -64,7 +71,7 @@ func LoadState(filePath string) (*State, error) { return nil, err } - if err := json.Unmarshal(data, state); err != nil { + if err := json.Unmarshal([]byte(content), state); err != nil { return nil, err } @@ -76,9 +83,8 @@ func (s *State) SaveState() error { s.mu.RLock() defer s.mu.RUnlock() - // Ensure the directory exists - dir := filepath.Dir(s.filePath) - if err := os.MkdirAll(dir, 0755); err != nil { + absPath, err := filepath.Abs(s.filePath) + if err != nil { return err } @@ -87,7 +93,8 @@ func (s *State) SaveState() error { return err } - return os.WriteFile(s.filePath, data, 0644) + // io.Local.Write creates parent directories automatically + return io.Local.Write(absPath, string(data)) } // Add adds a container to the state and persists it. @@ -166,5 +173,5 @@ func EnsureLogsDir() error { if err != nil { return err } - return os.MkdirAll(logsDir, 0755) + return io.Local.EnsureDir(logsDir) } diff --git a/pkg/container/templates.go b/pkg/container/templates.go index b0068a00..80ec3005 100644 --- a/pkg/container/templates.go +++ b/pkg/container/templates.go @@ -7,6 +7,8 @@ import ( "path/filepath" "regexp" "strings" + + "github.com/host-uk/core/pkg/io" ) //go:embed templates/*.yml @@ -71,12 +73,12 @@ func GetTemplate(name string) (string, error) { userTemplatesDir := getUserTemplatesDir() if userTemplatesDir != "" { templatePath := filepath.Join(userTemplatesDir, name+".yml") - if _, err := os.Stat(templatePath); err == nil { - content, err := os.ReadFile(templatePath) + if io.Local.IsFile(templatePath) { + content, err := io.Local.Read(templatePath) if err != nil { return "", fmt.Errorf("failed to read user template %s: %w", name, err) } - return string(content), nil + return content, nil } } @@ -194,7 +196,7 @@ func getUserTemplatesDir() string { cwd, err := os.Getwd() if err == nil { wsDir := filepath.Join(cwd, ".core", "linuxkit") - if info, err := os.Stat(wsDir); err == nil && info.IsDir() { + if io.Local.IsDir(wsDir) { return wsDir } } @@ -206,7 +208,7 @@ func getUserTemplatesDir() string { } homeDir := filepath.Join(home, ".core", "linuxkit") - if info, err := os.Stat(homeDir); err == nil && info.IsDir() { + if io.Local.IsDir(homeDir) { return homeDir } @@ -217,7 +219,7 @@ func getUserTemplatesDir() string { func scanUserTemplates(dir string) []Template { var templates []Template - entries, err := os.ReadDir(dir) + entries, err := io.Local.List(dir) if err != nil { return templates } @@ -266,12 +268,12 @@ func scanUserTemplates(dir string) []Template { // extractTemplateDescription reads the first comment block from a YAML file // to use as a description. func extractTemplateDescription(path string) string { - content, err := os.ReadFile(path) + content, err := io.Local.Read(path) if err != nil { return "" } - lines := strings.Split(string(content), "\n") + lines := strings.Split(content, "\n") var descLines []string for _, line := range lines {