From 6abb69e1a40d28bf40ef4ebc9871f5432cf9a19d Mon Sep 17 00:00:00 2001 From: Virgil Date: Sun, 29 Mar 2026 17:37:24 +0000 Subject: [PATCH] refactor(container): complete AX compliance pass Co-Authored-By: Virgil --- cmd/vm/cmd_container.go | 18 ++++++++++++------ cmd/vm/cmd_templates.go | 25 ++++++++++++++++--------- container.go | 4 +++- devenv/claude.go | 4 ++-- devenv/claude_test.go | 4 ++-- devenv/config.go | 7 ++++--- devenv/config_test.go | 16 ++++++++-------- devenv/devops.go | 14 +++++++------- devenv/devops_test.go | 20 ++++++++++---------- devenv/images.go | 36 +++++++++++++++++++++--------------- devenv/images_test.go | 6 +++--- devenv/serve.go | 9 ++++++--- devenv/serve_test.go | 4 ++-- devenv/shell.go | 14 ++++++++++---- devenv/shell_test.go | 10 +++++----- devenv/ssh_utils.go | 31 +++++++++++++++++++++++-------- devenv/test.go | 10 +++++----- devenv/test_test.go | 6 +++--- hypervisor.go | 5 ++--- linuxkit.go | 31 +++++++++++++++++++++---------- linuxkit_test.go | 2 +- sources/cdn.go | 3 +-- sources/cdn_test.go | 6 +++--- sources/github_test.go | 6 +++--- sources/source_test.go | 6 +++--- state.go | 36 ++++++++++++++++++++++-------------- templates.go | 2 +- 27 files changed, 199 insertions(+), 136 deletions(-) diff --git a/cmd/vm/cmd_container.go b/cmd/vm/cmd_container.go index 2fdbfe8..067a220 100644 --- a/cmd/vm/cmd_container.go +++ b/cmd/vm/cmd_container.go @@ -230,7 +230,7 @@ func stopContainer(id string) error { // Support partial ID matching fullID, err := resolveContainerID(manager, id) if err != nil { - return err + return coreerr.E("stopContainer", "resolve container ID", err) } fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.stop.stopping")), fullID[:8]) @@ -249,7 +249,7 @@ func resolveContainerID(manager *container.LinuxKitManager, partialID string) (s ctx := context.Background() containers, err := manager.List(ctx) if err != nil { - return "", err + return "", coreerr.E("resolveContainerID", "list containers", err) } var matches []*container.Container @@ -298,7 +298,7 @@ func viewLogs(id string, follow bool) error { fullID, err := resolveContainerID(manager, id) if err != nil { - return err + return coreerr.E("viewLogs", "resolve container ID", err) } ctx := context.Background() @@ -309,7 +309,10 @@ func viewLogs(id string, follow bool) error { defer func() { _ = reader.Close() }() _, err = goio.Copy(os.Stdout, reader) - return err + if err != nil { + return coreerr.E("viewLogs", "copy log output", err) + } + return nil } // addVMExecCommand adds the 'exec' command under vm. @@ -337,9 +340,12 @@ func execInContainer(id string, cmd []string) error { fullID, err := resolveContainerID(manager, id) if err != nil { - return err + return coreerr.E("execInContainer", "resolve container ID", err) } ctx := context.Background() - return manager.Exec(ctx, fullID, cmd) + if err := manager.Exec(ctx, fullID, cmd); err != nil { + return coreerr.E("execInContainer", "run command in container", err) + } + return nil } diff --git a/cmd/vm/cmd_templates.go b/cmd/vm/cmd_templates.go index 4d14a04..1732bd2 100644 --- a/cmd/vm/cmd_templates.go +++ b/cmd/vm/cmd_templates.go @@ -102,7 +102,7 @@ func listTemplates() error { func showTemplate(name string) error { content, err := container.GetTemplate(name) if err != nil { - return err + return coreerr.E("showTemplate", "get template", err) } fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("common.label.template")), repoNameStyle.Render(name)) @@ -114,7 +114,7 @@ func showTemplate(name string) error { func showTemplateVars(name string) error { content, err := container.GetTemplate(name) if err != nil { - return err + return coreerr.E("showTemplateVars", "get template", err) } required, optional := container.ExtractVariables(content) @@ -155,11 +155,15 @@ func RunFromTemplate(templateName string, vars map[string]string, runOpts contai } // Create a temporary directory for the build - tmpDir, err := os.MkdirTemp("", "core-linuxkit-*") + tmpID, err := container.GenerateID() if err != nil { return coreerr.E("RunFromTemplate", i18n.T("common.error.failed", map[string]any{"Action": "create temp directory"}), err) } - defer func() { _ = os.RemoveAll(tmpDir) }() + tmpDir := filepath.Join(os.TempDir(), "core-linuxkit-"+tmpID) + if err := io.Local.EnsureDir(tmpDir); err != nil { + return coreerr.E("RunFromTemplate", i18n.T("common.error.failed", map[string]any{"Action": "create temp directory"}), err) + } + defer func() { _ = io.Local.DeleteAll(tmpDir) }() // Write the YAML file yamlPath := filepath.Join(tmpDir, templateName+".yml") @@ -218,7 +222,7 @@ func buildLinuxKitImage(yamlPath, outputPath string) error { // Check if linuxkit is available lkPath, err := lookupLinuxKit() if err != nil { - return err + return coreerr.E("buildLinuxKitImage", "find linuxkit binary", err) } // Build the image @@ -231,7 +235,10 @@ func buildLinuxKitImage(yamlPath, outputPath string) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - return cmd.Run() + if err := cmd.Run(); err != nil { + return coreerr.E("buildLinuxKitImage", "run linuxkit build", err) + } + return nil } // findBuiltImage finds the built image file. @@ -241,7 +248,7 @@ func findBuiltImage(basePath string) string { for _, ext := range extensions { path := basePath + ext - if _, err := os.Stat(path); err == nil { + if io.Local.IsFile(path) { return path } } @@ -250,7 +257,7 @@ func findBuiltImage(basePath string) string { dir := filepath.Dir(basePath) base := filepath.Base(basePath) - entries, err := os.ReadDir(dir) + entries, err := io.Local.List(dir) if err != nil { return "" } @@ -283,7 +290,7 @@ func lookupLinuxKit() (string, error) { } for _, p := range paths { - if _, err := os.Stat(p); err == nil { + if io.Local.IsFile(p) { return p, nil } } diff --git a/container.go b/container.go index d7161c3..65924ba 100644 --- a/container.go +++ b/container.go @@ -9,6 +9,8 @@ import ( "encoding/hex" "io" "time" + + coreerr "dappco.re/go/core/log" ) // Container represents a running LinuxKit container/VM instance. @@ -84,7 +86,7 @@ type Manager interface { func GenerateID() (string, error) { bytes := make([]byte, 4) if _, err := rand.Read(bytes); err != nil { - return "", err + return "", coreerr.E("GenerateID", "read random bytes", err) } return hex.EncodeToString(bytes), nil } diff --git a/devenv/claude.go b/devenv/claude.go index bf3de40..76dd91c 100644 --- a/devenv/claude.go +++ b/devenv/claude.go @@ -24,7 +24,7 @@ func (d *DevOps) Claude(ctx context.Context, projectDir string, opts ClaudeOptio // Auto-boot if not running running, err := d.IsRunning(ctx) if err != nil { - return err + return coreerr.E("DevOps.Claude", "check running state", err) } if !running { fmt.Println("Dev environment not running, booting...") @@ -123,7 +123,7 @@ func formatAuthList(opts ClaudeOptions) string { func (d *DevOps) CopyGHAuth(ctx context.Context) error { home, err := os.UserHomeDir() if err != nil { - return err + return coreerr.E("DevOps.CopyGHAuth", "get home dir", err) } ghConfigDir := filepath.Join(home, ".config", "gh") diff --git a/devenv/claude_test.go b/devenv/claude_test.go index 179ef6c..5387ef8 100644 --- a/devenv/claude_test.go +++ b/devenv/claude_test.go @@ -6,14 +6,14 @@ import ( "github.com/stretchr/testify/assert" ) -func TestClaudeOptions_Default(t *testing.T) { +func TestClaudeOptions_Good_Default(t *testing.T) { opts := ClaudeOptions{} assert.False(t, opts.NoAuth) assert.Nil(t, opts.Auth) assert.Empty(t, opts.Model) } -func TestClaudeOptions_Custom(t *testing.T) { +func TestClaudeOptions_Good_Custom(t *testing.T) { opts := ClaudeOptions{ NoAuth: true, Auth: []string{"gh", "anthropic"}, diff --git a/devenv/config.go b/devenv/config.go index 8fefb7e..0a76180 100644 --- a/devenv/config.go +++ b/devenv/config.go @@ -5,6 +5,7 @@ import ( "path/filepath" "dappco.re/go/core/io" + coreerr "dappco.re/go/core/log" "forge.lthn.ai/core/config" ) @@ -57,7 +58,7 @@ func DefaultConfig() *Config { func ConfigPath() (string, error) { home, err := os.UserHomeDir() if err != nil { - return "", err + return "", coreerr.E("ConfigPath", "get home dir", err) } return filepath.Join(home, ".core", "config.yaml"), nil } @@ -79,11 +80,11 @@ func LoadConfig(m io.Medium) (*Config, error) { // Use centralized config service c, err := config.New(config.WithMedium(m), config.WithPath(configPath)) if err != nil { - return nil, err + return nil, coreerr.E("LoadConfig", "create config service", err) } if err := c.Get("", cfg); err != nil { - return nil, err + return nil, coreerr.E("LoadConfig", "load config", err) } return cfg, nil diff --git a/devenv/config_test.go b/devenv/config_test.go index 5090339..fcb5bfd 100644 --- a/devenv/config_test.go +++ b/devenv/config_test.go @@ -10,14 +10,14 @@ import ( "github.com/stretchr/testify/require" ) -func TestDefaultConfig(t *testing.T) { +func TestDefaultConfig_Good(t *testing.T) { cfg := DefaultConfig() assert.Equal(t, 1, cfg.Version) assert.Equal(t, "auto", cfg.Images.Source) assert.Equal(t, "host-uk/core-images", cfg.Images.GitHub.Repo) } -func TestConfigPath(t *testing.T) { +func TestConfigPath_Good(t *testing.T) { path, err := ConfigPath() assert.NoError(t, err) assert.Contains(t, path, ".core/config.yaml") @@ -79,7 +79,7 @@ func TestLoadConfig_Bad(t *testing.T) { }) } -func TestConfig_Struct(t *testing.T) { +func TestConfig_Good_Struct(t *testing.T) { cfg := &Config{ Version: 2, Images: ImagesConfig{ @@ -102,7 +102,7 @@ func TestConfig_Struct(t *testing.T) { assert.Equal(t, "https://cdn.example.com", cfg.Images.CDN.URL) } -func TestDefaultConfig_Complete(t *testing.T) { +func TestDefaultConfig_Good_Complete(t *testing.T) { cfg := DefaultConfig() assert.Equal(t, 1, cfg.Version) assert.Equal(t, "auto", cfg.Images.Source) @@ -205,7 +205,7 @@ images: } } -func TestImagesConfig_Struct(t *testing.T) { +func TestImagesConfig_Good_Struct(t *testing.T) { ic := ImagesConfig{ Source: "auto", GitHub: GitHubConfig{Repo: "test/repo"}, @@ -214,17 +214,17 @@ func TestImagesConfig_Struct(t *testing.T) { assert.Equal(t, "test/repo", ic.GitHub.Repo) } -func TestGitHubConfig_Struct(t *testing.T) { +func TestGitHubConfig_Good_Struct(t *testing.T) { gc := GitHubConfig{Repo: "owner/repo"} assert.Equal(t, "owner/repo", gc.Repo) } -func TestRegistryConfig_Struct(t *testing.T) { +func TestRegistryConfig_Good_Struct(t *testing.T) { rc := RegistryConfig{Image: "ghcr.io/owner/image:latest"} assert.Equal(t, "ghcr.io/owner/image:latest", rc.Image) } -func TestCDNConfig_Struct(t *testing.T) { +func TestCDNConfig_Good_Struct(t *testing.T) { cc := CDNConfig{URL: "https://cdn.example.com/images"} assert.Equal(t, "https://cdn.example.com/images", cc.URL) } diff --git a/devenv/devops.go b/devenv/devops.go index ea21672..519539d 100644 --- a/devenv/devops.go +++ b/devenv/devops.go @@ -64,7 +64,7 @@ func ImagesDir() (string, error) { } home, err := os.UserHomeDir() if err != nil { - return "", err + return "", coreerr.E("ImagesDir", "get home dir", err) } return filepath.Join(home, ".core", "images"), nil } @@ -73,7 +73,7 @@ func ImagesDir() (string, error) { func ImagePath() (string, error) { dir, err := ImagesDir() if err != nil { - return "", err + return "", coreerr.E("ImagePath", "get images directory", err) } return filepath.Join(dir, ImageName()), nil } @@ -135,7 +135,7 @@ func (d *DevOps) Boot(ctx context.Context, opts BootOptions) error { imagePath, err := ImagePath() if err != nil { - return err + return coreerr.E("DevOps.Boot", "get image path", err) } // Build run options for LinuxKitManager @@ -149,7 +149,7 @@ func (d *DevOps) Boot(ctx context.Context, opts BootOptions) error { _, err = d.container.Run(ctx, imagePath, runOpts) if err != nil { - return err + return coreerr.E("DevOps.Boot", "run container", err) } // Wait for SSH to be ready and scan host key @@ -175,7 +175,7 @@ func (d *DevOps) Boot(ctx context.Context, opts BootOptions) error { func (d *DevOps) Stop(ctx context.Context) error { c, err := d.findContainer(ctx, "core-dev") if err != nil { - return err + return coreerr.E("DevOps.Stop", "find container", err) } if c == nil { return coreerr.E("DevOps.Stop", "dev environment not found", nil) @@ -187,7 +187,7 @@ func (d *DevOps) Stop(ctx context.Context) error { func (d *DevOps) IsRunning(ctx context.Context) (bool, error) { c, err := d.findContainer(ctx, "core-dev") if err != nil { - return false, err + return false, coreerr.E("DevOps.IsRunning", "find container", err) } return c != nil && c.Status == container.StatusRunning, nil } @@ -196,7 +196,7 @@ func (d *DevOps) IsRunning(ctx context.Context) (bool, error) { func (d *DevOps) findContainer(ctx context.Context, name string) (*container.Container, error) { containers, err := d.container.List(ctx) if err != nil { - return nil, err + return nil, coreerr.E("DevOps.findContainer", "list containers", err) } for _, c := range containers { if c.Name == name { diff --git a/devenv/devops_test.go b/devenv/devops_test.go index 55f9e79..d617434 100644 --- a/devenv/devops_test.go +++ b/devenv/devops_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestImageName(t *testing.T) { +func TestImageName_Good(t *testing.T) { name := ImageName() assert.Contains(t, name, "core-devops-") assert.Contains(t, name, runtime.GOOS) @@ -23,7 +23,7 @@ func TestImageName(t *testing.T) { assert.True(t, (name[len(name)-6:] == ".qcow2")) } -func TestImagesDir(t *testing.T) { +func TestImagesDir_Good(t *testing.T) { t.Run("default directory", func(t *testing.T) { // Unset env if it exists orig := os.Getenv("CORE_IMAGES_DIR") @@ -45,7 +45,7 @@ func TestImagesDir(t *testing.T) { }) } -func TestImagePath(t *testing.T) { +func TestImagePath_Good(t *testing.T) { customDir := "/tmp/images" t.Setenv("CORE_IMAGES_DIR", customDir) @@ -55,7 +55,7 @@ func TestImagePath(t *testing.T) { assert.Equal(t, expected, path) } -func TestDefaultBootOptions(t *testing.T) { +func TestDefaultBootOptions_Good(t *testing.T) { opts := DefaultBootOptions() assert.Equal(t, 4096, opts.Memory) assert.Equal(t, 2, opts.CPUs) @@ -366,7 +366,7 @@ func TestDevOps_Stop_Bad_NotFound(t *testing.T) { assert.Contains(t, err.Error(), "not found") } -func TestBootOptions_Custom(t *testing.T) { +func TestBootOptions_Good_Custom(t *testing.T) { opts := BootOptions{ Memory: 8192, CPUs: 4, @@ -379,7 +379,7 @@ func TestBootOptions_Custom(t *testing.T) { assert.True(t, opts.Fresh) } -func TestDevStatus_Struct(t *testing.T) { +func TestDevStatus_Good_Struct(t *testing.T) { status := DevStatus{ Installed: true, Running: true, @@ -738,7 +738,7 @@ func TestDevOps_Boot_Good_FreshWithNoExisting(t *testing.T) { assert.NoError(t, err) } -func TestImageName_Format(t *testing.T) { +func TestImageName_Good_Format(t *testing.T) { name := ImageName() // Check format: core-devops-{os}-{arch}.qcow2 assert.Contains(t, name, "core-devops-") @@ -747,7 +747,7 @@ func TestImageName_Format(t *testing.T) { assert.True(t, filepath.Ext(name) == ".qcow2") } -func TestDevOps_Install_Delegates(t *testing.T) { +func TestDevOps_Install_Good_Delegates(t *testing.T) { // This test verifies the Install method delegates to ImageManager tempDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tempDir) @@ -765,7 +765,7 @@ func TestDevOps_Install_Delegates(t *testing.T) { assert.Error(t, err) } -func TestDevOps_CheckUpdate_Delegates(t *testing.T) { +func TestDevOps_CheckUpdate_Good_Delegates(t *testing.T) { // This test verifies the CheckUpdate method delegates to ImageManager tempDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tempDir) @@ -815,7 +815,7 @@ func TestDevOps_Boot_Good_Success(t *testing.T) { assert.NoError(t, err) // Mock hypervisor succeeds } -func TestDevOps_Config(t *testing.T) { +func TestDevOps_Good_Config(t *testing.T) { tempDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tempDir) diff --git a/devenv/images.go b/devenv/images.go index ce5edf6..9e724de 100644 --- a/devenv/images.go +++ b/devenv/images.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "os" "path/filepath" "time" @@ -40,19 +39,19 @@ type ImageInfo struct { func NewImageManager(m io.Medium, cfg *Config) (*ImageManager, error) { imagesDir, err := ImagesDir() if err != nil { - return nil, err + return nil, coreerr.E("NewImageManager", "get images directory", err) } // Ensure images directory exists if err := m.EnsureDir(imagesDir); err != nil { - return nil, err + return nil, coreerr.E("NewImageManager", "ensure images directory", err) } // Load or create manifest manifestPath := filepath.Join(imagesDir, "manifest.json") manifest, err := loadManifest(m, manifestPath) if err != nil { - return nil, err + return nil, coreerr.E("NewImageManager", "load manifest", err) } // Build source list based on config @@ -98,7 +97,7 @@ func (m *ImageManager) IsInstalled() bool { func (m *ImageManager) Install(ctx context.Context, progress func(downloaded, total int64)) error { imagesDir, err := ImagesDir() if err != nil { - return err + return coreerr.E("ImageManager.Install", "get images directory", err) } // Find first available source @@ -123,7 +122,7 @@ func (m *ImageManager) Install(ctx context.Context, progress func(downloaded, to // Download if err := src.Download(ctx, m.medium, imagesDir, progress); err != nil { - return err + return coreerr.E("ImageManager.Install", "download image", err) } // Update manifest @@ -133,7 +132,10 @@ func (m *ImageManager) Install(ctx context.Context, progress func(downloaded, to Source: src.Name(), } - return m.manifest.Save() + if err := m.manifest.Save(); err != nil { + return coreerr.E("ImageManager.Install", "save manifest", err) + } + return nil } // CheckUpdate checks if an update is available. @@ -158,7 +160,7 @@ func (m *ImageManager) CheckUpdate(ctx context.Context) (current, latest string, latest, err = src.LatestVersion(ctx) if err != nil { - return current, "", false, err + return current, "", false, coreerr.E("ImageManager.CheckUpdate", "get latest version", err) } hasUpdate = current != latest @@ -172,16 +174,17 @@ func loadManifest(m io.Medium, path string) (*Manifest, error) { path: path, } + if !m.Exists(path) { + return manifest, nil + } + content, err := m.Read(path) if err != nil { - if os.IsNotExist(err) { - return manifest, nil - } - return nil, err + return nil, coreerr.E("loadManifest", "read manifest", err) } if err := json.Unmarshal([]byte(content), manifest); err != nil { - return nil, err + return nil, coreerr.E("loadManifest", "unmarshal manifest", err) } manifest.medium = m manifest.path = path @@ -193,7 +196,10 @@ func loadManifest(m io.Medium, path string) (*Manifest, error) { func (m *Manifest) Save() error { data, err := json.MarshalIndent(m, "", " ") if err != nil { - return err + return coreerr.E("Manifest.Save", "marshal manifest", err) } - return m.medium.Write(m.path, string(data)) + if err := m.medium.Write(m.path, string(data)); err != nil { + return coreerr.E("Manifest.Save", "write manifest", err) + } + return nil } diff --git a/devenv/images_test.go b/devenv/images_test.go index f1b1a83..51ba964 100644 --- a/devenv/images_test.go +++ b/devenv/images_test.go @@ -63,7 +63,7 @@ func TestNewImageManager_Good(t *testing.T) { }) } -func TestManifest_Save(t *testing.T) { +func TestManifest_Save_Good(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "manifest.json") @@ -171,7 +171,7 @@ func TestLoadManifest_Good_ExistingData(t *testing.T) { assert.Equal(t, "cdn", m.Images["test.img"].Source) } -func TestImageInfo_Struct(t *testing.T) { +func TestImageInfo_Good_Struct(t *testing.T) { info := ImageInfo{ Version: "1.0.0", SHA256: "abc123", @@ -570,7 +570,7 @@ func TestImageManager_CheckUpdate_Good_FirstSourceUnavailable(t *testing.T) { assert.True(t, hasUpdate) } -func TestManifest_Struct(t *testing.T) { +func TestManifest_Good_Struct(t *testing.T) { m := &Manifest{ Images: map[string]ImageInfo{ "test.img": {Version: "1.0.0"}, diff --git a/devenv/serve.go b/devenv/serve.go index 16bf96d..cbc6739 100644 --- a/devenv/serve.go +++ b/devenv/serve.go @@ -21,7 +21,7 @@ type ServeOptions struct { func (d *DevOps) Serve(ctx context.Context, projectDir string, opts ServeOptions) error { running, err := d.IsRunning(ctx) if err != nil { - return err + return coreerr.E("DevOps.Serve", "check running state", err) } if !running { return coreerr.E("DevOps.Serve", "dev environment not running (run 'core dev boot' first)", nil) @@ -54,7 +54,7 @@ func (d *DevOps) Serve(ctx context.Context, projectDir string, opts ServeOptions func (d *DevOps) mountProject(ctx context.Context, path string) error { absPath, err := filepath.Abs(path) if err != nil { - return err + return coreerr.E("DevOps.mountProject", "get absolute path", err) } // Use reverse SSHFS mount @@ -68,7 +68,10 @@ func (d *DevOps) mountProject(ctx context.Context, path string) error { "root@localhost", fmt.Sprintf("mkdir -p /app && sshfs -p 10000 %s@localhost:%s /app -o allow_other", os.Getenv("USER"), absPath), ) - return cmd.Run() + if err := cmd.Run(); err != nil { + return coreerr.E("DevOps.mountProject", "run sshfs mount", err) + } + return nil } // DetectServeCommand auto-detects the serve command for a project. diff --git a/devenv/serve_test.go b/devenv/serve_test.go index 5dfd4ee..7b0b1e2 100644 --- a/devenv/serve_test.go +++ b/devenv/serve_test.go @@ -96,13 +96,13 @@ func TestDetectServeCommand_Good_Priority(t *testing.T) { assert.Equal(t, "php artisan octane:start --host=0.0.0.0 --port=8000", cmd) } -func TestServeOptions_Default(t *testing.T) { +func TestServeOptions_Good_Default(t *testing.T) { opts := ServeOptions{} assert.Equal(t, 0, opts.Port) assert.Equal(t, "", opts.Path) } -func TestServeOptions_Custom(t *testing.T) { +func TestServeOptions_Good_Custom(t *testing.T) { opts := ServeOptions{ Port: 3000, Path: "public", diff --git a/devenv/shell.go b/devenv/shell.go index 0b10e02..cdcdd93 100644 --- a/devenv/shell.go +++ b/devenv/shell.go @@ -19,7 +19,7 @@ type ShellOptions struct { func (d *DevOps) Shell(ctx context.Context, opts ShellOptions) error { running, err := d.IsRunning(ctx) if err != nil { - return err + return coreerr.E("DevOps.Shell", "check running state", err) } if !running { return coreerr.E("DevOps.Shell", "dev environment not running (run 'core dev boot' first)", nil) @@ -52,7 +52,10 @@ func (d *DevOps) sshShell(ctx context.Context, command []string) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - return cmd.Run() + if err := cmd.Run(); err != nil { + return coreerr.E("DevOps.sshShell", "run ssh command", err) + } + return nil } // serialConsole attaches to the QEMU serial console. @@ -60,7 +63,7 @@ func (d *DevOps) serialConsole(ctx context.Context) error { // Find the container to get its console socket c, err := d.findContainer(ctx, "core-dev") if err != nil { - return err + return coreerr.E("DevOps.serialConsole", "find container", err) } if c == nil { return coreerr.E("DevOps.serialConsole", "console not available: container not found", nil) @@ -72,5 +75,8 @@ func (d *DevOps) serialConsole(ctx context.Context) error { cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - return cmd.Run() + if err := cmd.Run(); err != nil { + return coreerr.E("DevOps.serialConsole", "run socat", err) + } + return nil } diff --git a/devenv/shell_test.go b/devenv/shell_test.go index b9d57b7..b1e33f4 100644 --- a/devenv/shell_test.go +++ b/devenv/shell_test.go @@ -6,13 +6,13 @@ import ( "github.com/stretchr/testify/assert" ) -func TestShellOptions_Default(t *testing.T) { +func TestShellOptions_Good_Default(t *testing.T) { opts := ShellOptions{} assert.False(t, opts.Console) assert.Nil(t, opts.Command) } -func TestShellOptions_Console(t *testing.T) { +func TestShellOptions_Good_Console(t *testing.T) { opts := ShellOptions{ Console: true, } @@ -20,7 +20,7 @@ func TestShellOptions_Console(t *testing.T) { assert.Nil(t, opts.Command) } -func TestShellOptions_Command(t *testing.T) { +func TestShellOptions_Good_Command(t *testing.T) { opts := ShellOptions{ Command: []string{"ls", "-la"}, } @@ -28,7 +28,7 @@ func TestShellOptions_Command(t *testing.T) { assert.Equal(t, []string{"ls", "-la"}, opts.Command) } -func TestShellOptions_ConsoleWithCommand(t *testing.T) { +func TestShellOptions_Good_ConsoleWithCommand(t *testing.T) { opts := ShellOptions{ Console: true, Command: []string{"echo", "hello"}, @@ -37,7 +37,7 @@ func TestShellOptions_ConsoleWithCommand(t *testing.T) { assert.Equal(t, []string{"echo", "hello"}, opts.Command) } -func TestShellOptions_EmptyCommand(t *testing.T) { +func TestShellOptions_Good_EmptyCommand(t *testing.T) { opts := ShellOptions{ Command: []string{}, } diff --git a/devenv/ssh_utils.go b/devenv/ssh_utils.go index e1afc9c..e5a2bda 100644 --- a/devenv/ssh_utils.go +++ b/devenv/ssh_utils.go @@ -44,15 +44,22 @@ func ensureHostKey(ctx context.Context, port int) error { } // Read existing known_hosts to avoid duplicates - existingStr, _ := coreio.Local.Read(knownHostsPath) + existingStr := "" + if coreio.Local.IsFile(knownHostsPath) { + existingStr, err = coreio.Local.Read(knownHostsPath) + if err != nil { + return coreerr.E("ensureHostKey", "read known_hosts", err) + } + } // Append new keys that aren't already there - f, err := os.OpenFile(knownHostsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) - if err != nil { - return coreerr.E("ensureHostKey", "open known_hosts", err) + var updated strings.Builder + updated.WriteString(existingStr) + if len(existingStr) > 0 && !strings.HasSuffix(existingStr, "\n") { + updated.WriteString("\n") } - defer f.Close() + wroteLine := false lines := strings.Split(string(out), "\n") for _, line := range lines { line = strings.TrimSpace(line) @@ -60,11 +67,19 @@ func ensureHostKey(ctx context.Context, port int) error { continue } if !strings.Contains(existingStr, line) { - if _, err := f.WriteString(line + "\n"); err != nil { - return coreerr.E("ensureHostKey", "write known_hosts", err) - } + updated.WriteString(line) + updated.WriteString("\n") + wroteLine = true } } + if !wroteLine { + return nil + } + + if err := coreio.Local.WriteMode(knownHostsPath, updated.String(), 0600); err != nil { + return coreerr.E("ensureHostKey", "write known_hosts", err) + } + return nil } diff --git a/devenv/test.go b/devenv/test.go index 5ec7957..279005a 100644 --- a/devenv/test.go +++ b/devenv/test.go @@ -35,7 +35,7 @@ type TestOptions struct { func (d *DevOps) Test(ctx context.Context, projectDir string, opts TestOptions) error { running, err := d.IsRunning(ctx) if err != nil { - return err + return coreerr.E("DevOps.Test", "check running state", err) } if !running { return coreerr.E("DevOps.Test", "dev environment not running (run 'core dev boot' first)", nil) @@ -49,7 +49,7 @@ func (d *DevOps) Test(ctx context.Context, projectDir string, opts TestOptions) } else if opts.Name != "" { cfg, err := LoadTestConfig(d.medium, projectDir) if err != nil { - return err + return coreerr.E("DevOps.Test", "load test config", err) } for _, c := range cfg.Commands { if c.Name == opts.Name { @@ -116,17 +116,17 @@ func LoadTestConfig(m io.Medium, projectDir string) (*TestConfig, error) { path := filepath.Join(projectDir, ".core", "test.yaml") absPath, err := filepath.Abs(path) if err != nil { - return nil, err + return nil, coreerr.E("LoadTestConfig", "get absolute path", err) } content, err := m.Read(absPath) if err != nil { - return nil, err + return nil, coreerr.E("LoadTestConfig", "read test config", err) } var cfg TestConfig if err := yaml.Unmarshal([]byte(content), &cfg); err != nil { - return nil, err + return nil, coreerr.E("LoadTestConfig", "unmarshal test config", err) } return &cfg, nil diff --git a/devenv/test_test.go b/devenv/test_test.go index baa5d8c..d0b1641 100644 --- a/devenv/test_test.go +++ b/devenv/test_test.go @@ -180,7 +180,7 @@ func TestHasComposerScript_Bad_MissingScript(t *testing.T) { } } -func TestTestConfig_Struct(t *testing.T) { +func TestTestConfig_Good_Struct(t *testing.T) { cfg := &TestConfig{ Version: 2, Command: "my-test", @@ -201,7 +201,7 @@ func TestTestConfig_Struct(t *testing.T) { } } -func TestTestCommand_Struct(t *testing.T) { +func TestTestCommand_Good_Struct(t *testing.T) { cmd := TestCommand{ Name: "integration", Run: "go test -tags=integration ./...", @@ -214,7 +214,7 @@ func TestTestCommand_Struct(t *testing.T) { } } -func TestTestOptions_Struct(t *testing.T) { +func TestTestOptions_Good_Struct(t *testing.T) { opts := TestOptions{ Name: "unit", Command: []string{"go", "test", "-v"}, diff --git a/hypervisor.go b/hypervisor.go index 087093d..e525960 100644 --- a/hypervisor.go +++ b/hypervisor.go @@ -3,12 +3,12 @@ package container import ( "context" "fmt" - "os" "os/exec" "path/filepath" "runtime" "strings" + "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" ) @@ -141,8 +141,7 @@ func (q *QemuHypervisor) BuildCommand(ctx context.Context, image string, opts *H // isKVMAvailable checks if KVM is available on the system. func isKVMAvailable() bool { - _, err := os.Stat("/dev/kvm") - return err == nil + return io.Local.Exists("/dev/kvm") } // HyperkitHypervisor implements Hypervisor for macOS Hyperkit. diff --git a/linuxkit.go b/linuxkit.go index c1cf962..3598a0c 100644 --- a/linuxkit.go +++ b/linuxkit.go @@ -35,7 +35,7 @@ func NewLinuxKitManager(m io.Medium) (*LinuxKitManager, error) { hypervisor, err := DetectHypervisor() if err != nil { - return nil, err + return nil, coreerr.E("NewLinuxKitManager", "detect hypervisor", err) } return &LinuxKitManager{ @@ -119,7 +119,7 @@ func (m *LinuxKitManager) Run(ctx context.Context, image string, opts RunOptions } // Create log file - logFile, err := os.Create(logPath) + logFile, err := io.Local.Create(logPath) if err != nil { return nil, coreerr.E("LinuxKitManager.Run", "failed to create log file", err) } @@ -298,7 +298,7 @@ func (m *LinuxKitManager) Stop(ctx context.Context, id string) error { // List returns all known containers, verifying process state. func (m *LinuxKitManager) List(ctx context.Context) ([]*Container, error) { if err := ctx.Err(); err != nil { - return nil, err + return nil, coreerr.E("LinuxKitManager.List", "check context", err) } containers := m.state.All() @@ -330,7 +330,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) (goio.ReadCloser, error) { if err := ctx.Err(); err != nil { - return nil, err + return nil, coreerr.E("LinuxKitManager.Logs", "check context", err) } _, ok := m.state.Get(id) if !ok { @@ -348,11 +348,19 @@ func (m *LinuxKitManager) Logs(ctx context.Context, id string, follow bool) (goi if !follow { // Simple case: just open and return the file - return m.medium.Open(logPath) + file, err := m.medium.Open(logPath) + if err != nil { + return nil, coreerr.E("LinuxKitManager.Logs", "open log file", err) + } + return file, nil } // Follow mode: create a reader that tails the file - return newFollowReader(ctx, m.medium, logPath) + reader, err := newFollowReader(ctx, m.medium, logPath) + if err != nil { + return nil, coreerr.E("LinuxKitManager.Logs", "create follow reader", err) + } + return reader, nil } // followReader implements goio.ReadCloser for following log files. @@ -368,7 +376,7 @@ type followReader struct { func newFollowReader(ctx context.Context, m io.Medium, path string) (*followReader, error) { file, err := m.Open(path) if err != nil { - return nil, err + return nil, coreerr.E("newFollowReader", "open log file", err) } // Note: We don't seek here because Medium.Open doesn't guarantee Seekability. @@ -398,7 +406,7 @@ func (f *followReader) Read(p []byte) (int, error) { return n, nil } if err != nil && err != goio.EOF { - return 0, err + return 0, coreerr.E("followReader.Read", "read log stream", err) } // No data available, wait a bit and try again @@ -420,7 +428,7 @@ func (f *followReader) Close() error { // Exec executes a command inside the container via SSH. func (m *LinuxKitManager) Exec(ctx context.Context, id string, cmd []string) error { if err := ctx.Err(); err != nil { - return err + return coreerr.E("LinuxKitManager.Exec", "check context", err) } container, ok := m.state.Get(id) if !ok { @@ -449,7 +457,10 @@ func (m *LinuxKitManager) Exec(ctx context.Context, id string, cmd []string) err sshCmd.Stdout = os.Stdout sshCmd.Stderr = os.Stderr - return sshCmd.Run() + if err := sshCmd.Run(); err != nil { + return coreerr.E("LinuxKitManager.Exec", "run ssh command", err) + } + return nil } // State returns the manager's state (for testing). diff --git a/linuxkit_test.go b/linuxkit_test.go index 08496c3..49496c5 100644 --- a/linuxkit_test.go +++ b/linuxkit_test.go @@ -629,7 +629,7 @@ func TestLinuxKitManager_Stop_Good_ContextCancelled(t *testing.T) { err = manager.Stop(cancelCtx, container.ID) // Should return context error assert.Error(t, err) - assert.Equal(t, context.Canceled, err) + assert.ErrorIs(t, err, context.Canceled) } func TestIsProcessRunning_Good_ExistingProcess(t *testing.T) { diff --git a/sources/cdn.go b/sources/cdn.go index 9503ce6..9fc772b 100644 --- a/sources/cdn.go +++ b/sources/cdn.go @@ -5,7 +5,6 @@ import ( "fmt" goio "io" "net/http" - "os" "path/filepath" "dappco.re/go/core/io" @@ -80,7 +79,7 @@ func (s *CDNSource) Download(ctx context.Context, m io.Medium, dest string, prog // Create destination file destPath := filepath.Join(dest, s.config.ImageName) - f, err := os.Create(destPath) + f, err := m.Create(destPath) if err != nil { return coreerr.E("cdn.Download", "create destination file", err) } diff --git a/sources/cdn_test.go b/sources/cdn_test.go index 50fc12b..9d63431 100644 --- a/sources/cdn_test.go +++ b/sources/cdn_test.go @@ -235,12 +235,12 @@ func TestCDNSource_Download_Bad_HTTPErrorCodes(t *testing.T) { } } -func TestCDNSource_InterfaceCompliance(t *testing.T) { +func TestCDNSource_Good_InterfaceCompliance(t *testing.T) { // Verify CDNSource implements ImageSource var _ ImageSource = (*CDNSource)(nil) } -func TestCDNSource_Config(t *testing.T) { +func TestCDNSource_Good_Config(t *testing.T) { cfg := SourceConfig{ CDNURL: "https://cdn.example.com", ImageName: "my-image.qcow2", @@ -291,7 +291,7 @@ func TestCDNSource_Download_Good_CreatesDestDir(t *testing.T) { assert.True(t, info.IsDir()) } -func TestSourceConfig_Struct(t *testing.T) { +func TestSourceConfig_Good_Struct(t *testing.T) { cfg := SourceConfig{ GitHubRepo: "owner/repo", RegistryImage: "ghcr.io/owner/image", diff --git a/sources/github_test.go b/sources/github_test.go index 7281129..cd0a663 100644 --- a/sources/github_test.go +++ b/sources/github_test.go @@ -20,12 +20,12 @@ func TestGitHubSource_Good_Available(t *testing.T) { _ = src.Available() } -func TestGitHubSource_Name(t *testing.T) { +func TestGitHubSource_Good_Name(t *testing.T) { src := NewGitHubSource(SourceConfig{}) assert.Equal(t, "github", src.Name()) } -func TestGitHubSource_Config(t *testing.T) { +func TestGitHubSource_Good_Config(t *testing.T) { cfg := SourceConfig{ GitHubRepo: "owner/repo", ImageName: "test-image.qcow2", @@ -62,7 +62,7 @@ func TestNewGitHubSource_Good(t *testing.T) { assert.Equal(t, cfg.GitHubRepo, src.config.GitHubRepo) } -func TestGitHubSource_InterfaceCompliance(t *testing.T) { +func TestGitHubSource_Good_InterfaceCompliance(t *testing.T) { // Verify GitHubSource implements ImageSource var _ ImageSource = (*GitHubSource)(nil) } diff --git a/sources/source_test.go b/sources/source_test.go index a63f09b..e46ceee 100644 --- a/sources/source_test.go +++ b/sources/source_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSourceConfig_Empty(t *testing.T) { +func TestSourceConfig_Good_Empty(t *testing.T) { cfg := SourceConfig{} assert.Empty(t, cfg.GitHubRepo) assert.Empty(t, cfg.RegistryImage) @@ -14,7 +14,7 @@ func TestSourceConfig_Empty(t *testing.T) { assert.Empty(t, cfg.ImageName) } -func TestSourceConfig_Complete(t *testing.T) { +func TestSourceConfig_Good_Complete(t *testing.T) { cfg := SourceConfig{ GitHubRepo: "owner/repo", RegistryImage: "ghcr.io/owner/image:v1", @@ -28,7 +28,7 @@ func TestSourceConfig_Complete(t *testing.T) { assert.Equal(t, "my-image-darwin-arm64.qcow2", cfg.ImageName) } -func TestImageSource_Interface(t *testing.T) { +func TestImageSource_Good_Interface(t *testing.T) { // Ensure both sources implement the interface var _ ImageSource = (*GitHubSource)(nil) var _ ImageSource = (*CDNSource)(nil) diff --git a/state.go b/state.go index cbe3ad9..ad34e35 100644 --- a/state.go +++ b/state.go @@ -7,6 +7,7 @@ import ( "sync" "dappco.re/go/core/io" + coreerr "dappco.re/go/core/log" ) // State manages persistent container state. @@ -22,7 +23,7 @@ type State struct { func DefaultStateDir() (string, error) { home, err := os.UserHomeDir() if err != nil { - return "", err + return "", coreerr.E("DefaultStateDir", "get home dir", err) } return filepath.Join(home, ".core"), nil } @@ -31,7 +32,7 @@ func DefaultStateDir() (string, error) { func DefaultStatePath() (string, error) { dir, err := DefaultStateDir() if err != nil { - return "", err + return "", coreerr.E("DefaultStatePath", "get state directory", err) } return filepath.Join(dir, "containers.json"), nil } @@ -40,7 +41,7 @@ func DefaultStatePath() (string, error) { func DefaultLogsDir() (string, error) { dir, err := DefaultStateDir() if err != nil { - return "", err + return "", coreerr.E("DefaultLogsDir", "get state directory", err) } return filepath.Join(dir, "logs"), nil } @@ -58,16 +59,17 @@ func NewState(filePath string) *State { func LoadState(filePath string) (*State, error) { state := NewState(filePath) + if !io.Local.Exists(filePath) { + return state, nil + } + dataStr, err := io.Local.Read(filePath) if err != nil { - if os.IsNotExist(err) { - return state, nil - } - return nil, err + return nil, coreerr.E("LoadState", "read state file", err) } if err := json.Unmarshal([]byte(dataStr), state); err != nil { - return nil, err + return nil, coreerr.E("LoadState", "unmarshal state file", err) } return state, nil @@ -81,15 +83,18 @@ func (s *State) SaveState() error { // Ensure the directory exists dir := filepath.Dir(s.filePath) if err := io.Local.EnsureDir(dir); err != nil { - return err + return coreerr.E("State.SaveState", "ensure state directory", err) } data, err := json.MarshalIndent(s, "", " ") if err != nil { - return err + return coreerr.E("State.SaveState", "marshal state", err) } - return io.Local.Write(s.filePath, string(data)) + if err := io.Local.Write(s.filePath, string(data)); err != nil { + return coreerr.E("State.SaveState", "write state file", err) + } + return nil } // Add adds a container to the state and persists it. @@ -157,7 +162,7 @@ func (s *State) FilePath() string { func LogPath(id string) (string, error) { logsDir, err := DefaultLogsDir() if err != nil { - return "", err + return "", coreerr.E("LogPath", "get logs directory", err) } return filepath.Join(logsDir, id+".log"), nil } @@ -166,7 +171,10 @@ func LogPath(id string) (string, error) { func EnsureLogsDir() error { logsDir, err := DefaultLogsDir() if err != nil { - return err + return coreerr.E("EnsureLogsDir", "get logs directory", err) } - return io.Local.EnsureDir(logsDir) + if err := io.Local.EnsureDir(logsDir); err != nil { + return coreerr.E("EnsureLogsDir", "ensure logs directory", err) + } + return nil } diff --git a/templates.go b/templates.go index 4777ac2..2204c1c 100644 --- a/templates.go +++ b/templates.go @@ -107,7 +107,7 @@ func GetTemplate(name string) (string, error) { func ApplyTemplate(name string, vars map[string]string) (string, error) { content, err := GetTemplate(name) if err != nil { - return "", err + return "", coreerr.E("ApplyTemplate", "get template", err) } return ApplyVariables(content, vars) -- 2.45.3