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 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-02-02 05:17:12 +00:00
parent d49683fd01
commit 94974a6c3b
4 changed files with 56 additions and 45 deletions

View file

@ -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.

View file

@ -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)

View file

@ -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)
}

View file

@ -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 {