refactor(container): align with core v0.8.0-alpha.1
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
37ccb287c0
commit
ba8b3df12c
29 changed files with 1119 additions and 635 deletions
|
|
@ -2,14 +2,13 @@ package vm
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
goio "io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/container"
|
||||
"dappco.re/go/core/container/internal/proc"
|
||||
"dappco.re/go/core/i18n"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -82,12 +81,12 @@ func runContainer(image, name string, detach bool, memory, cpus, sshPort int) er
|
|||
SSHPort: sshPort,
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s\n", dimStyle.Render(i18n.Label("image")), image)
|
||||
core.Print(nil, "%s %s", dimStyle.Render(i18n.Label("image")), image)
|
||||
if name != "" {
|
||||
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.name")), name)
|
||||
core.Print(nil, "%s %s", dimStyle.Render(i18n.T("cmd.vm.label.name")), name)
|
||||
}
|
||||
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.hypervisor")), manager.Hypervisor().Name())
|
||||
fmt.Println()
|
||||
core.Print(nil, "%s %s", dimStyle.Render(i18n.T("cmd.vm.label.hypervisor")), manager.Hypervisor().Name())
|
||||
core.Println()
|
||||
|
||||
ctx := context.Background()
|
||||
c, err := manager.Run(ctx, image, opts)
|
||||
|
|
@ -96,13 +95,14 @@ func runContainer(image, name string, detach bool, memory, cpus, sshPort int) er
|
|||
}
|
||||
|
||||
if detach {
|
||||
fmt.Printf("%s %s\n", successStyle.Render(i18n.Label("started")), c.ID)
|
||||
fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.vm.label.pid")), c.PID)
|
||||
fmt.Println()
|
||||
fmt.Println(i18n.T("cmd.vm.hint.view_logs", map[string]any{"ID": c.ID[:8]}))
|
||||
fmt.Println(i18n.T("cmd.vm.hint.stop", map[string]any{"ID": c.ID[:8]}))
|
||||
core.Print(nil, "%s %s", successStyle.Render(i18n.Label("started")), c.ID)
|
||||
core.Print(nil, "%s %d", dimStyle.Render(i18n.T("cmd.vm.label.pid")), c.PID)
|
||||
core.Println()
|
||||
core.Println(i18n.T("cmd.vm.hint.view_logs", map[string]any{"ID": c.ID[:8]}))
|
||||
core.Println(i18n.T("cmd.vm.hint.stop", map[string]any{"ID": c.ID[:8]}))
|
||||
} else {
|
||||
fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.container_stopped")), c.ID)
|
||||
core.Println()
|
||||
core.Print(nil, "%s %s", dimStyle.Render(i18n.T("cmd.vm.label.container_stopped")), c.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -151,16 +151,16 @@ func listContainers(all bool) error {
|
|||
|
||||
if len(containers) == 0 {
|
||||
if all {
|
||||
fmt.Println(i18n.T("cmd.vm.ps.no_containers"))
|
||||
core.Println(i18n.T("cmd.vm.ps.no_containers"))
|
||||
} else {
|
||||
fmt.Println(i18n.T("cmd.vm.ps.no_running"))
|
||||
core.Println(i18n.T("cmd.vm.ps.no_running"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintln(w, i18n.T("cmd.vm.ps.header"))
|
||||
_, _ = fmt.Fprintln(w, "--\t----\t-----\t------\t-------\t---")
|
||||
w := tabwriter.NewWriter(proc.Stdout, 0, 0, 2, ' ', 0)
|
||||
core.Print(w, "%s", i18n.T("cmd.vm.ps.header"))
|
||||
core.Print(w, "%s", "--\t----\t-----\t------\t-------\t---")
|
||||
|
||||
for _, c := range containers {
|
||||
// Shorten image path
|
||||
|
|
@ -183,7 +183,7 @@ func listContainers(all bool) error {
|
|||
status = errorStyle.Render(status)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%d\n",
|
||||
core.Print(w, "%s\t%s\t%s\t%s\t%s\t%d",
|
||||
c.ID[:8], c.Name, imageName, status, duration, c.PID)
|
||||
}
|
||||
|
||||
|
|
@ -193,15 +193,15 @@ func listContainers(all bool) error {
|
|||
|
||||
func formatDuration(d time.Duration) string {
|
||||
if d < time.Minute {
|
||||
return fmt.Sprintf("%ds", int(d.Seconds()))
|
||||
return core.Sprintf("%ds", int(d.Seconds()))
|
||||
}
|
||||
if d < time.Hour {
|
||||
return fmt.Sprintf("%dm", int(d.Minutes()))
|
||||
return core.Sprintf("%dm", int(d.Minutes()))
|
||||
}
|
||||
if d < 24*time.Hour {
|
||||
return fmt.Sprintf("%dh", int(d.Hours()))
|
||||
return core.Sprintf("%dh", int(d.Hours()))
|
||||
}
|
||||
return fmt.Sprintf("%dd", int(d.Hours()/24))
|
||||
return core.Sprintf("%dd", int(d.Hours()/24))
|
||||
}
|
||||
|
||||
// addVMStopCommand adds the 'stop' command under vm.
|
||||
|
|
@ -233,14 +233,14 @@ func stopContainer(id string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.stop.stopping")), fullID[:8])
|
||||
core.Print(nil, "%s %s", dimStyle.Render(i18n.T("cmd.vm.stop.stopping")), fullID[:8])
|
||||
|
||||
ctx := context.Background()
|
||||
if err := manager.Stop(ctx, fullID); err != nil {
|
||||
return coreerr.E("stopContainer", i18n.T("i18n.fail.stop", "container"), err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", successStyle.Render(i18n.T("common.status.stopped")))
|
||||
core.Print(nil, "%s", successStyle.Render(i18n.T("common.status.stopped")))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +254,7 @@ func resolveContainerID(manager *container.LinuxKitManager, partialID string) (s
|
|||
|
||||
var matches []*container.Container
|
||||
for _, c := range containers {
|
||||
if strings.HasPrefix(c.ID, partialID) || strings.HasPrefix(c.Name, partialID) {
|
||||
if core.HasPrefix(c.ID, partialID) || core.HasPrefix(c.Name, partialID) {
|
||||
matches = append(matches, c)
|
||||
}
|
||||
}
|
||||
|
|
@ -308,7 +308,7 @@ func viewLogs(id string, follow bool) error {
|
|||
}
|
||||
defer func() { _ = reader.Close() }()
|
||||
|
||||
_, err = goio.Copy(os.Stdout, reader)
|
||||
_, err = goio.Copy(proc.Stdout, reader)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,12 @@ package vm
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/container"
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
"dappco.re/go/core/container/internal/proc"
|
||||
"dappco.re/go/core/i18n"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
|
@ -72,29 +70,30 @@ func listTemplates() error {
|
|||
templates := container.ListTemplates()
|
||||
|
||||
if len(templates) == 0 {
|
||||
fmt.Println(i18n.T("cmd.vm.templates.no_templates"))
|
||||
core.Println(i18n.T("cmd.vm.templates.no_templates"))
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n\n", repoNameStyle.Render(i18n.T("cmd.vm.templates.title")))
|
||||
core.Print(nil, "%s", repoNameStyle.Render(i18n.T("cmd.vm.templates.title")))
|
||||
core.Println()
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintln(w, i18n.T("cmd.vm.templates.header"))
|
||||
_, _ = fmt.Fprintln(w, "----\t-----------")
|
||||
w := tabwriter.NewWriter(proc.Stdout, 0, 0, 2, ' ', 0)
|
||||
core.Print(w, "%s", i18n.T("cmd.vm.templates.header"))
|
||||
core.Print(w, "%s", "----\t-----------")
|
||||
|
||||
for _, tmpl := range templates {
|
||||
desc := tmpl.Description
|
||||
if len(desc) > 60 {
|
||||
desc = desc[:57] + "..."
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", repoNameStyle.Render(tmpl.Name), desc)
|
||||
core.Print(w, "%s\t%s", repoNameStyle.Render(tmpl.Name), desc)
|
||||
}
|
||||
_ = w.Flush()
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.show"), dimStyle.Render("core vm templates show <name>"))
|
||||
fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.vars"), dimStyle.Render("core vm templates vars <name>"))
|
||||
fmt.Printf("%s %s\n", i18n.T("cmd.vm.templates.hint.run"), dimStyle.Render("core vm run --template <name> --var SSH_KEY=\"...\""))
|
||||
core.Println()
|
||||
core.Print(nil, "%s %s", i18n.T("cmd.vm.templates.hint.show"), dimStyle.Render("core vm templates show <name>"))
|
||||
core.Print(nil, "%s %s", i18n.T("cmd.vm.templates.hint.vars"), dimStyle.Render("core vm templates vars <name>"))
|
||||
core.Print(nil, "%s %s", i18n.T("cmd.vm.templates.hint.run"), dimStyle.Render("core vm run --template <name> --var SSH_KEY=\"...\""))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -105,8 +104,9 @@ func showTemplate(name string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("common.label.template")), repoNameStyle.Render(name))
|
||||
fmt.Println(content)
|
||||
core.Print(nil, "%s %s", dimStyle.Render(i18n.T("common.label.template")), repoNameStyle.Render(name))
|
||||
core.Println()
|
||||
core.Println(content)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -119,28 +119,29 @@ func showTemplateVars(name string) error {
|
|||
|
||||
required, optional := container.ExtractVariables(content)
|
||||
|
||||
fmt.Printf("%s %s\n\n", dimStyle.Render(i18n.T("common.label.template")), repoNameStyle.Render(name))
|
||||
core.Print(nil, "%s %s", dimStyle.Render(i18n.T("common.label.template")), repoNameStyle.Render(name))
|
||||
core.Println()
|
||||
|
||||
if len(required) > 0 {
|
||||
fmt.Printf("%s\n", errorStyle.Render(i18n.T("cmd.vm.templates.vars.required")))
|
||||
core.Print(nil, "%s", errorStyle.Render(i18n.T("cmd.vm.templates.vars.required")))
|
||||
for _, v := range required {
|
||||
fmt.Printf(" %s\n", varStyle.Render("${"+v+"}"))
|
||||
core.Print(nil, " %s", varStyle.Render("${"+v+"}"))
|
||||
}
|
||||
fmt.Println()
|
||||
core.Println()
|
||||
}
|
||||
|
||||
if len(optional) > 0 {
|
||||
fmt.Printf("%s\n", successStyle.Render(i18n.T("cmd.vm.templates.vars.optional")))
|
||||
core.Print(nil, "%s", successStyle.Render(i18n.T("cmd.vm.templates.vars.optional")))
|
||||
for v, def := range optional {
|
||||
fmt.Printf(" %s = %s\n",
|
||||
core.Print(nil, " %s = %s",
|
||||
varStyle.Render("${"+v+"}"),
|
||||
defaultStyle.Render(def))
|
||||
}
|
||||
fmt.Println()
|
||||
core.Println()
|
||||
}
|
||||
|
||||
if len(required) == 0 && len(optional) == 0 {
|
||||
fmt.Println(i18n.T("cmd.vm.templates.vars.none"))
|
||||
core.Println(i18n.T("cmd.vm.templates.vars.none"))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -155,23 +156,23 @@ func RunFromTemplate(templateName string, vars map[string]string, runOpts contai
|
|||
}
|
||||
|
||||
// Create a temporary directory for the build
|
||||
tmpDir, err := os.MkdirTemp("", "core-linuxkit-*")
|
||||
tmpDir, err := coreutil.MkdirTemp("core-linuxkit-")
|
||||
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) }()
|
||||
defer func() { _ = io.Local.DeleteAll(tmpDir) }()
|
||||
|
||||
// Write the YAML file
|
||||
yamlPath := filepath.Join(tmpDir, templateName+".yml")
|
||||
yamlPath := coreutil.JoinPath(tmpDir, core.Concat(templateName, ".yml"))
|
||||
if err := io.Local.Write(yamlPath, content); err != nil {
|
||||
return coreerr.E("RunFromTemplate", i18n.T("common.error.failed", map[string]any{"Action": "write template"}), err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.template")), repoNameStyle.Render(templateName))
|
||||
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.building")), yamlPath)
|
||||
core.Print(nil, "%s %s", dimStyle.Render(i18n.T("common.label.template")), repoNameStyle.Render(templateName))
|
||||
core.Print(nil, "%s %s", dimStyle.Render(i18n.T("cmd.vm.label.building")), yamlPath)
|
||||
|
||||
// Build the image using linuxkit
|
||||
outputPath := filepath.Join(tmpDir, templateName)
|
||||
outputPath := coreutil.JoinPath(tmpDir, templateName)
|
||||
if err := buildLinuxKitImage(yamlPath, outputPath); err != nil {
|
||||
return coreerr.E("RunFromTemplate", i18n.T("common.error.failed", map[string]any{"Action": "build image"}), err)
|
||||
}
|
||||
|
|
@ -182,8 +183,8 @@ func RunFromTemplate(templateName string, vars map[string]string, runOpts contai
|
|||
return coreerr.E("RunFromTemplate", i18n.T("cmd.vm.error.no_image_found"), nil)
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("common.label.image")), imagePath)
|
||||
fmt.Println()
|
||||
core.Print(nil, "%s %s", dimStyle.Render(i18n.T("common.label.image")), imagePath)
|
||||
core.Println()
|
||||
|
||||
// Run the image
|
||||
manager, err := container.NewLinuxKitManager(io.Local)
|
||||
|
|
@ -191,8 +192,8 @@ func RunFromTemplate(templateName string, vars map[string]string, runOpts contai
|
|||
return coreerr.E("RunFromTemplate", i18n.T("common.error.failed", map[string]any{"Action": "initialize container manager"}), err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.hypervisor")), manager.Hypervisor().Name())
|
||||
fmt.Println()
|
||||
core.Print(nil, "%s %s", dimStyle.Render(i18n.T("cmd.vm.label.hypervisor")), manager.Hypervisor().Name())
|
||||
core.Println()
|
||||
|
||||
ctx := context.Background()
|
||||
c, err := manager.Run(ctx, imagePath, runOpts)
|
||||
|
|
@ -201,13 +202,14 @@ func RunFromTemplate(templateName string, vars map[string]string, runOpts contai
|
|||
}
|
||||
|
||||
if runOpts.Detach {
|
||||
fmt.Printf("%s %s\n", successStyle.Render(i18n.T("common.label.started")), c.ID)
|
||||
fmt.Printf("%s %d\n", dimStyle.Render(i18n.T("cmd.vm.label.pid")), c.PID)
|
||||
fmt.Println()
|
||||
fmt.Println(i18n.T("cmd.vm.hint.view_logs", map[string]any{"ID": c.ID[:8]}))
|
||||
fmt.Println(i18n.T("cmd.vm.hint.stop", map[string]any{"ID": c.ID[:8]}))
|
||||
core.Print(nil, "%s %s", successStyle.Render(i18n.T("common.label.started")), c.ID)
|
||||
core.Print(nil, "%s %d", dimStyle.Render(i18n.T("cmd.vm.label.pid")), c.PID)
|
||||
core.Println()
|
||||
core.Println(i18n.T("cmd.vm.hint.view_logs", map[string]any{"ID": c.ID[:8]}))
|
||||
core.Println(i18n.T("cmd.vm.hint.stop", map[string]any{"ID": c.ID[:8]}))
|
||||
} else {
|
||||
fmt.Printf("\n%s %s\n", dimStyle.Render(i18n.T("cmd.vm.label.container_stopped")), c.ID)
|
||||
core.Println()
|
||||
core.Print(nil, "%s %s", dimStyle.Render(i18n.T("cmd.vm.label.container_stopped")), c.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -223,13 +225,13 @@ func buildLinuxKitImage(yamlPath, outputPath string) error {
|
|||
|
||||
// Build the image
|
||||
// linuxkit build --format iso-bios --name <output> <yaml>
|
||||
cmd := exec.Command(lkPath, "build",
|
||||
cmd := proc.NewCommand(lkPath, "build",
|
||||
"--format", "iso-bios",
|
||||
"--name", outputPath,
|
||||
yamlPath)
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = proc.Stdout
|
||||
cmd.Stderr = proc.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
|
@ -240,27 +242,27 @@ func findBuiltImage(basePath string) string {
|
|||
extensions := []string{".iso", "-bios.iso", ".qcow2", ".raw", ".vmdk"}
|
||||
|
||||
for _, ext := range extensions {
|
||||
path := basePath + ext
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
path := core.Concat(basePath, ext)
|
||||
if io.Local.IsFile(path) {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// Check directory for any image file
|
||||
dir := filepath.Dir(basePath)
|
||||
base := filepath.Base(basePath)
|
||||
dir := core.PathDir(basePath)
|
||||
base := core.PathBase(basePath)
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
entries, err := io.Local.List(dir)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if strings.HasPrefix(name, base) {
|
||||
if core.HasPrefix(name, base) {
|
||||
for _, ext := range []string{".iso", ".qcow2", ".raw", ".vmdk"} {
|
||||
if strings.HasSuffix(name, ext) {
|
||||
return filepath.Join(dir, name)
|
||||
if core.HasSuffix(name, ext) {
|
||||
return coreutil.JoinPath(dir, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -272,7 +274,7 @@ func findBuiltImage(basePath string) string {
|
|||
// lookupLinuxKit finds the linuxkit binary.
|
||||
func lookupLinuxKit() (string, error) {
|
||||
// Check PATH first
|
||||
if path, err := exec.LookPath("linuxkit"); err == nil {
|
||||
if path, err := proc.LookPath("linuxkit"); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
|
|
@ -283,7 +285,7 @@ func lookupLinuxKit() (string, error) {
|
|||
}
|
||||
|
||||
for _, p := range paths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
if io.Local.Exists(p) {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -297,15 +299,28 @@ func ParseVarFlags(varFlags []string) map[string]string {
|
|||
vars := make(map[string]string)
|
||||
|
||||
for _, v := range varFlags {
|
||||
parts := strings.SplitN(v, "=", 2)
|
||||
parts := core.SplitN(v, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
key := core.Trim(parts[0])
|
||||
value := core.Trim(parts[1])
|
||||
// Remove surrounding quotes if present
|
||||
value = strings.Trim(value, "\"'")
|
||||
value = stripWrappingQuotes(value)
|
||||
vars[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return vars
|
||||
}
|
||||
|
||||
func stripWrappingQuotes(value string) string {
|
||||
if len(value) < 2 {
|
||||
return value
|
||||
}
|
||||
if core.HasPrefix(value, "\"") && core.HasSuffix(value, "\"") {
|
||||
return core.TrimSuffix(core.TrimPrefix(value, "\""), "\"")
|
||||
}
|
||||
if core.HasPrefix(value, "'") && core.HasSuffix(value, "'") {
|
||||
return core.TrimSuffix(core.TrimPrefix(value, "'"), "'")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,13 @@ package devenv
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
"dappco.re/go/core/container/internal/proc"
|
||||
)
|
||||
|
||||
// ClaudeOptions configures the Claude sandbox session.
|
||||
|
|
@ -27,7 +26,7 @@ func (d *DevOps) Claude(ctx context.Context, projectDir string, opts ClaudeOptio
|
|||
return err
|
||||
}
|
||||
if !running {
|
||||
fmt.Println("Dev environment not running, booting...")
|
||||
core.Println("Dev environment not running, booting...")
|
||||
if err := d.Boot(ctx, DefaultBootOptions()); err != nil {
|
||||
return coreerr.E("DevOps.Claude", "failed to boot", err)
|
||||
}
|
||||
|
|
@ -50,20 +49,22 @@ func (d *DevOps) Claude(ctx context.Context, projectDir string, opts ClaudeOptio
|
|||
for _, auth := range authTypes {
|
||||
switch auth {
|
||||
case "anthropic":
|
||||
if key := os.Getenv("ANTHROPIC_API_KEY"); key != "" {
|
||||
envVars = append(envVars, "ANTHROPIC_API_KEY="+key)
|
||||
if key := core.Env("ANTHROPIC_API_KEY"); key != "" {
|
||||
envVars = append(envVars, core.Concat("ANTHROPIC_API_KEY=", key))
|
||||
}
|
||||
case "git":
|
||||
// Forward git config
|
||||
name, _ := exec.Command("git", "config", "user.name").Output()
|
||||
email, _ := exec.Command("git", "config", "user.email").Output()
|
||||
name, _ := proc.NewCommand("git", "config", "user.name").Output()
|
||||
email, _ := proc.NewCommand("git", "config", "user.email").Output()
|
||||
if len(name) > 0 {
|
||||
envVars = append(envVars, "GIT_AUTHOR_NAME="+strings.TrimSpace(string(name)))
|
||||
envVars = append(envVars, "GIT_COMMITTER_NAME="+strings.TrimSpace(string(name)))
|
||||
trimmed := core.Trim(string(name))
|
||||
envVars = append(envVars, core.Concat("GIT_AUTHOR_NAME=", trimmed))
|
||||
envVars = append(envVars, core.Concat("GIT_COMMITTER_NAME=", trimmed))
|
||||
}
|
||||
if len(email) > 0 {
|
||||
envVars = append(envVars, "GIT_AUTHOR_EMAIL="+strings.TrimSpace(string(email)))
|
||||
envVars = append(envVars, "GIT_COMMITTER_EMAIL="+strings.TrimSpace(string(email)))
|
||||
trimmed := core.Trim(string(email))
|
||||
envVars = append(envVars, core.Concat("GIT_AUTHOR_EMAIL=", trimmed))
|
||||
envVars = append(envVars, core.Concat("GIT_COMMITTER_EMAIL=", trimmed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -75,7 +76,7 @@ func (d *DevOps) Claude(ctx context.Context, projectDir string, opts ClaudeOptio
|
|||
"-o", "UserKnownHostsFile=~/.core/known_hosts",
|
||||
"-o", "LogLevel=ERROR",
|
||||
"-A", // SSH agent forwarding
|
||||
"-p", fmt.Sprintf("%d", DefaultSSHPort),
|
||||
"-p", core.Sprintf("%d", DefaultSSHPort),
|
||||
}
|
||||
|
||||
args = append(args, "root@localhost")
|
||||
|
|
@ -88,23 +89,20 @@ func (d *DevOps) Claude(ctx context.Context, projectDir string, opts ClaudeOptio
|
|||
args = append(args, claudeCmd)
|
||||
|
||||
// Set environment for SSH
|
||||
cmd := exec.CommandContext(ctx, "ssh", args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd := proc.NewCommandContext(ctx, "ssh", args...)
|
||||
cmd.Stdin = proc.Stdin
|
||||
cmd.Stdout = proc.Stdout
|
||||
cmd.Stderr = proc.Stderr
|
||||
|
||||
// Pass environment variables through SSH
|
||||
for _, env := range envVars {
|
||||
parts := strings.SplitN(env, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
cmd.Env = append(os.Environ(), env)
|
||||
}
|
||||
if len(envVars) > 0 {
|
||||
cmd.Env = append(proc.Environ(), envVars...)
|
||||
}
|
||||
|
||||
fmt.Println("Starting Claude in sandboxed environment...")
|
||||
fmt.Println("Project mounted at /app")
|
||||
fmt.Println("Auth forwarded: SSH agent" + formatAuthList(opts))
|
||||
fmt.Println()
|
||||
core.Println("Starting Claude in sandboxed environment...")
|
||||
core.Println("Project mounted at /app")
|
||||
core.Println(core.Concat("Auth forwarded: SSH agent", formatAuthList(opts)))
|
||||
core.Println()
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
|
@ -116,27 +114,27 @@ func formatAuthList(opts ClaudeOptions) string {
|
|||
if len(opts.Auth) == 0 {
|
||||
return ", gh, anthropic, git"
|
||||
}
|
||||
return ", " + strings.Join(opts.Auth, ", ")
|
||||
return core.Concat(", ", core.Join(", ", opts.Auth...))
|
||||
}
|
||||
|
||||
// CopyGHAuth copies GitHub CLI auth to the VM.
|
||||
func (d *DevOps) CopyGHAuth(ctx context.Context) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
home := coreutil.HomeDir()
|
||||
if home == "" {
|
||||
return coreerr.E("DevOps.CopyGHAuth", "home directory not available", nil)
|
||||
}
|
||||
|
||||
ghConfigDir := filepath.Join(home, ".config", "gh")
|
||||
ghConfigDir := coreutil.JoinPath(home, ".config", "gh")
|
||||
if !io.Local.IsDir(ghConfigDir) {
|
||||
return nil // No gh config to copy
|
||||
}
|
||||
|
||||
// Use scp to copy gh config
|
||||
cmd := exec.CommandContext(ctx, "scp",
|
||||
cmd := proc.NewCommandContext(ctx, "scp",
|
||||
"-o", "StrictHostKeyChecking=yes",
|
||||
"-o", "UserKnownHostsFile=~/.core/known_hosts",
|
||||
"-o", "LogLevel=ERROR",
|
||||
"-P", fmt.Sprintf("%d", DefaultSSHPort),
|
||||
"-P", core.Sprintf("%d", DefaultSSHPort),
|
||||
"-r", ghConfigDir,
|
||||
"root@localhost:/root/.config/",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package devenv
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"dappco.re/go/core/io"
|
||||
"forge.lthn.ai/core/config"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
)
|
||||
|
||||
// Config holds global devops configuration from ~/.core/config.yaml.
|
||||
|
|
@ -55,11 +56,11 @@ func DefaultConfig() *Config {
|
|||
|
||||
// ConfigPath returns the path to the config file.
|
||||
func ConfigPath() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
home := coreutil.HomeDir()
|
||||
if home == "" {
|
||||
return "", core.E("ConfigPath", "home directory not available", nil)
|
||||
}
|
||||
return filepath.Join(home, ".core", "config.yaml"), nil
|
||||
return coreutil.JoinPath(home, ".core", "config.yaml"), nil
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from ~/.core/config.yaml using the provided medium.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package devenv
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -27,9 +27,7 @@ func TestLoadConfig_Good(t *testing.T) {
|
|||
t.Run("returns default if not exists", func(t *testing.T) {
|
||||
// Mock HOME to a temp dir
|
||||
tempHome := t.TempDir()
|
||||
origHome := os.Getenv("HOME")
|
||||
t.Setenv("HOME", tempHome)
|
||||
defer func() { _ = os.Setenv("HOME", origHome) }()
|
||||
|
||||
cfg, err := LoadConfig(io.Local)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -40,8 +38,8 @@ func TestLoadConfig_Good(t *testing.T) {
|
|||
tempHome := t.TempDir()
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
coreDir := filepath.Join(tempHome, ".core")
|
||||
err := os.MkdirAll(coreDir, 0755)
|
||||
coreDir := coreutil.JoinPath(tempHome, ".core")
|
||||
err := io.Local.EnsureDir(coreDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
configData := `
|
||||
|
|
@ -51,7 +49,7 @@ images:
|
|||
cdn:
|
||||
url: https://cdn.example.com
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(coreDir, "config.yaml"), []byte(configData), 0644)
|
||||
err = io.Local.Write(coreutil.JoinPath(coreDir, "config.yaml"), configData)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg, err := LoadConfig(io.Local)
|
||||
|
|
@ -67,11 +65,11 @@ func TestLoadConfig_Bad(t *testing.T) {
|
|||
tempHome := t.TempDir()
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
coreDir := filepath.Join(tempHome, ".core")
|
||||
err := os.MkdirAll(coreDir, 0755)
|
||||
coreDir := coreutil.JoinPath(tempHome, ".core")
|
||||
err := io.Local.EnsureDir(coreDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepath.Join(coreDir, "config.yaml"), []byte("invalid: yaml: :"), 0644)
|
||||
err = io.Local.Write(coreutil.JoinPath(coreDir, "config.yaml"), "invalid: yaml: :")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = LoadConfig(io.Local)
|
||||
|
|
@ -115,8 +113,8 @@ func TestLoadConfig_Good_PartialConfig(t *testing.T) {
|
|||
tempHome := t.TempDir()
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
coreDir := filepath.Join(tempHome, ".core")
|
||||
err := os.MkdirAll(coreDir, 0755)
|
||||
coreDir := coreutil.JoinPath(tempHome, ".core")
|
||||
err := io.Local.EnsureDir(coreDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Config only specifies source, should merge with defaults
|
||||
|
|
@ -125,7 +123,7 @@ version: 1
|
|||
images:
|
||||
source: github
|
||||
`
|
||||
err = os.WriteFile(filepath.Join(coreDir, "config.yaml"), []byte(configData), 0644)
|
||||
err = io.Local.Write(coreutil.JoinPath(coreDir, "config.yaml"), configData)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg, err := LoadConfig(io.Local)
|
||||
|
|
@ -191,11 +189,11 @@ images:
|
|||
tempHome := t.TempDir()
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
coreDir := filepath.Join(tempHome, ".core")
|
||||
err := os.MkdirAll(coreDir, 0755)
|
||||
coreDir := coreutil.JoinPath(tempHome, ".core")
|
||||
err := io.Local.EnsureDir(coreDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepath.Join(coreDir, "config.yaml"), []byte(tt.config), 0644)
|
||||
err = io.Local.Write(coreutil.JoinPath(coreDir, "config.yaml"), tt.config)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg, err := LoadConfig(io.Local)
|
||||
|
|
@ -232,24 +230,24 @@ func TestCDNConfig_Struct(t *testing.T) {
|
|||
func TestLoadConfig_Bad_UnreadableFile(t *testing.T) {
|
||||
// This test is platform-specific and may not work on all systems
|
||||
// Skip if we can't test file permissions properly
|
||||
if os.Getuid() == 0 {
|
||||
if syscall.Getuid() == 0 {
|
||||
t.Skip("Skipping permission test when running as root")
|
||||
}
|
||||
|
||||
tempHome := t.TempDir()
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
coreDir := filepath.Join(tempHome, ".core")
|
||||
err := os.MkdirAll(coreDir, 0755)
|
||||
coreDir := coreutil.JoinPath(tempHome, ".core")
|
||||
err := io.Local.EnsureDir(coreDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
configPath := filepath.Join(coreDir, "config.yaml")
|
||||
err = os.WriteFile(configPath, []byte("version: 1"), 0000)
|
||||
configPath := coreutil.JoinPath(coreDir, "config.yaml")
|
||||
err = io.Local.WriteMode(configPath, "version: 1", 0000)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = LoadConfig(io.Local)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Restore permissions so cleanup works
|
||||
_ = os.Chmod(configPath, 0644)
|
||||
_ = syscall.Chmod(configPath, 0644)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@ package devenv
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/container"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -54,19 +54,19 @@ func New(m io.Medium) (*DevOps, error) {
|
|||
|
||||
// ImageName returns the platform-specific image name.
|
||||
func ImageName() string {
|
||||
return fmt.Sprintf("core-devops-%s-%s.qcow2", runtime.GOOS, runtime.GOARCH)
|
||||
return core.Sprintf("core-devops-%s-%s.qcow2", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
// ImagesDir returns the path to the images directory.
|
||||
func ImagesDir() (string, error) {
|
||||
if dir := os.Getenv("CORE_IMAGES_DIR"); dir != "" {
|
||||
if dir := core.Env("CORE_IMAGES_DIR"); dir != "" {
|
||||
return dir, nil
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
home := coreutil.HomeDir()
|
||||
if home == "" {
|
||||
return "", core.E("ImagesDir", "home directory not available", nil)
|
||||
}
|
||||
return filepath.Join(home, ".core", "images"), nil
|
||||
return coreutil.JoinPath(home, ".core", "images"), nil
|
||||
}
|
||||
|
||||
// ImagePath returns the full path to the platform-specific image.
|
||||
|
|
@ -75,7 +75,7 @@ func ImagePath() (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, ImageName()), nil
|
||||
return coreutil.JoinPath(dir, ImageName()), nil
|
||||
}
|
||||
|
||||
// IsInstalled checks if the dev image is installed.
|
||||
|
|
|
|||
|
|
@ -2,19 +2,28 @@ package devenv
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/container"
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
"dappco.re/go/core/container/internal/proc"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newManagedTempDir(t *testing.T, prefix string) string {
|
||||
t.Helper()
|
||||
dir, err := coreutil.MkdirTemp(prefix)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = io.Local.DeleteAll(dir) })
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestImageName(t *testing.T) {
|
||||
name := ImageName()
|
||||
assert.Contains(t, name, "core-devops-")
|
||||
|
|
@ -25,10 +34,7 @@ func TestImageName(t *testing.T) {
|
|||
|
||||
func TestImagesDir(t *testing.T) {
|
||||
t.Run("default directory", func(t *testing.T) {
|
||||
// Unset env if it exists
|
||||
orig := os.Getenv("CORE_IMAGES_DIR")
|
||||
_ = os.Unsetenv("CORE_IMAGES_DIR")
|
||||
defer func() { _ = os.Setenv("CORE_IMAGES_DIR", orig) }()
|
||||
t.Setenv("CORE_IMAGES_DIR", "")
|
||||
|
||||
dir, err := ImagesDir()
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -51,7 +57,7 @@ func TestImagePath(t *testing.T) {
|
|||
|
||||
path, err := ImagePath()
|
||||
assert.NoError(t, err)
|
||||
expected := filepath.Join(customDir, ImageName())
|
||||
expected := coreutil.JoinPath(customDir, ImageName())
|
||||
assert.Equal(t, expected, path)
|
||||
}
|
||||
|
||||
|
|
@ -81,8 +87,8 @@ func TestIsInstalled_Good(t *testing.T) {
|
|||
t.Setenv("CORE_IMAGES_DIR", tempDir)
|
||||
|
||||
// Create the image file
|
||||
imagePath := filepath.Join(tempDir, ImageName())
|
||||
err := os.WriteFile(imagePath, []byte("fake image data"), 0644)
|
||||
imagePath := coreutil.JoinPath(tempDir, ImageName())
|
||||
err := io.Local.Write(imagePath, "fake image data")
|
||||
require.NoError(t, err)
|
||||
|
||||
d := &DevOps{medium: io.Local}
|
||||
|
|
@ -94,8 +100,8 @@ type mockHypervisor struct{}
|
|||
|
||||
func (m *mockHypervisor) Name() string { return "mock" }
|
||||
func (m *mockHypervisor) Available() bool { return true }
|
||||
func (m *mockHypervisor) BuildCommand(ctx context.Context, image string, opts *container.HypervisorOptions) (*exec.Cmd, error) {
|
||||
return exec.Command("true"), nil
|
||||
func (m *mockHypervisor) BuildCommand(ctx context.Context, image string, opts *container.HypervisorOptions) (*proc.Command, error) {
|
||||
return proc.NewCommand("true"), nil
|
||||
}
|
||||
|
||||
func TestDevOps_Status_Good(t *testing.T) {
|
||||
|
|
@ -107,7 +113,7 @@ func TestDevOps_Status_Good(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Setup mock container manager
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -122,7 +128,7 @@ func TestDevOps_Status_Good(t *testing.T) {
|
|||
ID: "test-id",
|
||||
Name: "core-dev",
|
||||
Status: container.StatusRunning,
|
||||
PID: os.Getpid(), // Use our own PID so isProcessRunning returns true
|
||||
PID: syscall.Getpid(), // Use our own PID so isProcessRunning returns true
|
||||
StartedAt: time.Now().Add(-time.Hour),
|
||||
Memory: 2048,
|
||||
CPUs: 4,
|
||||
|
|
@ -147,7 +153,7 @@ func TestDevOps_Status_Good_NotInstalled(t *testing.T) {
|
|||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -170,15 +176,15 @@ func TestDevOps_Status_Good_NoContainer(t *testing.T) {
|
|||
t.Setenv("CORE_IMAGES_DIR", tempDir)
|
||||
|
||||
// Create fake image to mark as installed
|
||||
imagePath := filepath.Join(tempDir, ImageName())
|
||||
err := os.WriteFile(imagePath, []byte("fake"), 0644)
|
||||
imagePath := coreutil.JoinPath(tempDir, ImageName())
|
||||
err := io.Local.Write(imagePath, "fake")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := DefaultConfig()
|
||||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -204,7 +210,7 @@ func TestDevOps_IsRunning_Good(t *testing.T) {
|
|||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -218,7 +224,7 @@ func TestDevOps_IsRunning_Good(t *testing.T) {
|
|||
ID: "test-id",
|
||||
Name: "core-dev",
|
||||
Status: container.StatusRunning,
|
||||
PID: os.Getpid(),
|
||||
PID: syscall.Getpid(),
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
err = state.Add(c)
|
||||
|
|
@ -237,7 +243,7 @@ func TestDevOps_IsRunning_Bad_NotRunning(t *testing.T) {
|
|||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -260,7 +266,7 @@ func TestDevOps_IsRunning_Bad_ContainerStopped(t *testing.T) {
|
|||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -293,7 +299,7 @@ func TestDevOps_findContainer_Good(t *testing.T) {
|
|||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -307,7 +313,7 @@ func TestDevOps_findContainer_Good(t *testing.T) {
|
|||
ID: "test-id",
|
||||
Name: "my-container",
|
||||
Status: container.StatusRunning,
|
||||
PID: os.Getpid(),
|
||||
PID: syscall.Getpid(),
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
err = state.Add(c)
|
||||
|
|
@ -328,7 +334,7 @@ func TestDevOps_findContainer_Bad_NotFound(t *testing.T) {
|
|||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -351,7 +357,7 @@ func TestDevOps_Stop_Bad_NotFound(t *testing.T) {
|
|||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -408,7 +414,7 @@ func TestDevOps_Boot_Bad_NotInstalled(t *testing.T) {
|
|||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -428,15 +434,15 @@ func TestDevOps_Boot_Bad_AlreadyRunning(t *testing.T) {
|
|||
t.Setenv("CORE_IMAGES_DIR", tempDir)
|
||||
|
||||
// Create fake image
|
||||
imagePath := filepath.Join(tempDir, ImageName())
|
||||
err := os.WriteFile(imagePath, []byte("fake"), 0644)
|
||||
imagePath := coreutil.JoinPath(tempDir, ImageName())
|
||||
err := io.Local.Write(imagePath, "fake")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := DefaultConfig()
|
||||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -451,7 +457,7 @@ func TestDevOps_Boot_Bad_AlreadyRunning(t *testing.T) {
|
|||
ID: "test-id",
|
||||
Name: "core-dev",
|
||||
Status: container.StatusRunning,
|
||||
PID: os.Getpid(),
|
||||
PID: syscall.Getpid(),
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
err = state.Add(c)
|
||||
|
|
@ -467,8 +473,8 @@ func TestDevOps_Status_Good_WithImageVersion(t *testing.T) {
|
|||
t.Setenv("CORE_IMAGES_DIR", tempDir)
|
||||
|
||||
// Create fake image
|
||||
imagePath := filepath.Join(tempDir, ImageName())
|
||||
err := os.WriteFile(imagePath, []byte("fake"), 0644)
|
||||
imagePath := coreutil.JoinPath(tempDir, ImageName())
|
||||
err := io.Local.Write(imagePath, "fake")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := DefaultConfig()
|
||||
|
|
@ -481,7 +487,7 @@ func TestDevOps_Status_Good_WithImageVersion(t *testing.T) {
|
|||
Source: "test",
|
||||
}
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -506,7 +512,7 @@ func TestDevOps_findContainer_Good_MultipleContainers(t *testing.T) {
|
|||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -521,14 +527,14 @@ func TestDevOps_findContainer_Good_MultipleContainers(t *testing.T) {
|
|||
ID: "id-1",
|
||||
Name: "container-1",
|
||||
Status: container.StatusRunning,
|
||||
PID: os.Getpid(),
|
||||
PID: syscall.Getpid(),
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
c2 := &container.Container{
|
||||
ID: "id-2",
|
||||
Name: "container-2",
|
||||
Status: container.StatusRunning,
|
||||
PID: os.Getpid(),
|
||||
PID: syscall.Getpid(),
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
err = state.Add(c1)
|
||||
|
|
@ -551,7 +557,7 @@ func TestDevOps_Status_Good_ContainerWithUptime(t *testing.T) {
|
|||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -566,7 +572,7 @@ func TestDevOps_Status_Good_ContainerWithUptime(t *testing.T) {
|
|||
ID: "test-id",
|
||||
Name: "core-dev",
|
||||
Status: container.StatusRunning,
|
||||
PID: os.Getpid(),
|
||||
PID: syscall.Getpid(),
|
||||
StartedAt: startTime,
|
||||
Memory: 4096,
|
||||
CPUs: 2,
|
||||
|
|
@ -588,7 +594,7 @@ func TestDevOps_IsRunning_Bad_DifferentContainerName(t *testing.T) {
|
|||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -603,7 +609,7 @@ func TestDevOps_IsRunning_Bad_DifferentContainerName(t *testing.T) {
|
|||
ID: "test-id",
|
||||
Name: "other-container",
|
||||
Status: container.StatusRunning,
|
||||
PID: os.Getpid(),
|
||||
PID: syscall.Getpid(),
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
err = state.Add(c)
|
||||
|
|
@ -617,21 +623,19 @@ func TestDevOps_IsRunning_Bad_DifferentContainerName(t *testing.T) {
|
|||
|
||||
func TestDevOps_Boot_Good_FreshFlag(t *testing.T) {
|
||||
t.Setenv("CORE_SKIP_SSH_SCAN", "true")
|
||||
tempDir, err := os.MkdirTemp("", "devops-test-*")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = os.RemoveAll(tempDir) })
|
||||
tempDir := newManagedTempDir(t, "devops-test-")
|
||||
t.Setenv("CORE_IMAGES_DIR", tempDir)
|
||||
|
||||
// Create fake image
|
||||
imagePath := filepath.Join(tempDir, ImageName())
|
||||
err = os.WriteFile(imagePath, []byte("fake"), 0644)
|
||||
imagePath := coreutil.JoinPath(tempDir, ImageName())
|
||||
err := io.Local.Write(imagePath, "fake")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := DefaultConfig()
|
||||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -673,7 +677,7 @@ func TestDevOps_Stop_Bad_ContainerNotRunning(t *testing.T) {
|
|||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -702,21 +706,19 @@ func TestDevOps_Stop_Bad_ContainerNotRunning(t *testing.T) {
|
|||
|
||||
func TestDevOps_Boot_Good_FreshWithNoExisting(t *testing.T) {
|
||||
t.Setenv("CORE_SKIP_SSH_SCAN", "true")
|
||||
tempDir, err := os.MkdirTemp("", "devops-boot-fresh-*")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = os.RemoveAll(tempDir) })
|
||||
tempDir := newManagedTempDir(t, "devops-boot-fresh-")
|
||||
t.Setenv("CORE_IMAGES_DIR", tempDir)
|
||||
|
||||
// Create fake image
|
||||
imagePath := filepath.Join(tempDir, ImageName())
|
||||
err = os.WriteFile(imagePath, []byte("fake"), 0644)
|
||||
imagePath := coreutil.JoinPath(tempDir, ImageName())
|
||||
err := io.Local.Write(imagePath, "fake")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := DefaultConfig()
|
||||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
@ -744,7 +746,7 @@ func TestImageName_Format(t *testing.T) {
|
|||
assert.Contains(t, name, "core-devops-")
|
||||
assert.Contains(t, name, runtime.GOOS)
|
||||
assert.Contains(t, name, runtime.GOARCH)
|
||||
assert.True(t, filepath.Ext(name) == ".qcow2")
|
||||
assert.True(t, core.PathExt(name) == ".qcow2")
|
||||
}
|
||||
|
||||
func TestDevOps_Install_Delegates(t *testing.T) {
|
||||
|
|
@ -785,21 +787,19 @@ func TestDevOps_CheckUpdate_Delegates(t *testing.T) {
|
|||
|
||||
func TestDevOps_Boot_Good_Success(t *testing.T) {
|
||||
t.Setenv("CORE_SKIP_SSH_SCAN", "true")
|
||||
tempDir, err := os.MkdirTemp("", "devops-boot-success-*")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = os.RemoveAll(tempDir) })
|
||||
tempDir := newManagedTempDir(t, "devops-boot-success-")
|
||||
t.Setenv("CORE_IMAGES_DIR", tempDir)
|
||||
|
||||
// Create fake image
|
||||
imagePath := filepath.Join(tempDir, ImageName())
|
||||
err = os.WriteFile(imagePath, []byte("fake"), 0644)
|
||||
imagePath := coreutil.JoinPath(tempDir, ImageName())
|
||||
err := io.Local.Write(imagePath, "fake")
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := DefaultConfig()
|
||||
mgr, err := NewImageManager(io.Local, cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
statePath := filepath.Join(tempDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tempDir, "containers.json")
|
||||
state := container.NewState(statePath)
|
||||
h := &mockHypervisor{}
|
||||
cm := container.NewLinuxKitManagerWithHypervisor(io.Local, state, h)
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ package devenv
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/container/sources"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
)
|
||||
|
||||
// ImageManager handles image downloads and updates.
|
||||
|
|
@ -49,7 +49,7 @@ func NewImageManager(m io.Medium, cfg *Config) (*ImageManager, error) {
|
|||
}
|
||||
|
||||
// Load or create manifest
|
||||
manifestPath := filepath.Join(imagesDir, "manifest.json")
|
||||
manifestPath := coreutil.JoinPath(imagesDir, "manifest.json")
|
||||
manifest, err := loadManifest(m, manifestPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -119,7 +119,7 @@ func (m *ImageManager) Install(ctx context.Context, progress func(downloaded, to
|
|||
return coreerr.E("ImageManager.Install", "failed to get latest version", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Downloading %s from %s...\n", ImageName(), src.Name())
|
||||
core.Print(nil, "Downloading %s from %s...", ImageName(), src.Name())
|
||||
|
||||
// Download
|
||||
if err := src.Download(ctx, m.medium, imagesDir, progress); err != nil {
|
||||
|
|
@ -174,14 +174,15 @@ func loadManifest(m io.Medium, path string) (*Manifest, error) {
|
|||
|
||||
content, err := m.Read(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if core.Is(err, fs.ErrNotExist) {
|
||||
return manifest, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(content), manifest); err != nil {
|
||||
return nil, err
|
||||
result := core.JSONUnmarshalString(content, manifest)
|
||||
if !result.OK {
|
||||
return nil, result.Value.(error)
|
||||
}
|
||||
manifest.medium = m
|
||||
manifest.path = path
|
||||
|
|
@ -191,9 +192,9 @@ func loadManifest(m io.Medium, path string) (*Manifest, error) {
|
|||
|
||||
// Save writes the manifest to disk.
|
||||
func (m *Manifest) Save() error {
|
||||
data, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
result := core.JSONMarshal(m)
|
||||
if !result.OK {
|
||||
return result.Value.(error)
|
||||
}
|
||||
return m.medium.Write(m.path, string(data))
|
||||
return m.medium.Write(m.path, string(result.Value.([]byte)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@ package devenv
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
"dappco.re/go/core/container/sources"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -25,8 +24,8 @@ func TestImageManager_Good_IsInstalled(t *testing.T) {
|
|||
assert.False(t, mgr.IsInstalled())
|
||||
|
||||
// Create fake image
|
||||
imagePath := filepath.Join(tmpDir, ImageName())
|
||||
err = os.WriteFile(imagePath, []byte("fake"), 0644)
|
||||
imagePath := coreutil.JoinPath(tmpDir, ImageName())
|
||||
err = io.Local.Write(imagePath, "fake")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now installed
|
||||
|
|
@ -65,7 +64,7 @@ func TestNewImageManager_Good(t *testing.T) {
|
|||
|
||||
func TestManifest_Save(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "manifest.json")
|
||||
path := coreutil.JoinPath(tmpDir, "manifest.json")
|
||||
|
||||
m := &Manifest{
|
||||
medium: io.Local,
|
||||
|
|
@ -82,8 +81,7 @@ func TestManifest_Save(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// Verify file exists and has content
|
||||
_, err = os.Stat(path)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, io.Local.IsFile(path))
|
||||
|
||||
// Reload
|
||||
m2, err := loadManifest(io.Local, path)
|
||||
|
|
@ -94,8 +92,8 @@ func TestManifest_Save(t *testing.T) {
|
|||
func TestLoadManifest_Bad(t *testing.T) {
|
||||
t.Run("invalid json", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "manifest.json")
|
||||
err := os.WriteFile(path, []byte("invalid json"), 0644)
|
||||
path := coreutil.JoinPath(tmpDir, "manifest.json")
|
||||
err := io.Local.Write(path, "invalid json")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = loadManifest(io.Local, path)
|
||||
|
|
@ -146,7 +144,7 @@ func TestNewImageManager_Good_UnknownSourceFallsToAuto(t *testing.T) {
|
|||
|
||||
func TestLoadManifest_Good_Empty(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "nonexistent.json")
|
||||
path := coreutil.JoinPath(tmpDir, "nonexistent.json")
|
||||
|
||||
m, err := loadManifest(io.Local, path)
|
||||
assert.NoError(t, err)
|
||||
|
|
@ -158,10 +156,10 @@ func TestLoadManifest_Good_Empty(t *testing.T) {
|
|||
|
||||
func TestLoadManifest_Good_ExistingData(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "manifest.json")
|
||||
path := coreutil.JoinPath(tmpDir, "manifest.json")
|
||||
|
||||
data := `{"images":{"test.img":{"version":"2.0.0","source":"cdn"}}}`
|
||||
err := os.WriteFile(path, []byte(data), 0644)
|
||||
err := io.Local.Write(path, data)
|
||||
require.NoError(t, err)
|
||||
|
||||
m, err := loadManifest(io.Local, path)
|
||||
|
|
@ -186,7 +184,7 @@ func TestImageInfo_Struct(t *testing.T) {
|
|||
|
||||
func TestManifest_Save_Good_CreatesDirs(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
nestedPath := filepath.Join(tmpDir, "nested", "dir", "manifest.json")
|
||||
nestedPath := coreutil.JoinPath(tmpDir, "nested", "dir", "manifest.json")
|
||||
|
||||
m := &Manifest{
|
||||
medium: io.Local,
|
||||
|
|
@ -200,13 +198,12 @@ func TestManifest_Save_Good_CreatesDirs(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// Verify file was created
|
||||
_, err = os.Stat(nestedPath)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, io.Local.IsFile(nestedPath))
|
||||
}
|
||||
|
||||
func TestManifest_Save_Good_Overwrite(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "manifest.json")
|
||||
path := coreutil.JoinPath(tmpDir, "manifest.json")
|
||||
|
||||
// First save
|
||||
m1 := &Manifest{
|
||||
|
|
@ -244,7 +241,7 @@ func TestImageManager_Install_Bad_NoSourceAvailable(t *testing.T) {
|
|||
mgr := &ImageManager{
|
||||
medium: io.Local,
|
||||
config: DefaultConfig(),
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")},
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: coreutil.JoinPath(tmpDir, "manifest.json")},
|
||||
sources: nil, // no sources
|
||||
}
|
||||
|
||||
|
|
@ -255,7 +252,7 @@ func TestImageManager_Install_Bad_NoSourceAvailable(t *testing.T) {
|
|||
|
||||
func TestNewImageManager_Good_CreatesDir(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
imagesDir := filepath.Join(tmpDir, "images")
|
||||
imagesDir := coreutil.JoinPath(tmpDir, "images")
|
||||
t.Setenv("CORE_IMAGES_DIR", imagesDir)
|
||||
|
||||
cfg := DefaultConfig()
|
||||
|
|
@ -264,7 +261,7 @@ func TestNewImageManager_Good_CreatesDir(t *testing.T) {
|
|||
assert.NotNil(t, mgr)
|
||||
|
||||
// Verify directory was created
|
||||
info, err := os.Stat(imagesDir)
|
||||
info, err := io.Local.Stat(imagesDir)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
}
|
||||
|
|
@ -288,8 +285,8 @@ func (m *mockImageSource) Download(ctx context.Context, medium io.Medium, dest s
|
|||
return m.downloadErr
|
||||
}
|
||||
// Create a fake image file
|
||||
imagePath := filepath.Join(dest, ImageName())
|
||||
return os.WriteFile(imagePath, []byte("mock image content"), 0644)
|
||||
imagePath := coreutil.JoinPath(dest, ImageName())
|
||||
return medium.Write(imagePath, "mock image content")
|
||||
}
|
||||
|
||||
func TestImageManager_Install_Good_WithMockSource(t *testing.T) {
|
||||
|
|
@ -305,7 +302,7 @@ func TestImageManager_Install_Good_WithMockSource(t *testing.T) {
|
|||
mgr := &ImageManager{
|
||||
medium: io.Local,
|
||||
config: DefaultConfig(),
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")},
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: coreutil.JoinPath(tmpDir, "manifest.json")},
|
||||
sources: []sources.ImageSource{mock},
|
||||
}
|
||||
|
||||
|
|
@ -334,7 +331,7 @@ func TestImageManager_Install_Bad_DownloadError(t *testing.T) {
|
|||
mgr := &ImageManager{
|
||||
medium: io.Local,
|
||||
config: DefaultConfig(),
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")},
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: coreutil.JoinPath(tmpDir, "manifest.json")},
|
||||
sources: []sources.ImageSource{mock},
|
||||
}
|
||||
|
||||
|
|
@ -355,7 +352,7 @@ func TestImageManager_Install_Bad_VersionError(t *testing.T) {
|
|||
mgr := &ImageManager{
|
||||
medium: io.Local,
|
||||
config: DefaultConfig(),
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")},
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: coreutil.JoinPath(tmpDir, "manifest.json")},
|
||||
sources: []sources.ImageSource{mock},
|
||||
}
|
||||
|
||||
|
|
@ -381,7 +378,7 @@ func TestImageManager_Install_Good_SkipsUnavailableSource(t *testing.T) {
|
|||
mgr := &ImageManager{
|
||||
medium: io.Local,
|
||||
config: DefaultConfig(),
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")},
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: coreutil.JoinPath(tmpDir, "manifest.json")},
|
||||
sources: []sources.ImageSource{unavailableMock, availableMock},
|
||||
}
|
||||
|
||||
|
|
@ -411,7 +408,7 @@ func TestImageManager_CheckUpdate_Good_WithMockSource(t *testing.T) {
|
|||
Images: map[string]ImageInfo{
|
||||
ImageName(): {Version: "v1.0.0", Source: "mock"},
|
||||
},
|
||||
path: filepath.Join(tmpDir, "manifest.json"),
|
||||
path: coreutil.JoinPath(tmpDir, "manifest.json"),
|
||||
},
|
||||
sources: []sources.ImageSource{mock},
|
||||
}
|
||||
|
|
@ -441,7 +438,7 @@ func TestImageManager_CheckUpdate_Good_NoUpdate(t *testing.T) {
|
|||
Images: map[string]ImageInfo{
|
||||
ImageName(): {Version: "v1.0.0", Source: "mock"},
|
||||
},
|
||||
path: filepath.Join(tmpDir, "manifest.json"),
|
||||
path: coreutil.JoinPath(tmpDir, "manifest.json"),
|
||||
},
|
||||
sources: []sources.ImageSource{mock},
|
||||
}
|
||||
|
|
@ -470,7 +467,7 @@ func TestImageManager_CheckUpdate_Bad_NoSource(t *testing.T) {
|
|||
Images: map[string]ImageInfo{
|
||||
ImageName(): {Version: "v1.0.0", Source: "mock"},
|
||||
},
|
||||
path: filepath.Join(tmpDir, "manifest.json"),
|
||||
path: coreutil.JoinPath(tmpDir, "manifest.json"),
|
||||
},
|
||||
sources: []sources.ImageSource{unavailableMock},
|
||||
}
|
||||
|
|
@ -498,7 +495,7 @@ func TestImageManager_CheckUpdate_Bad_VersionError(t *testing.T) {
|
|||
Images: map[string]ImageInfo{
|
||||
ImageName(): {Version: "v1.0.0", Source: "mock"},
|
||||
},
|
||||
path: filepath.Join(tmpDir, "manifest.json"),
|
||||
path: coreutil.JoinPath(tmpDir, "manifest.json"),
|
||||
},
|
||||
sources: []sources.ImageSource{mock},
|
||||
}
|
||||
|
|
@ -515,7 +512,7 @@ func TestImageManager_Install_Bad_EmptySources(t *testing.T) {
|
|||
mgr := &ImageManager{
|
||||
medium: io.Local,
|
||||
config: DefaultConfig(),
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")},
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: coreutil.JoinPath(tmpDir, "manifest.json")},
|
||||
sources: []sources.ImageSource{}, // Empty slice, not nil
|
||||
}
|
||||
|
||||
|
|
@ -534,7 +531,7 @@ func TestImageManager_Install_Bad_AllUnavailable(t *testing.T) {
|
|||
mgr := &ImageManager{
|
||||
medium: io.Local,
|
||||
config: DefaultConfig(),
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")},
|
||||
manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: coreutil.JoinPath(tmpDir, "manifest.json")},
|
||||
sources: []sources.ImageSource{mock1, mock2},
|
||||
}
|
||||
|
||||
|
|
@ -558,7 +555,7 @@ func TestImageManager_CheckUpdate_Good_FirstSourceUnavailable(t *testing.T) {
|
|||
Images: map[string]ImageInfo{
|
||||
ImageName(): {Version: "v1.0.0", Source: "available"},
|
||||
},
|
||||
path: filepath.Join(tmpDir, "manifest.json"),
|
||||
path: coreutil.JoinPath(tmpDir, "manifest.json"),
|
||||
},
|
||||
sources: []sources.ImageSource{unavailable, available},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ package devenv
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
"dappco.re/go/core/container/internal/proc"
|
||||
)
|
||||
|
||||
// ServeOptions configures the dev server.
|
||||
|
|
@ -33,7 +33,7 @@ func (d *DevOps) Serve(ctx context.Context, projectDir string, opts ServeOptions
|
|||
|
||||
servePath := projectDir
|
||||
if opts.Path != "" {
|
||||
servePath = filepath.Join(projectDir, opts.Path)
|
||||
servePath = coreutil.JoinPath(projectDir, opts.Path)
|
||||
}
|
||||
|
||||
// Mount project directory via SSHFS
|
||||
|
|
@ -43,8 +43,8 @@ func (d *DevOps) Serve(ctx context.Context, projectDir string, opts ServeOptions
|
|||
|
||||
// Detect and run serve command
|
||||
serveCmd := DetectServeCommand(d.medium, servePath)
|
||||
fmt.Printf("Starting server: %s\n", serveCmd)
|
||||
fmt.Printf("Listening on http://localhost:%d\n", opts.Port)
|
||||
core.Print(nil, "Starting server: %s", serveCmd)
|
||||
core.Print(nil, "Listening on http://localhost:%d", opts.Port)
|
||||
|
||||
// Run serve command via SSH
|
||||
return d.sshShell(ctx, []string{"cd", "/app", "&&", serveCmd})
|
||||
|
|
@ -52,21 +52,18 @@ func (d *DevOps) Serve(ctx context.Context, projectDir string, opts ServeOptions
|
|||
|
||||
// mountProject mounts a directory into the VM via SSHFS.
|
||||
func (d *DevOps) mountProject(ctx context.Context, path string) error {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
absPath := coreutil.AbsPath(path)
|
||||
|
||||
// Use reverse SSHFS mount
|
||||
// The VM connects back to host to mount the directory
|
||||
cmd := exec.CommandContext(ctx, "ssh",
|
||||
cmd := proc.NewCommandContext(ctx, "ssh",
|
||||
"-o", "StrictHostKeyChecking=yes",
|
||||
"-o", "UserKnownHostsFile=~/.core/known_hosts",
|
||||
"-o", "LogLevel=ERROR",
|
||||
"-R", "10000:localhost:22", // Reverse tunnel for SSHFS
|
||||
"-p", fmt.Sprintf("%d", DefaultSSHPort),
|
||||
"-p", core.Sprintf("%d", DefaultSSHPort),
|
||||
"root@localhost",
|
||||
fmt.Sprintf("mkdir -p /app && sshfs -p 10000 %s@localhost:%s /app -o allow_other", os.Getenv("USER"), absPath),
|
||||
core.Sprintf("mkdir -p /app && sshfs -p 10000 %s@localhost:%s /app -o allow_other", core.Env("USER"), absPath),
|
||||
)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
package devenv
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDetectServeCommand_Good_Laravel(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "artisan"), []byte("#!/usr/bin/env php"), 0644)
|
||||
err := io.Local.Write(coreutil.JoinPath(tmpDir, "artisan"), "#!/usr/bin/env php")
|
||||
assert.NoError(t, err)
|
||||
|
||||
cmd := DetectServeCommand(io.Local, tmpDir)
|
||||
|
|
@ -21,7 +20,7 @@ func TestDetectServeCommand_Good_Laravel(t *testing.T) {
|
|||
func TestDetectServeCommand_Good_NodeDev(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
packageJSON := `{"scripts":{"dev":"vite","start":"node index.js"}}`
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte(packageJSON), 0644)
|
||||
err := io.Local.Write(coreutil.JoinPath(tmpDir, "package.json"), packageJSON)
|
||||
assert.NoError(t, err)
|
||||
|
||||
cmd := DetectServeCommand(io.Local, tmpDir)
|
||||
|
|
@ -31,7 +30,7 @@ func TestDetectServeCommand_Good_NodeDev(t *testing.T) {
|
|||
func TestDetectServeCommand_Good_NodeStart(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
packageJSON := `{"scripts":{"start":"node server.js"}}`
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte(packageJSON), 0644)
|
||||
err := io.Local.Write(coreutil.JoinPath(tmpDir, "package.json"), packageJSON)
|
||||
assert.NoError(t, err)
|
||||
|
||||
cmd := DetectServeCommand(io.Local, tmpDir)
|
||||
|
|
@ -40,7 +39,7 @@ func TestDetectServeCommand_Good_NodeStart(t *testing.T) {
|
|||
|
||||
func TestDetectServeCommand_Good_PHP(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "composer.json"), []byte(`{"require":{}}`), 0644)
|
||||
err := io.Local.Write(coreutil.JoinPath(tmpDir, "composer.json"), `{"require":{}}`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
cmd := DetectServeCommand(io.Local, tmpDir)
|
||||
|
|
@ -49,9 +48,9 @@ func TestDetectServeCommand_Good_PHP(t *testing.T) {
|
|||
|
||||
func TestDetectServeCommand_Good_GoMain(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module example"), 0644)
|
||||
err := io.Local.Write(coreutil.JoinPath(tmpDir, "go.mod"), "module example")
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte("package main"), 0644)
|
||||
err = io.Local.Write(coreutil.JoinPath(tmpDir, "main.go"), "package main")
|
||||
assert.NoError(t, err)
|
||||
|
||||
cmd := DetectServeCommand(io.Local, tmpDir)
|
||||
|
|
@ -60,7 +59,7 @@ func TestDetectServeCommand_Good_GoMain(t *testing.T) {
|
|||
|
||||
func TestDetectServeCommand_Good_GoWithoutMain(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module example"), 0644)
|
||||
err := io.Local.Write(coreutil.JoinPath(tmpDir, "go.mod"), "module example")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// No main.go, so falls through to fallback
|
||||
|
|
@ -70,7 +69,7 @@ func TestDetectServeCommand_Good_GoWithoutMain(t *testing.T) {
|
|||
|
||||
func TestDetectServeCommand_Good_Django(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "manage.py"), []byte("#!/usr/bin/env python"), 0644)
|
||||
err := io.Local.Write(coreutil.JoinPath(tmpDir, "manage.py"), "#!/usr/bin/env python")
|
||||
assert.NoError(t, err)
|
||||
|
||||
cmd := DetectServeCommand(io.Local, tmpDir)
|
||||
|
|
@ -87,9 +86,9 @@ func TestDetectServeCommand_Good_Fallback(t *testing.T) {
|
|||
func TestDetectServeCommand_Good_Priority(t *testing.T) {
|
||||
// Laravel (artisan) should take priority over PHP (composer.json)
|
||||
tmpDir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "artisan"), []byte("#!/usr/bin/env php"), 0644)
|
||||
err := io.Local.Write(coreutil.JoinPath(tmpDir, "artisan"), "#!/usr/bin/env php")
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(tmpDir, "composer.json"), []byte(`{"require":{}}`), 0644)
|
||||
err = io.Local.Write(coreutil.JoinPath(tmpDir, "composer.json"), `{"require":{}}`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
cmd := DetectServeCommand(io.Local, tmpDir)
|
||||
|
|
@ -113,8 +112,8 @@ func TestServeOptions_Custom(t *testing.T) {
|
|||
|
||||
func TestHasFile_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
err := os.WriteFile(testFile, []byte("content"), 0644)
|
||||
testFile := coreutil.JoinPath(tmpDir, "test.txt")
|
||||
err := io.Local.Write(testFile, "content")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.True(t, hasFile(io.Local, tmpDir, "test.txt"))
|
||||
|
|
@ -128,8 +127,8 @@ func TestHasFile_Bad(t *testing.T) {
|
|||
|
||||
func TestHasFile_Bad_Directory(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
subDir := filepath.Join(tmpDir, "subdir")
|
||||
err := os.Mkdir(subDir, 0755)
|
||||
subDir := coreutil.JoinPath(tmpDir, "subdir")
|
||||
err := io.Local.EnsureDir(subDir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// hasFile correctly returns false for directories (only true for regular files)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ package devenv
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/container/internal/proc"
|
||||
)
|
||||
|
||||
// ShellOptions configures the shell connection.
|
||||
|
|
@ -39,7 +39,7 @@ func (d *DevOps) sshShell(ctx context.Context, command []string) error {
|
|||
"-o", "UserKnownHostsFile=~/.core/known_hosts",
|
||||
"-o", "LogLevel=ERROR",
|
||||
"-A", // Agent forwarding
|
||||
"-p", fmt.Sprintf("%d", DefaultSSHPort),
|
||||
"-p", core.Sprintf("%d", DefaultSSHPort),
|
||||
"root@localhost",
|
||||
}
|
||||
|
||||
|
|
@ -47,10 +47,10 @@ func (d *DevOps) sshShell(ctx context.Context, command []string) error {
|
|||
args = append(args, command...)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "ssh", args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd := proc.NewCommandContext(ctx, "ssh", args...)
|
||||
cmd.Stdin = proc.Stdin
|
||||
cmd.Stdout = proc.Stdout
|
||||
cmd.Stderr = proc.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
|
@ -67,10 +67,10 @@ func (d *DevOps) serialConsole(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// Use socat to connect to the console socket
|
||||
socketPath := fmt.Sprintf("/tmp/core-%s-console.sock", c.ID)
|
||||
cmd := exec.CommandContext(ctx, "socat", "-,raw,echo=0", "unix-connect:"+socketPath)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
socketPath := core.Sprintf("/tmp/core-%s-console.sock", c.ID)
|
||||
cmd := proc.NewCommandContext(ctx, "socat", "-,raw,echo=0", "unix-connect:"+socketPath)
|
||||
cmd.Stdin = proc.Stdin
|
||||
cmd.Stdout = proc.Stdout
|
||||
cmd.Stderr = proc.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,38 +2,37 @@ package devenv
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
"dappco.re/go/core/container/internal/proc"
|
||||
)
|
||||
|
||||
// ensureHostKey ensures that the host key for the dev environment is in the known hosts file.
|
||||
// This is used after boot to allow StrictHostKeyChecking=yes to work.
|
||||
func ensureHostKey(ctx context.Context, port int) error {
|
||||
// Skip if requested (used in tests)
|
||||
if os.Getenv("CORE_SKIP_SSH_SCAN") == "true" {
|
||||
if core.Env("CORE_SKIP_SSH_SCAN") == "true" {
|
||||
return nil
|
||||
}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return coreerr.E("ensureHostKey", "get home dir", err)
|
||||
home := coreutil.HomeDir()
|
||||
if home == "" {
|
||||
return coreerr.E("ensureHostKey", "get home dir", nil)
|
||||
}
|
||||
|
||||
knownHostsPath := filepath.Join(home, ".core", "known_hosts")
|
||||
knownHostsPath := coreutil.JoinPath(home, ".core", "known_hosts")
|
||||
|
||||
// Ensure directory exists
|
||||
if err := coreio.Local.EnsureDir(filepath.Dir(knownHostsPath)); err != nil {
|
||||
if err := coreio.Local.EnsureDir(core.PathDir(knownHostsPath)); err != nil {
|
||||
return coreerr.E("ensureHostKey", "create known_hosts dir", err)
|
||||
}
|
||||
|
||||
// Get host key using ssh-keyscan
|
||||
cmd := exec.CommandContext(ctx, "ssh-keyscan", "-p", fmt.Sprintf("%d", port), "localhost")
|
||||
cmd := proc.NewCommandContext(ctx, "ssh-keyscan", "-p", core.Sprintf("%d", port), "localhost")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return coreerr.E("ensureHostKey", "ssh-keyscan failed", err)
|
||||
|
|
@ -46,21 +45,27 @@ func ensureHostKey(ctx context.Context, port int) error {
|
|||
// Read existing known_hosts to avoid duplicates
|
||||
existingStr, _ := coreio.Local.Read(knownHostsPath)
|
||||
|
||||
if !coreio.Local.Exists(knownHostsPath) {
|
||||
if err := coreio.Local.WriteMode(knownHostsPath, "", 0600); err != nil {
|
||||
return coreerr.E("ensureHostKey", "create 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)
|
||||
f, err := coreio.Local.Append(knownHostsPath)
|
||||
if err != nil {
|
||||
return coreerr.E("ensureHostKey", "open known_hosts", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
lines := strings.Split(string(out), "\n")
|
||||
lines := core.Split(string(out), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
line = core.Trim(line)
|
||||
if line == "" || core.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(existingStr, line) {
|
||||
if _, err := f.WriteString(line + "\n"); err != nil {
|
||||
if !core.Contains(existingStr, line) {
|
||||
if _, err := f.Write([]byte(core.Concat(line, "\n"))); err != nil {
|
||||
return coreerr.E("ensureHostKey", "write known_hosts", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ package devenv
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
)
|
||||
|
||||
// TestConfig holds test configuration from .core/test.yaml.
|
||||
|
|
@ -45,7 +45,7 @@ func (d *DevOps) Test(ctx context.Context, projectDir string, opts TestOptions)
|
|||
|
||||
// Priority: explicit command > named command > auto-detect
|
||||
if len(opts.Command) > 0 {
|
||||
cmd = strings.Join(opts.Command, " ")
|
||||
cmd = core.Join(" ", opts.Command...)
|
||||
} else if opts.Name != "" {
|
||||
cfg, err := LoadTestConfig(d.medium, projectDir)
|
||||
if err != nil {
|
||||
|
|
@ -113,11 +113,7 @@ func DetectTestCommand(m io.Medium, projectDir string) string {
|
|||
|
||||
// LoadTestConfig loads .core/test.yaml.
|
||||
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
|
||||
}
|
||||
absPath := coreutil.AbsPath(coreutil.JoinPath(projectDir, ".core", "test.yaml"))
|
||||
|
||||
content, err := m.Read(absPath)
|
||||
if err != nil {
|
||||
|
|
@ -133,20 +129,12 @@ func LoadTestConfig(m io.Medium, projectDir string) (*TestConfig, error) {
|
|||
}
|
||||
|
||||
func hasFile(m io.Medium, dir, name string) bool {
|
||||
path := filepath.Join(dir, name)
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
absPath := coreutil.AbsPath(coreutil.JoinPath(dir, name))
|
||||
return m.IsFile(absPath)
|
||||
}
|
||||
|
||||
func hasPackageScript(m io.Medium, projectDir, script string) bool {
|
||||
path := filepath.Join(projectDir, "package.json")
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
absPath := coreutil.AbsPath(coreutil.JoinPath(projectDir, "package.json"))
|
||||
|
||||
content, err := m.Read(absPath)
|
||||
if err != nil {
|
||||
|
|
@ -156,7 +144,8 @@ func hasPackageScript(m io.Medium, projectDir, script string) bool {
|
|||
var pkg struct {
|
||||
Scripts map[string]string `json:"scripts"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(content), &pkg); err != nil {
|
||||
result := core.JSONUnmarshalString(content, &pkg)
|
||||
if !result.OK {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -165,11 +154,7 @@ func hasPackageScript(m io.Medium, projectDir, script string) bool {
|
|||
}
|
||||
|
||||
func hasComposerScript(m io.Medium, projectDir, script string) bool {
|
||||
path := filepath.Join(projectDir, "composer.json")
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
absPath := coreutil.AbsPath(coreutil.JoinPath(projectDir, "composer.json"))
|
||||
|
||||
content, err := m.Read(absPath)
|
||||
if err != nil {
|
||||
|
|
@ -179,7 +164,8 @@ func hasComposerScript(m io.Medium, projectDir, script string) bool {
|
|||
var pkg struct {
|
||||
Scripts map[string]any `json:"scripts"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(content), &pkg); err != nil {
|
||||
result := core.JSONUnmarshalString(content, &pkg)
|
||||
if !result.OK {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
package devenv
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
"dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
func TestDetectTestCommand_Good_ComposerJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "composer.json"), []byte(`{"scripts":{"test":"pest"}}`), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "composer.json"), `{"scripts":{"test":"pest"}}`)
|
||||
|
||||
cmd := DetectTestCommand(io.Local, tmpDir)
|
||||
if cmd != "composer test" {
|
||||
|
|
@ -20,7 +19,7 @@ func TestDetectTestCommand_Good_ComposerJSON(t *testing.T) {
|
|||
|
||||
func TestDetectTestCommand_Good_PackageJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte(`{"scripts":{"test":"vitest"}}`), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "package.json"), `{"scripts":{"test":"vitest"}}`)
|
||||
|
||||
cmd := DetectTestCommand(io.Local, tmpDir)
|
||||
if cmd != "npm test" {
|
||||
|
|
@ -30,7 +29,7 @@ func TestDetectTestCommand_Good_PackageJSON(t *testing.T) {
|
|||
|
||||
func TestDetectTestCommand_Good_GoMod(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module example"), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "go.mod"), "module example")
|
||||
|
||||
cmd := DetectTestCommand(io.Local, tmpDir)
|
||||
if cmd != "go test ./..." {
|
||||
|
|
@ -40,9 +39,9 @@ func TestDetectTestCommand_Good_GoMod(t *testing.T) {
|
|||
|
||||
func TestDetectTestCommand_Good_CoreTestYaml(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
coreDir := filepath.Join(tmpDir, ".core")
|
||||
_ = os.MkdirAll(coreDir, 0755)
|
||||
_ = os.WriteFile(filepath.Join(coreDir, "test.yaml"), []byte("command: custom-test"), 0644)
|
||||
coreDir := coreutil.JoinPath(tmpDir, ".core")
|
||||
_ = io.Local.EnsureDir(coreDir)
|
||||
_ = io.Local.Write(coreutil.JoinPath(coreDir, "test.yaml"), "command: custom-test")
|
||||
|
||||
cmd := DetectTestCommand(io.Local, tmpDir)
|
||||
if cmd != "custom-test" {
|
||||
|
|
@ -52,7 +51,7 @@ func TestDetectTestCommand_Good_CoreTestYaml(t *testing.T) {
|
|||
|
||||
func TestDetectTestCommand_Good_Pytest(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "pytest.ini"), []byte("[pytest]"), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "pytest.ini"), "[pytest]")
|
||||
|
||||
cmd := DetectTestCommand(io.Local, tmpDir)
|
||||
if cmd != "pytest" {
|
||||
|
|
@ -62,7 +61,7 @@ func TestDetectTestCommand_Good_Pytest(t *testing.T) {
|
|||
|
||||
func TestDetectTestCommand_Good_Taskfile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "Taskfile.yaml"), []byte("version: '3'"), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "Taskfile.yaml"), "version: '3'")
|
||||
|
||||
cmd := DetectTestCommand(io.Local, tmpDir)
|
||||
if cmd != "task test" {
|
||||
|
|
@ -82,10 +81,10 @@ func TestDetectTestCommand_Bad_NoFiles(t *testing.T) {
|
|||
func TestDetectTestCommand_Good_Priority(t *testing.T) {
|
||||
// .core/test.yaml should take priority over other detection methods
|
||||
tmpDir := t.TempDir()
|
||||
coreDir := filepath.Join(tmpDir, ".core")
|
||||
_ = os.MkdirAll(coreDir, 0755)
|
||||
_ = os.WriteFile(filepath.Join(coreDir, "test.yaml"), []byte("command: my-custom-test"), 0644)
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module example"), 0644)
|
||||
coreDir := coreutil.JoinPath(tmpDir, ".core")
|
||||
_ = io.Local.EnsureDir(coreDir)
|
||||
_ = io.Local.Write(coreutil.JoinPath(coreDir, "test.yaml"), "command: my-custom-test")
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "go.mod"), "module example")
|
||||
|
||||
cmd := DetectTestCommand(io.Local, tmpDir)
|
||||
if cmd != "my-custom-test" {
|
||||
|
|
@ -95,8 +94,8 @@ func TestDetectTestCommand_Good_Priority(t *testing.T) {
|
|||
|
||||
func TestLoadTestConfig_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
coreDir := filepath.Join(tmpDir, ".core")
|
||||
_ = os.MkdirAll(coreDir, 0755)
|
||||
coreDir := coreutil.JoinPath(tmpDir, ".core")
|
||||
_ = io.Local.EnsureDir(coreDir)
|
||||
|
||||
configYAML := `version: 1
|
||||
command: default-test
|
||||
|
|
@ -108,7 +107,7 @@ commands:
|
|||
env:
|
||||
CI: "true"
|
||||
`
|
||||
_ = os.WriteFile(filepath.Join(coreDir, "test.yaml"), []byte(configYAML), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(coreDir, "test.yaml"), configYAML)
|
||||
|
||||
cfg, err := LoadTestConfig(io.Local, tmpDir)
|
||||
if err != nil {
|
||||
|
|
@ -143,7 +142,7 @@ func TestLoadTestConfig_Bad_NotFound(t *testing.T) {
|
|||
|
||||
func TestHasPackageScript_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte(`{"scripts":{"test":"jest","build":"webpack"}}`), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "package.json"), `{"scripts":{"test":"jest","build":"webpack"}}`)
|
||||
|
||||
if !hasPackageScript(io.Local, tmpDir, "test") {
|
||||
t.Error("expected to find 'test' script")
|
||||
|
|
@ -155,7 +154,7 @@ func TestHasPackageScript_Good(t *testing.T) {
|
|||
|
||||
func TestHasPackageScript_Bad_MissingScript(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte(`{"scripts":{"build":"webpack"}}`), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "package.json"), `{"scripts":{"build":"webpack"}}`)
|
||||
|
||||
if hasPackageScript(io.Local, tmpDir, "test") {
|
||||
t.Error("expected not to find 'test' script")
|
||||
|
|
@ -164,7 +163,7 @@ func TestHasPackageScript_Bad_MissingScript(t *testing.T) {
|
|||
|
||||
func TestHasComposerScript_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "composer.json"), []byte(`{"scripts":{"test":"pest","post-install-cmd":"@php artisan migrate"}}`), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "composer.json"), `{"scripts":{"test":"pest","post-install-cmd":"@php artisan migrate"}}`)
|
||||
|
||||
if !hasComposerScript(io.Local, tmpDir, "test") {
|
||||
t.Error("expected to find 'test' script")
|
||||
|
|
@ -173,7 +172,7 @@ func TestHasComposerScript_Good(t *testing.T) {
|
|||
|
||||
func TestHasComposerScript_Bad_MissingScript(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "composer.json"), []byte(`{"scripts":{"build":"@php build.php"}}`), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "composer.json"), `{"scripts":{"build":"@php build.php"}}`)
|
||||
|
||||
if hasComposerScript(io.Local, tmpDir, "test") {
|
||||
t.Error("expected not to find 'test' script")
|
||||
|
|
@ -229,7 +228,7 @@ func TestTestOptions_Struct(t *testing.T) {
|
|||
|
||||
func TestDetectTestCommand_Good_TaskfileYml(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "Taskfile.yml"), []byte("version: '3'"), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "Taskfile.yml"), "version: '3'")
|
||||
|
||||
cmd := DetectTestCommand(io.Local, tmpDir)
|
||||
if cmd != "task test" {
|
||||
|
|
@ -239,7 +238,7 @@ func TestDetectTestCommand_Good_TaskfileYml(t *testing.T) {
|
|||
|
||||
func TestDetectTestCommand_Good_Pyproject(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "pyproject.toml"), []byte("[tool.pytest]"), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "pyproject.toml"), "[tool.pytest]")
|
||||
|
||||
cmd := DetectTestCommand(io.Local, tmpDir)
|
||||
if cmd != "pytest" {
|
||||
|
|
@ -257,7 +256,7 @@ func TestHasPackageScript_Bad_NoFile(t *testing.T) {
|
|||
|
||||
func TestHasPackageScript_Bad_InvalidJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte(`invalid json`), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "package.json"), `invalid json`)
|
||||
|
||||
if hasPackageScript(io.Local, tmpDir, "test") {
|
||||
t.Error("expected false for invalid JSON")
|
||||
|
|
@ -266,7 +265,7 @@ func TestHasPackageScript_Bad_InvalidJSON(t *testing.T) {
|
|||
|
||||
func TestHasPackageScript_Bad_NoScripts(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte(`{"name":"test"}`), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "package.json"), `{"name":"test"}`)
|
||||
|
||||
if hasPackageScript(io.Local, tmpDir, "test") {
|
||||
t.Error("expected false for missing scripts section")
|
||||
|
|
@ -283,7 +282,7 @@ func TestHasComposerScript_Bad_NoFile(t *testing.T) {
|
|||
|
||||
func TestHasComposerScript_Bad_InvalidJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "composer.json"), []byte(`invalid json`), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "composer.json"), `invalid json`)
|
||||
|
||||
if hasComposerScript(io.Local, tmpDir, "test") {
|
||||
t.Error("expected false for invalid JSON")
|
||||
|
|
@ -292,7 +291,7 @@ func TestHasComposerScript_Bad_InvalidJSON(t *testing.T) {
|
|||
|
||||
func TestHasComposerScript_Bad_NoScripts(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "composer.json"), []byte(`{"name":"test/pkg"}`), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "composer.json"), `{"name":"test/pkg"}`)
|
||||
|
||||
if hasComposerScript(io.Local, tmpDir, "test") {
|
||||
t.Error("expected false for missing scripts section")
|
||||
|
|
@ -301,9 +300,9 @@ func TestHasComposerScript_Bad_NoScripts(t *testing.T) {
|
|||
|
||||
func TestLoadTestConfig_Bad_InvalidYAML(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
coreDir := filepath.Join(tmpDir, ".core")
|
||||
_ = os.MkdirAll(coreDir, 0755)
|
||||
_ = os.WriteFile(filepath.Join(coreDir, "test.yaml"), []byte("invalid: yaml: :"), 0644)
|
||||
coreDir := coreutil.JoinPath(tmpDir, ".core")
|
||||
_ = io.Local.EnsureDir(coreDir)
|
||||
_ = io.Local.Write(coreutil.JoinPath(coreDir, "test.yaml"), "invalid: yaml: :")
|
||||
|
||||
_, err := LoadTestConfig(io.Local, tmpDir)
|
||||
if err == nil {
|
||||
|
|
@ -313,9 +312,9 @@ func TestLoadTestConfig_Bad_InvalidYAML(t *testing.T) {
|
|||
|
||||
func TestLoadTestConfig_Good_MinimalConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
coreDir := filepath.Join(tmpDir, ".core")
|
||||
_ = os.MkdirAll(coreDir, 0755)
|
||||
_ = os.WriteFile(filepath.Join(coreDir, "test.yaml"), []byte("version: 1"), 0644)
|
||||
coreDir := coreutil.JoinPath(tmpDir, ".core")
|
||||
_ = io.Local.EnsureDir(coreDir)
|
||||
_ = io.Local.Write(coreutil.JoinPath(coreDir, "test.yaml"), "version: 1")
|
||||
|
||||
cfg, err := LoadTestConfig(io.Local, tmpDir)
|
||||
if err != nil {
|
||||
|
|
@ -332,7 +331,7 @@ func TestLoadTestConfig_Good_MinimalConfig(t *testing.T) {
|
|||
func TestDetectTestCommand_Good_ComposerWithoutScript(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
// composer.json without test script should not return composer test
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "composer.json"), []byte(`{"name":"test/pkg"}`), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "composer.json"), `{"name":"test/pkg"}`)
|
||||
|
||||
cmd := DetectTestCommand(io.Local, tmpDir)
|
||||
// Falls through to empty (no match)
|
||||
|
|
@ -344,7 +343,7 @@ func TestDetectTestCommand_Good_ComposerWithoutScript(t *testing.T) {
|
|||
func TestDetectTestCommand_Good_PackageJSONWithoutScript(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
// package.json without test or dev script
|
||||
_ = os.WriteFile(filepath.Join(tmpDir, "package.json"), []byte(`{"name":"test"}`), 0644)
|
||||
_ = io.Local.Write(coreutil.JoinPath(tmpDir, "package.json"), `{"name":"test"}`)
|
||||
|
||||
cmd := DetectTestCommand(io.Local, tmpDir)
|
||||
// Falls through to empty
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -3,6 +3,7 @@ module dappco.re/go/core/container
|
|||
go 1.26.0
|
||||
|
||||
require (
|
||||
dappco.re/go/core v0.8.0-alpha.1
|
||||
dappco.re/go/core/i18n v0.2.0
|
||||
dappco.re/go/core/io v0.2.0
|
||||
dappco.re/go/core/log v0.1.0
|
||||
|
|
@ -13,7 +14,6 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
dappco.re/go/core v0.5.0 // indirect
|
||||
forge.lthn.ai/core/go v0.3.3 // indirect
|
||||
forge.lthn.ai/core/go-i18n v0.1.7 // indirect
|
||||
forge.lthn.ai/core/go-inference v0.1.6 // indirect
|
||||
|
|
|
|||
37
go.sum
37
go.sum
|
|
@ -1,17 +1,19 @@
|
|||
dappco.re/go/core v0.5.0 h1:P5DJoaCiK5Q+af5UiTdWqUIW4W4qYKzpgGK50thm21U=
|
||||
dappco.re/go/core v0.5.0/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
|
||||
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||
dappco.re/go/core/i18n v0.2.0 h1:NHzk6RCU93/qVRA3f2jvMr9P1R6FYheR/sHL+TnvKbI=
|
||||
dappco.re/go/core/i18n v0.2.0/go.mod h1:9eSVJXr3OpIGWQvDynfhqcp27xnLMwlYLgsByU+p7ok=
|
||||
dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
|
||||
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
|
||||
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
|
||||
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
|
||||
forge.lthn.ai/Snider/Borg v0.3.1/go.mod h1:Z7DJD0yHXsxSyM7Mjl6/g4gH1NBsIz44Bf5AFlV76Wg=
|
||||
forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg=
|
||||
forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs=
|
||||
forge.lthn.ai/core/config v0.1.8 h1:xP2hys7T94QGVF/OTh84/Zr5Dm/dL/0vzjht8zi+LOg=
|
||||
forge.lthn.ai/core/config v0.1.8/go.mod h1:8epZrkwoCt+5ayrqdinOUU/+w6UoxOyv9ZrdgVOgYfQ=
|
||||
forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0=
|
||||
forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ=
|
||||
forge.lthn.ai/core/go-crypt v0.1.6/go.mod h1:4VZAGqxlbadhSB66sJkdj54/HSJ+bSxVgwWK5kMMYDo=
|
||||
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA=
|
||||
forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8=
|
||||
forge.lthn.ai/core/go-inference v0.1.6 h1:ce42zC0zO8PuISUyAukAN1NACEdWp5wF1mRgnh5+58E=
|
||||
|
|
@ -20,8 +22,22 @@ forge.lthn.ai/core/go-io v0.1.7 h1:Tdb6sqh+zz1lsGJaNX9RFWM6MJ/RhSAyxfulLXrJsbk=
|
|||
forge.lthn.ai/core/go-io v0.1.7/go.mod h1:8lRLFk4Dnp5cR/Cyzh9WclD5566TbpdRgwcH7UZLWn4=
|
||||
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
|
||||
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
|
||||
github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||
|
|
@ -32,15 +48,19 @@ github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF
|
|||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
|
|
@ -51,6 +71,7 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE
|
|||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
|
|
@ -71,10 +92,12 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
|
|||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
|
|
@ -82,6 +105,7 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
|
|||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
|
|
@ -93,6 +117,7 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
|||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
|
|
@ -101,8 +126,11 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
|
|||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
|
|
@ -111,8 +139,13 @@ golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
|||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
|
|
|
|||
|
|
@ -2,14 +2,13 @@ package container
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/container/internal/proc"
|
||||
)
|
||||
|
||||
// Hypervisor defines the interface for VM hypervisors.
|
||||
|
|
@ -19,7 +18,7 @@ type Hypervisor interface {
|
|||
// Available checks if the hypervisor is available on the system.
|
||||
Available() bool
|
||||
// BuildCommand builds the command to run a VM with the given options.
|
||||
BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*exec.Cmd, error)
|
||||
BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*proc.Command, error)
|
||||
}
|
||||
|
||||
// HypervisorOptions contains options for running a VM.
|
||||
|
|
@ -60,20 +59,20 @@ func (q *QemuHypervisor) Name() string {
|
|||
|
||||
// Available checks if QEMU is installed and accessible.
|
||||
func (q *QemuHypervisor) Available() bool {
|
||||
_, err := exec.LookPath(q.Binary)
|
||||
_, err := proc.LookPath(q.Binary)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// BuildCommand creates the QEMU command for running a VM.
|
||||
func (q *QemuHypervisor) BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*exec.Cmd, error) {
|
||||
func (q *QemuHypervisor) BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*proc.Command, error) {
|
||||
format := DetectImageFormat(image)
|
||||
if format == FormatUnknown {
|
||||
return nil, coreerr.E("QemuHypervisor.BuildCommand", "unknown image format: "+image, nil)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-m", fmt.Sprintf("%d", opts.Memory),
|
||||
"-smp", fmt.Sprintf("%d", opts.CPUs),
|
||||
"-m", core.Sprintf("%d", opts.Memory),
|
||||
"-smp", core.Sprintf("%d", opts.CPUs),
|
||||
"-enable-kvm",
|
||||
}
|
||||
|
||||
|
|
@ -83,11 +82,11 @@ func (q *QemuHypervisor) BuildCommand(ctx context.Context, image string, opts *H
|
|||
args = append(args, "-cdrom", image)
|
||||
args = append(args, "-boot", "d")
|
||||
case FormatQCOW2:
|
||||
args = append(args, "-drive", fmt.Sprintf("file=%s,format=qcow2", image))
|
||||
args = append(args, "-drive", core.Sprintf("file=%s,format=qcow2", image))
|
||||
case FormatVMDK:
|
||||
args = append(args, "-drive", fmt.Sprintf("file=%s,format=vmdk", image))
|
||||
args = append(args, "-drive", core.Sprintf("file=%s,format=vmdk", image))
|
||||
case FormatRaw:
|
||||
args = append(args, "-drive", fmt.Sprintf("file=%s,format=raw", image))
|
||||
args = append(args, "-drive", core.Sprintf("file=%s,format=raw", image))
|
||||
}
|
||||
|
||||
// Always run in nographic mode for container-like behavior
|
||||
|
|
@ -99,10 +98,10 @@ func (q *QemuHypervisor) BuildCommand(ctx context.Context, image string, opts *H
|
|||
// Network with port forwarding
|
||||
netdev := "user,id=net0"
|
||||
if opts.SSHPort > 0 {
|
||||
netdev += fmt.Sprintf(",hostfwd=tcp::%d-:22", opts.SSHPort)
|
||||
netdev += core.Sprintf(",hostfwd=tcp::%d-:22", opts.SSHPort)
|
||||
}
|
||||
for hostPort, guestPort := range opts.Ports {
|
||||
netdev += fmt.Sprintf(",hostfwd=tcp::%d-:%d", hostPort, guestPort)
|
||||
netdev += core.Sprintf(",hostfwd=tcp::%d-:%d", hostPort, guestPort)
|
||||
}
|
||||
args = append(args, "-netdev", netdev)
|
||||
args = append(args, "-device", "virtio-net-pci,netdev=net0")
|
||||
|
|
@ -110,10 +109,10 @@ func (q *QemuHypervisor) BuildCommand(ctx context.Context, image string, opts *H
|
|||
// Add 9p shares for volumes
|
||||
shareID := 0
|
||||
for hostPath, guestPath := range opts.Volumes {
|
||||
tag := fmt.Sprintf("share%d", shareID)
|
||||
tag := core.Sprintf("share%d", shareID)
|
||||
args = append(args,
|
||||
"-fsdev", fmt.Sprintf("local,id=%s,path=%s,security_model=none", tag, hostPath),
|
||||
"-device", fmt.Sprintf("virtio-9p-pci,fsdev=%s,mount_tag=%s", tag, filepath.Base(guestPath)),
|
||||
"-fsdev", core.Sprintf("local,id=%s,path=%s,security_model=none", tag, hostPath),
|
||||
"-device", core.Sprintf("virtio-9p-pci,fsdev=%s,mount_tag=%s", tag, core.PathBase(guestPath)),
|
||||
)
|
||||
shareID++
|
||||
}
|
||||
|
|
@ -135,14 +134,12 @@ func (q *QemuHypervisor) BuildCommand(ctx context.Context, image string, opts *H
|
|||
}
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, q.Binary, args...)
|
||||
return cmd, nil
|
||||
return proc.NewCommandContext(ctx, q.Binary, args...), nil
|
||||
}
|
||||
|
||||
// isKVMAvailable checks if KVM is available on the system.
|
||||
func isKVMAvailable() bool {
|
||||
_, err := os.Stat("/dev/kvm")
|
||||
return err == nil
|
||||
return coreio.Local.Exists("/dev/kvm")
|
||||
}
|
||||
|
||||
// HyperkitHypervisor implements Hypervisor for macOS Hyperkit.
|
||||
|
|
@ -168,20 +165,20 @@ func (h *HyperkitHypervisor) Available() bool {
|
|||
if runtime.GOOS != "darwin" {
|
||||
return false
|
||||
}
|
||||
_, err := exec.LookPath(h.Binary)
|
||||
_, err := proc.LookPath(h.Binary)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// BuildCommand creates the Hyperkit command for running a VM.
|
||||
func (h *HyperkitHypervisor) BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*exec.Cmd, error) {
|
||||
func (h *HyperkitHypervisor) BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*proc.Command, error) {
|
||||
format := DetectImageFormat(image)
|
||||
if format == FormatUnknown {
|
||||
return nil, coreerr.E("HyperkitHypervisor.BuildCommand", "unknown image format: "+image, nil)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-m", fmt.Sprintf("%dM", opts.Memory),
|
||||
"-c", fmt.Sprintf("%d", opts.CPUs),
|
||||
"-m", core.Sprintf("%dM", opts.Memory),
|
||||
"-c", core.Sprintf("%d", opts.CPUs),
|
||||
"-A", // ACPI
|
||||
"-u", // Unlimited console output
|
||||
"-s", "0:0,hostbridge",
|
||||
|
|
@ -192,9 +189,9 @@ func (h *HyperkitHypervisor) BuildCommand(ctx context.Context, image string, opt
|
|||
// Add PCI slot for disk (slot 2)
|
||||
switch format {
|
||||
case FormatISO:
|
||||
args = append(args, "-s", fmt.Sprintf("2:0,ahci-cd,%s", image))
|
||||
args = append(args, "-s", core.Sprintf("2:0,ahci-cd,%s", image))
|
||||
case FormatQCOW2, FormatVMDK, FormatRaw:
|
||||
args = append(args, "-s", fmt.Sprintf("2:0,virtio-blk,%s", image))
|
||||
args = append(args, "-s", core.Sprintf("2:0,virtio-blk,%s", image))
|
||||
}
|
||||
|
||||
// Network with port forwarding (slot 3)
|
||||
|
|
@ -203,24 +200,23 @@ func (h *HyperkitHypervisor) BuildCommand(ctx context.Context, image string, opt
|
|||
// Hyperkit uses slirp for user networking with port forwarding
|
||||
portForwards := make([]string, 0)
|
||||
if opts.SSHPort > 0 {
|
||||
portForwards = append(portForwards, fmt.Sprintf("tcp:%d:22", opts.SSHPort))
|
||||
portForwards = append(portForwards, core.Sprintf("tcp:%d:22", opts.SSHPort))
|
||||
}
|
||||
for hostPort, guestPort := range opts.Ports {
|
||||
portForwards = append(portForwards, fmt.Sprintf("tcp:%d:%d", hostPort, guestPort))
|
||||
portForwards = append(portForwards, core.Sprintf("tcp:%d:%d", hostPort, guestPort))
|
||||
}
|
||||
if len(portForwards) > 0 {
|
||||
netArgs += "," + strings.Join(portForwards, ",")
|
||||
netArgs += "," + core.Join(",", portForwards...)
|
||||
}
|
||||
}
|
||||
args = append(args, "-s", "3:0,"+netArgs)
|
||||
|
||||
cmd := exec.CommandContext(ctx, h.Binary, args...)
|
||||
return cmd, nil
|
||||
return proc.NewCommandContext(ctx, h.Binary, args...), nil
|
||||
}
|
||||
|
||||
// DetectImageFormat determines the image format from its file extension.
|
||||
func DetectImageFormat(path string) ImageFormat {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
ext := core.Lower(core.PathExt(path))
|
||||
switch ext {
|
||||
case ".iso":
|
||||
return FormatISO
|
||||
|
|
@ -256,7 +252,7 @@ func DetectHypervisor() (Hypervisor, error) {
|
|||
|
||||
// GetHypervisor returns a specific hypervisor by name.
|
||||
func GetHypervisor(name string) (Hypervisor, error) {
|
||||
switch strings.ToLower(name) {
|
||||
switch core.Lower(name) {
|
||||
case "qemu":
|
||||
h := NewQemuHypervisor()
|
||||
if !h.Available() {
|
||||
|
|
|
|||
76
internal/coreutil/coreutil.go
Normal file
76
internal/coreutil/coreutil.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package coreutil
|
||||
|
||||
import (
|
||||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
)
|
||||
|
||||
// DirSep returns the active directory separator.
|
||||
func DirSep() string {
|
||||
if ds := core.Env("DS"); ds != "" {
|
||||
return ds
|
||||
}
|
||||
return "/"
|
||||
}
|
||||
|
||||
// JoinPath joins path segments using the active directory separator.
|
||||
func JoinPath(parts ...string) string {
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
return core.CleanPath(core.Join(DirSep(), parts...), DirSep())
|
||||
}
|
||||
|
||||
// HomeDir returns the current home directory, honouring test-time env overrides.
|
||||
func HomeDir() string {
|
||||
if home := core.Env("CORE_HOME"); home != "" {
|
||||
return home
|
||||
}
|
||||
if home := core.Env("HOME"); home != "" {
|
||||
return home
|
||||
}
|
||||
if home := core.Env("USERPROFILE"); home != "" {
|
||||
return home
|
||||
}
|
||||
return core.Env("DIR_HOME")
|
||||
}
|
||||
|
||||
// CurrentDir returns the current working directory, honouring shell PWD.
|
||||
func CurrentDir() string {
|
||||
if pwd := core.Env("PWD"); pwd != "" {
|
||||
return pwd
|
||||
}
|
||||
return core.Env("DIR_CWD")
|
||||
}
|
||||
|
||||
// TempDir returns the process temp directory, honouring TMPDIR.
|
||||
func TempDir() string {
|
||||
if dir := core.Env("TMPDIR"); dir != "" {
|
||||
return dir
|
||||
}
|
||||
return core.Env("DIR_TMP")
|
||||
}
|
||||
|
||||
// AbsPath resolves a path against the current working directory.
|
||||
func AbsPath(path string) string {
|
||||
if path == "" {
|
||||
return CurrentDir()
|
||||
}
|
||||
if core.PathIsAbs(path) {
|
||||
return core.CleanPath(path, DirSep())
|
||||
}
|
||||
return JoinPath(CurrentDir(), path)
|
||||
}
|
||||
|
||||
// MkdirTemp creates a temporary directory with a deterministic Core-generated name.
|
||||
func MkdirTemp(prefix string) (string, error) {
|
||||
name := prefix
|
||||
if name == "" {
|
||||
name = "tmp-"
|
||||
}
|
||||
path := JoinPath(TempDir(), core.Concat(name, core.ID()))
|
||||
if err := coreio.Local.EnsureDir(path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
401
internal/proc/proc.go
Normal file
401
internal/proc/proc.go
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
package proc
|
||||
|
||||
import (
|
||||
"context"
|
||||
goio "io"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
coreio "dappco.re/go/core/io"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
)
|
||||
|
||||
type fdProvider interface {
|
||||
Fd() uintptr
|
||||
}
|
||||
|
||||
type Process struct {
|
||||
Pid int
|
||||
}
|
||||
|
||||
func (p *Process) Kill() error {
|
||||
if p == nil || p.Pid <= 0 {
|
||||
return nil
|
||||
}
|
||||
return syscall.Kill(p.Pid, syscall.SIGKILL)
|
||||
}
|
||||
|
||||
func (p *Process) Signal(sig syscall.Signal) error {
|
||||
if p == nil || p.Pid <= 0 {
|
||||
return nil
|
||||
}
|
||||
return syscall.Kill(p.Pid, sig)
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Path string
|
||||
Args []string
|
||||
Dir string
|
||||
Env []string
|
||||
Stdin goio.Reader
|
||||
Stdout goio.Writer
|
||||
Stderr goio.Writer
|
||||
|
||||
Process *Process
|
||||
|
||||
ctx context.Context
|
||||
|
||||
started bool
|
||||
done chan struct{}
|
||||
waitErr error
|
||||
waited bool
|
||||
waitMu sync.Mutex
|
||||
|
||||
stdoutPipe *pipeReader
|
||||
stderrPipe *pipeReader
|
||||
}
|
||||
|
||||
type pipeReader struct {
|
||||
fd int
|
||||
childFD int
|
||||
}
|
||||
|
||||
func (p *pipeReader) Read(data []byte) (int, error) {
|
||||
n, err := syscall.Read(p.fd, data)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
if n == 0 {
|
||||
return 0, goio.EOF
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (p *pipeReader) Close() error {
|
||||
var first error
|
||||
if p.fd >= 0 {
|
||||
if err := syscall.Close(p.fd); err != nil {
|
||||
first = err
|
||||
}
|
||||
p.fd = -1
|
||||
}
|
||||
if p.childFD >= 0 {
|
||||
if err := syscall.Close(p.childFD); err != nil && first == nil {
|
||||
first = err
|
||||
}
|
||||
p.childFD = -1
|
||||
}
|
||||
return first
|
||||
}
|
||||
|
||||
type stdioReader struct {
|
||||
fd int
|
||||
}
|
||||
|
||||
func (s *stdioReader) Read(data []byte) (int, error) {
|
||||
n, err := syscall.Read(s.fd, data)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
if n == 0 {
|
||||
return 0, goio.EOF
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (s *stdioReader) Close() error { return nil }
|
||||
|
||||
func (s *stdioReader) Fd() uintptr { return uintptr(s.fd) }
|
||||
|
||||
type stdioWriter struct {
|
||||
fd int
|
||||
}
|
||||
|
||||
func (s *stdioWriter) Write(data []byte) (int, error) {
|
||||
total := 0
|
||||
for len(data) > 0 {
|
||||
n, err := syscall.Write(s.fd, data)
|
||||
total += n
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
data = data[n:]
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (s *stdioWriter) Close() error { return nil }
|
||||
|
||||
func (s *stdioWriter) Fd() uintptr { return uintptr(s.fd) }
|
||||
|
||||
var (
|
||||
Stdin goio.ReadCloser = &stdioReader{fd: 0}
|
||||
Stdout goio.WriteCloser = &stdioWriter{fd: 1}
|
||||
Stderr goio.WriteCloser = &stdioWriter{fd: 2}
|
||||
)
|
||||
|
||||
var (
|
||||
nullFD int
|
||||
nullOnce sync.Once
|
||||
nullErr error
|
||||
)
|
||||
|
||||
func Environ() []string {
|
||||
return syscall.Environ()
|
||||
}
|
||||
|
||||
func NewCommandContext(ctx context.Context, name string, args ...string) *Command {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return &Command{
|
||||
Path: name,
|
||||
Args: append([]string{name}, args...),
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func NewCommand(name string, args ...string) *Command {
|
||||
return NewCommandContext(context.Background(), name, args...)
|
||||
}
|
||||
|
||||
func LookPath(name string) (string, error) {
|
||||
if name == "" {
|
||||
return "", core.E("proc.LookPath", "empty command", nil)
|
||||
}
|
||||
if core.Contains(name, "/") || core.Contains(name, "\\") {
|
||||
if isExecutable(name) {
|
||||
return name, nil
|
||||
}
|
||||
return "", core.E("proc.LookPath", core.Concat("executable not found: ", name), nil)
|
||||
}
|
||||
|
||||
pathEnv := core.Env("PATH")
|
||||
sep := core.Env("PS")
|
||||
if sep == "" {
|
||||
sep = ":"
|
||||
}
|
||||
|
||||
for _, dir := range core.Split(pathEnv, sep) {
|
||||
if dir == "" {
|
||||
dir = "."
|
||||
}
|
||||
candidate := coreutil.JoinPath(dir, name)
|
||||
if isExecutable(candidate) {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", core.E("proc.LookPath", core.Concat("executable not found: ", name), nil)
|
||||
}
|
||||
|
||||
func (c *Command) StdoutPipe() (goio.ReadCloser, error) {
|
||||
if c.started {
|
||||
return nil, core.E("proc.Command.StdoutPipe", "command already started", nil)
|
||||
}
|
||||
if c.stdoutPipe != nil {
|
||||
return nil, core.E("proc.Command.StdoutPipe", "stdout pipe already requested", nil)
|
||||
}
|
||||
fds := make([]int, 2)
|
||||
if err := syscall.Pipe(fds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.stdoutPipe = &pipeReader{fd: fds[0], childFD: fds[1]}
|
||||
return c.stdoutPipe, nil
|
||||
}
|
||||
|
||||
func (c *Command) StderrPipe() (goio.ReadCloser, error) {
|
||||
if c.started {
|
||||
return nil, core.E("proc.Command.StderrPipe", "command already started", nil)
|
||||
}
|
||||
if c.stderrPipe != nil {
|
||||
return nil, core.E("proc.Command.StderrPipe", "stderr pipe already requested", nil)
|
||||
}
|
||||
fds := make([]int, 2)
|
||||
if err := syscall.Pipe(fds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.stderrPipe = &pipeReader{fd: fds[0], childFD: fds[1]}
|
||||
return c.stderrPipe, nil
|
||||
}
|
||||
|
||||
func (c *Command) Start() error {
|
||||
if c.started {
|
||||
return core.E("proc.Command.Start", "command already started", nil)
|
||||
}
|
||||
if c.ctx != nil {
|
||||
if err := c.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
path, err := LookPath(c.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
files := []uintptr{
|
||||
c.inputFD(),
|
||||
c.outputFD(c.stdoutPipe, c.Stdout),
|
||||
c.outputFD(c.stderrPipe, c.Stderr),
|
||||
}
|
||||
|
||||
env := c.Env
|
||||
if env == nil {
|
||||
env = Environ()
|
||||
}
|
||||
|
||||
pid, _, err := syscall.StartProcess(path, c.Args, &syscall.ProcAttr{
|
||||
Dir: c.Dir,
|
||||
Env: env,
|
||||
Files: files,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Process = &Process{Pid: pid}
|
||||
c.done = make(chan struct{})
|
||||
c.started = true
|
||||
c.closeChildPipeEnds()
|
||||
c.watchContext()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Command) Run() error {
|
||||
if err := c.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Wait()
|
||||
}
|
||||
|
||||
func (c *Command) Output() ([]byte, error) {
|
||||
if c.Stdout != nil {
|
||||
return nil, core.E("proc.Command.Output", "stdout already configured", nil)
|
||||
}
|
||||
reader, err := c.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = reader.Close() }()
|
||||
|
||||
if err := c.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, readErr := goio.ReadAll(reader)
|
||||
waitErr := c.Wait()
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
if waitErr != nil {
|
||||
return data, waitErr
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (c *Command) Wait() error {
|
||||
c.waitMu.Lock()
|
||||
defer c.waitMu.Unlock()
|
||||
|
||||
if !c.started {
|
||||
return core.E("proc.Command.Wait", "command not started", nil)
|
||||
}
|
||||
if c.waited {
|
||||
return c.waitErr
|
||||
}
|
||||
|
||||
var status syscall.WaitStatus
|
||||
for {
|
||||
_, err := syscall.Wait4(c.Process.Pid, &status, 0, nil)
|
||||
if err == syscall.EINTR {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
c.waitErr = err
|
||||
break
|
||||
}
|
||||
if status.Exited() && status.ExitStatus() != 0 {
|
||||
c.waitErr = core.E("proc.Command.Wait", core.Sprintf("exit status %d", status.ExitStatus()), nil)
|
||||
}
|
||||
if status.Signaled() {
|
||||
c.waitErr = core.E("proc.Command.Wait", core.Sprintf("signal %d", status.Signal()), nil)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
c.waited = true
|
||||
close(c.done)
|
||||
return c.waitErr
|
||||
}
|
||||
|
||||
func (c *Command) inputFD() uintptr {
|
||||
if c.Stdin == nil {
|
||||
return uintptr(openNull())
|
||||
}
|
||||
if file, ok := c.Stdin.(fdProvider); ok {
|
||||
return file.Fd()
|
||||
}
|
||||
return uintptr(openNull())
|
||||
}
|
||||
|
||||
func (c *Command) outputFD(pipe *pipeReader, writer goio.Writer) uintptr {
|
||||
if pipe != nil {
|
||||
return uintptr(pipe.childFD)
|
||||
}
|
||||
if writer == nil {
|
||||
return uintptr(openNull())
|
||||
}
|
||||
if file, ok := writer.(fdProvider); ok {
|
||||
return file.Fd()
|
||||
}
|
||||
return uintptr(openNull())
|
||||
}
|
||||
|
||||
func (c *Command) closeChildPipeEnds() {
|
||||
if c.stdoutPipe != nil && c.stdoutPipe.childFD >= 0 {
|
||||
_ = syscall.Close(c.stdoutPipe.childFD)
|
||||
c.stdoutPipe.childFD = -1
|
||||
}
|
||||
if c.stderrPipe != nil && c.stderrPipe.childFD >= 0 {
|
||||
_ = syscall.Close(c.stderrPipe.childFD)
|
||||
c.stderrPipe.childFD = -1
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Command) watchContext() {
|
||||
if c.ctx == nil || c.done == nil || c.Process == nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
_ = c.Process.Kill()
|
||||
case <-c.done:
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func isExecutable(path string) bool {
|
||||
info, err := coreio.Local.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return false
|
||||
}
|
||||
return info.Mode()&0111 != 0
|
||||
}
|
||||
|
||||
func openNull() int {
|
||||
nullOnce.Do(func() {
|
||||
nullFD, nullErr = syscall.Open("/dev/null", syscall.O_RDWR, 0)
|
||||
})
|
||||
if nullErr != nil {
|
||||
return 2
|
||||
}
|
||||
return nullFD
|
||||
}
|
||||
74
linuxkit.go
74
linuxkit.go
|
|
@ -3,15 +3,15 @@ package container
|
|||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
goio "io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/container/internal/proc"
|
||||
)
|
||||
|
||||
// LinuxKitManager implements the Manager interface for LinuxKit VMs.
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -196,11 +196,11 @@ func (m *LinuxKitManager) Run(ctx context.Context, image string, opts RunOptions
|
|||
|
||||
// Copy output to both log and stdout
|
||||
go func() {
|
||||
mw := goio.MultiWriter(logFile, os.Stdout)
|
||||
mw := goio.MultiWriter(logFile, proc.Stdout)
|
||||
_, _ = goio.Copy(mw, stdout)
|
||||
}()
|
||||
go func() {
|
||||
mw := goio.MultiWriter(logFile, os.Stderr)
|
||||
mw := goio.MultiWriter(logFile, proc.Stderr)
|
||||
_, _ = goio.Copy(mw, stderr)
|
||||
}()
|
||||
|
||||
|
|
@ -220,7 +220,7 @@ func (m *LinuxKitManager) Run(ctx context.Context, image string, opts RunOptions
|
|||
}
|
||||
|
||||
// waitForExit monitors a detached process and updates state when it exits.
|
||||
func (m *LinuxKitManager) waitForExit(id string, cmd *exec.Cmd) {
|
||||
func (m *LinuxKitManager) waitForExit(id string, cmd *proc.Command) {
|
||||
err := cmd.Wait()
|
||||
|
||||
container, ok := m.state.Get(id)
|
||||
|
|
@ -249,16 +249,7 @@ func (m *LinuxKitManager) Stop(ctx context.Context, id string) error {
|
|||
}
|
||||
|
||||
// Find the process
|
||||
process, err := os.FindProcess(container.PID)
|
||||
if err != nil {
|
||||
// Process doesn't exist, update state
|
||||
container.Status = StatusStopped
|
||||
_ = m.state.Update(container)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send SIGTERM
|
||||
if err := process.Signal(syscall.SIGTERM); err != nil {
|
||||
if err := syscall.Kill(container.PID, syscall.SIGTERM); err != nil {
|
||||
// Process might already be gone
|
||||
container.Status = StatusStopped
|
||||
_ = m.state.Update(container)
|
||||
|
|
@ -267,28 +258,23 @@ func (m *LinuxKitManager) Stop(ctx context.Context, id string) error {
|
|||
|
||||
// Honour already-cancelled contexts before waiting
|
||||
if err := ctx.Err(); err != nil {
|
||||
_ = process.Signal(syscall.SIGKILL)
|
||||
_ = syscall.Kill(container.PID, syscall.SIGKILL)
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for graceful shutdown with timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
_, _ = process.Wait()
|
||||
close(done)
|
||||
}()
|
||||
deadline := time.After(10 * time.Second)
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Process exited gracefully
|
||||
case <-time.After(10 * time.Second):
|
||||
// Force kill
|
||||
_ = process.Signal(syscall.SIGKILL)
|
||||
<-done
|
||||
case <-ctx.Done():
|
||||
// Context cancelled
|
||||
_ = process.Signal(syscall.SIGKILL)
|
||||
return ctx.Err()
|
||||
for isProcessRunning(container.PID) {
|
||||
select {
|
||||
case <-deadline:
|
||||
_ = syscall.Kill(container.PID, syscall.SIGKILL)
|
||||
case <-ctx.Done():
|
||||
_ = syscall.Kill(container.PID, syscall.SIGKILL)
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
|
||||
container.Status = StatusStopped
|
||||
|
|
@ -317,14 +303,10 @@ func (m *LinuxKitManager) List(ctx context.Context) ([]*Container, error) {
|
|||
|
||||
// isProcessRunning checks if a process with the given PID is still running.
|
||||
func isProcessRunning(pid int) bool {
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
if pid <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// On Unix, FindProcess always succeeds, so we need to send signal 0 to check
|
||||
err = process.Signal(syscall.Signal(0))
|
||||
return err == nil
|
||||
return syscall.Kill(pid, syscall.Signal(0)) == nil
|
||||
}
|
||||
|
||||
// Logs returns a reader for the container's log output.
|
||||
|
|
@ -436,7 +418,7 @@ func (m *LinuxKitManager) Exec(ctx context.Context, id string, cmd []string) err
|
|||
|
||||
// Build SSH command
|
||||
sshArgs := []string{
|
||||
"-p", fmt.Sprintf("%d", sshPort),
|
||||
"-p", core.Sprintf("%d", sshPort),
|
||||
"-o", "StrictHostKeyChecking=yes",
|
||||
"-o", "UserKnownHostsFile=~/.core/known_hosts",
|
||||
"-o", "LogLevel=ERROR",
|
||||
|
|
@ -444,10 +426,10 @@ func (m *LinuxKitManager) Exec(ctx context.Context, id string, cmd []string) err
|
|||
}
|
||||
sshArgs = append(sshArgs, cmd...)
|
||||
|
||||
sshCmd := exec.CommandContext(ctx, "ssh", sshArgs...)
|
||||
sshCmd.Stdin = os.Stdin
|
||||
sshCmd.Stdout = os.Stdout
|
||||
sshCmd.Stderr = os.Stderr
|
||||
sshCmd := proc.NewCommandContext(ctx, "ssh", sshArgs...)
|
||||
sshCmd.Stdin = proc.Stdin
|
||||
sshCmd.Stdout = proc.Stdout
|
||||
sshCmd.Stderr = proc.Stderr
|
||||
|
||||
return sshCmd.Run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ package container
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
"dappco.re/go/core/container/internal/proc"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -39,30 +40,30 @@ func (m *MockHypervisor) Available() bool {
|
|||
return m.available
|
||||
}
|
||||
|
||||
func (m *MockHypervisor) BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*exec.Cmd, error) {
|
||||
func (m *MockHypervisor) BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*proc.Command, error) {
|
||||
m.lastImage = image
|
||||
m.lastOpts = opts
|
||||
if m.buildErr != nil {
|
||||
return nil, m.buildErr
|
||||
}
|
||||
// Return a simple command that exits quickly
|
||||
return exec.CommandContext(ctx, m.commandToRun, "test"), nil
|
||||
return proc.NewCommandContext(ctx, m.commandToRun, "test"), nil
|
||||
}
|
||||
|
||||
// newTestManager creates a LinuxKitManager with mock hypervisor for testing.
|
||||
// Uses manual temp directory management to avoid race conditions with t.TempDir cleanup.
|
||||
func newTestManager(t *testing.T) (*LinuxKitManager, *MockHypervisor, string) {
|
||||
tmpDir, err := os.MkdirTemp("", "linuxkit-test-*")
|
||||
tmpDir, err := coreutil.MkdirTemp("linuxkit-test-")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Manual cleanup that handles race conditions with state file writes
|
||||
t.Cleanup(func() {
|
||||
// Give any pending file operations time to complete
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
_ = os.RemoveAll(tmpDir)
|
||||
_ = io.Local.DeleteAll(tmpDir)
|
||||
})
|
||||
|
||||
statePath := filepath.Join(tmpDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tmpDir, "containers.json")
|
||||
|
||||
state, err := LoadState(statePath)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -75,7 +76,7 @@ func newTestManager(t *testing.T) (*LinuxKitManager, *MockHypervisor, string) {
|
|||
|
||||
func TestNewLinuxKitManagerWithHypervisor_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
statePath := filepath.Join(tmpDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tmpDir, "containers.json")
|
||||
state, _ := LoadState(statePath)
|
||||
mock := NewMockHypervisor()
|
||||
|
||||
|
|
@ -90,8 +91,8 @@ func TestLinuxKitManager_Run_Good_Detached(t *testing.T) {
|
|||
manager, mock, tmpDir := newTestManager(t)
|
||||
|
||||
// Create a test image file
|
||||
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||
imagePath := coreutil.JoinPath(tmpDir, "test.iso")
|
||||
err := io.Local.Write(imagePath, "fake image")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use a command that runs briefly then exits
|
||||
|
|
@ -128,8 +129,8 @@ func TestLinuxKitManager_Run_Good_Detached(t *testing.T) {
|
|||
func TestLinuxKitManager_Run_Good_DefaultValues(t *testing.T) {
|
||||
manager, mock, tmpDir := newTestManager(t)
|
||||
|
||||
imagePath := filepath.Join(tmpDir, "test.qcow2")
|
||||
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||
imagePath := coreutil.JoinPath(tmpDir, "test.qcow2")
|
||||
err := io.Local.Write(imagePath, "fake image")
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -164,8 +165,8 @@ func TestLinuxKitManager_Run_Bad_ImageNotFound(t *testing.T) {
|
|||
func TestLinuxKitManager_Run_Bad_UnsupportedFormat(t *testing.T) {
|
||||
manager, _, tmpDir := newTestManager(t)
|
||||
|
||||
imagePath := filepath.Join(tmpDir, "test.txt")
|
||||
err := os.WriteFile(imagePath, []byte("not an image"), 0644)
|
||||
imagePath := coreutil.JoinPath(tmpDir, "test.txt")
|
||||
err := io.Local.Write(imagePath, "not an image")
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -213,7 +214,7 @@ func TestLinuxKitManager_Stop_Bad_NotFound(t *testing.T) {
|
|||
|
||||
func TestLinuxKitManager_Stop_Bad_NotRunning(t *testing.T) {
|
||||
_, _, tmpDir := newTestManager(t)
|
||||
statePath := filepath.Join(tmpDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tmpDir, "containers.json")
|
||||
state, err := LoadState(statePath)
|
||||
require.NoError(t, err)
|
||||
manager := NewLinuxKitManagerWithHypervisor(io.Local, state, NewMockHypervisor())
|
||||
|
|
@ -233,7 +234,7 @@ func TestLinuxKitManager_Stop_Bad_NotRunning(t *testing.T) {
|
|||
|
||||
func TestLinuxKitManager_List_Good(t *testing.T) {
|
||||
_, _, tmpDir := newTestManager(t)
|
||||
statePath := filepath.Join(tmpDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tmpDir, "containers.json")
|
||||
state, err := LoadState(statePath)
|
||||
require.NoError(t, err)
|
||||
manager := NewLinuxKitManagerWithHypervisor(io.Local, state, NewMockHypervisor())
|
||||
|
|
@ -250,7 +251,7 @@ func TestLinuxKitManager_List_Good(t *testing.T) {
|
|||
|
||||
func TestLinuxKitManager_List_Good_VerifiesRunningStatus(t *testing.T) {
|
||||
_, _, tmpDir := newTestManager(t)
|
||||
statePath := filepath.Join(tmpDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tmpDir, "containers.json")
|
||||
state, err := LoadState(statePath)
|
||||
require.NoError(t, err)
|
||||
manager := NewLinuxKitManagerWithHypervisor(io.Local, state, NewMockHypervisor())
|
||||
|
|
@ -275,8 +276,8 @@ func TestLinuxKitManager_Logs_Good(t *testing.T) {
|
|||
manager, _, tmpDir := newTestManager(t)
|
||||
|
||||
// Create a log file manually
|
||||
logsDir := filepath.Join(tmpDir, "logs")
|
||||
require.NoError(t, os.MkdirAll(logsDir, 0755))
|
||||
logsDir := coreutil.JoinPath(tmpDir, "logs")
|
||||
require.NoError(t, io.Local.EnsureDir(logsDir))
|
||||
|
||||
container := &Container{ID: "abc12345"}
|
||||
_ = manager.State().Add(container)
|
||||
|
|
@ -286,8 +287,8 @@ func TestLinuxKitManager_Logs_Good(t *testing.T) {
|
|||
logContent := "test log content\nline 2\n"
|
||||
logPath, err := LogPath("abc12345")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(logPath), 0755))
|
||||
require.NoError(t, os.WriteFile(logPath, []byte(logContent), 0644))
|
||||
require.NoError(t, io.Local.EnsureDir(core.PathDir(logPath)))
|
||||
require.NoError(t, io.Local.Write(logPath, logContent))
|
||||
|
||||
ctx := context.Background()
|
||||
reader, err := manager.Logs(ctx, "abc12345", false)
|
||||
|
|
@ -438,10 +439,10 @@ func TestLinuxKitManager_Logs_Good_Follow(t *testing.T) {
|
|||
// Create a log file at the expected location
|
||||
logPath, err := LogPath(uniqueID)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(logPath), 0755))
|
||||
require.NoError(t, io.Local.EnsureDir(core.PathDir(logPath)))
|
||||
|
||||
// Write initial content
|
||||
err = os.WriteFile(logPath, []byte("initial log content\n"), 0644)
|
||||
err = io.Local.Write(logPath, "initial log content\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a cancellable context
|
||||
|
|
@ -466,11 +467,11 @@ func TestLinuxKitManager_Logs_Good_Follow(t *testing.T) {
|
|||
|
||||
func TestFollowReader_Read_Good_WithData(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logPath := filepath.Join(tmpDir, "test.log")
|
||||
logPath := coreutil.JoinPath(tmpDir, "test.log")
|
||||
|
||||
// Create log file with content
|
||||
content := "test log line 1\ntest log line 2\n"
|
||||
err := os.WriteFile(logPath, []byte(content), 0644)
|
||||
err := io.Local.Write(logPath, content)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
|
|
@ -481,9 +482,9 @@ func TestFollowReader_Read_Good_WithData(t *testing.T) {
|
|||
defer func() { _ = reader.Close() }()
|
||||
|
||||
// The followReader seeks to end, so we need to append more content
|
||||
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
f, err := io.Local.Append(logPath)
|
||||
require.NoError(t, err)
|
||||
_, err = f.WriteString("new line\n")
|
||||
_, err = f.Write([]byte("new line\n"))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
|
|
@ -499,10 +500,10 @@ func TestFollowReader_Read_Good_WithData(t *testing.T) {
|
|||
|
||||
func TestFollowReader_Read_Good_ContextCancel(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logPath := filepath.Join(tmpDir, "test.log")
|
||||
logPath := coreutil.JoinPath(tmpDir, "test.log")
|
||||
|
||||
// Create log file
|
||||
err := os.WriteFile(logPath, []byte("initial content\n"), 0644)
|
||||
err := io.Local.Write(logPath, "initial content\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
|
@ -523,9 +524,9 @@ func TestFollowReader_Read_Good_ContextCancel(t *testing.T) {
|
|||
|
||||
func TestFollowReader_Close_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logPath := filepath.Join(tmpDir, "test.log")
|
||||
logPath := coreutil.JoinPath(tmpDir, "test.log")
|
||||
|
||||
err := os.WriteFile(logPath, []byte("content\n"), 0644)
|
||||
err := io.Local.Write(logPath, "content\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -552,8 +553,8 @@ func TestLinuxKitManager_Run_Bad_BuildCommandError(t *testing.T) {
|
|||
manager, mock, tmpDir := newTestManager(t)
|
||||
|
||||
// Create a test image file
|
||||
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||
imagePath := coreutil.JoinPath(tmpDir, "test.iso")
|
||||
err := io.Local.Write(imagePath, "fake image")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Configure mock to return an error
|
||||
|
|
@ -571,8 +572,8 @@ func TestLinuxKitManager_Run_Good_Foreground(t *testing.T) {
|
|||
manager, mock, tmpDir := newTestManager(t)
|
||||
|
||||
// Create a test image file
|
||||
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||
imagePath := coreutil.JoinPath(tmpDir, "test.iso")
|
||||
err := io.Local.Write(imagePath, "fake image")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use echo which exits quickly
|
||||
|
|
@ -599,8 +600,8 @@ func TestLinuxKitManager_Stop_Good_ContextCancelled(t *testing.T) {
|
|||
manager, mock, tmpDir := newTestManager(t)
|
||||
|
||||
// Create a test image file
|
||||
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||
imagePath := coreutil.JoinPath(tmpDir, "test.iso")
|
||||
err := io.Local.Write(imagePath, "fake image")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use a command that takes a long time
|
||||
|
|
@ -634,7 +635,7 @@ func TestLinuxKitManager_Stop_Good_ContextCancelled(t *testing.T) {
|
|||
|
||||
func TestIsProcessRunning_Good_ExistingProcess(t *testing.T) {
|
||||
// Use our own PID which definitely exists
|
||||
running := isProcessRunning(os.Getpid())
|
||||
running := isProcessRunning(syscall.Getpid())
|
||||
assert.True(t, running)
|
||||
}
|
||||
|
||||
|
|
@ -647,8 +648,8 @@ func TestIsProcessRunning_Bad_NonexistentProcess(t *testing.T) {
|
|||
func TestLinuxKitManager_Run_Good_WithPortsAndVolumes(t *testing.T) {
|
||||
manager, mock, tmpDir := newTestManager(t)
|
||||
|
||||
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||
imagePath := coreutil.JoinPath(tmpDir, "test.iso")
|
||||
err := io.Local.Write(imagePath, "fake image")
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -675,10 +676,10 @@ func TestLinuxKitManager_Run_Good_WithPortsAndVolumes(t *testing.T) {
|
|||
|
||||
func TestFollowReader_Read_Bad_ReaderError(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logPath := filepath.Join(tmpDir, "test.log")
|
||||
logPath := coreutil.JoinPath(tmpDir, "test.log")
|
||||
|
||||
// Create log file
|
||||
err := os.WriteFile(logPath, []byte("content\n"), 0644)
|
||||
err := io.Local.Write(logPath, "content\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -697,8 +698,8 @@ func TestFollowReader_Read_Bad_ReaderError(t *testing.T) {
|
|||
func TestLinuxKitManager_Run_Bad_StartError(t *testing.T) {
|
||||
manager, mock, tmpDir := newTestManager(t)
|
||||
|
||||
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||
imagePath := coreutil.JoinPath(tmpDir, "test.iso")
|
||||
err := io.Local.Write(imagePath, "fake image")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use a command that doesn't exist to cause Start() to fail
|
||||
|
|
@ -718,8 +719,8 @@ func TestLinuxKitManager_Run_Bad_StartError(t *testing.T) {
|
|||
func TestLinuxKitManager_Run_Bad_ForegroundStartError(t *testing.T) {
|
||||
manager, mock, tmpDir := newTestManager(t)
|
||||
|
||||
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||
imagePath := coreutil.JoinPath(tmpDir, "test.iso")
|
||||
err := io.Local.Write(imagePath, "fake image")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use a command that doesn't exist to cause Start() to fail
|
||||
|
|
@ -739,8 +740,8 @@ func TestLinuxKitManager_Run_Bad_ForegroundStartError(t *testing.T) {
|
|||
func TestLinuxKitManager_Run_Good_ForegroundWithError(t *testing.T) {
|
||||
manager, mock, tmpDir := newTestManager(t)
|
||||
|
||||
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||
imagePath := coreutil.JoinPath(tmpDir, "test.iso")
|
||||
err := io.Local.Write(imagePath, "fake image")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Use a command that exits with error
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ package sources
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
goio "io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
)
|
||||
|
||||
// CDNSource downloads images from a CDN or S3 bucket.
|
||||
|
|
@ -38,7 +38,7 @@ func (s *CDNSource) Available() bool {
|
|||
// LatestVersion fetches version from manifest or returns "latest".
|
||||
func (s *CDNSource) LatestVersion(ctx context.Context) (string, error) {
|
||||
// Try to fetch manifest.json for version info
|
||||
url := fmt.Sprintf("%s/manifest.json", s.config.CDNURL)
|
||||
url := core.Sprintf("%s/manifest.json", s.config.CDNURL)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return "latest", nil
|
||||
|
|
@ -56,7 +56,7 @@ func (s *CDNSource) LatestVersion(ctx context.Context) (string, error) {
|
|||
|
||||
// Download downloads the image from CDN.
|
||||
func (s *CDNSource) Download(ctx context.Context, m io.Medium, dest string, progress func(downloaded, total int64)) error {
|
||||
url := fmt.Sprintf("%s/%s", s.config.CDNURL, s.config.ImageName)
|
||||
url := core.Sprintf("%s/%s", s.config.CDNURL, s.config.ImageName)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
|
|
@ -70,7 +70,7 @@ func (s *CDNSource) Download(ctx context.Context, m io.Medium, dest string, prog
|
|||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return coreerr.E("cdn.Download", fmt.Sprintf("HTTP %d", resp.StatusCode), nil)
|
||||
return coreerr.E("cdn.Download", core.Sprintf("HTTP %d", resp.StatusCode), nil)
|
||||
}
|
||||
|
||||
// Ensure dest directory exists
|
||||
|
|
@ -79,8 +79,8 @@ 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)
|
||||
destPath := coreutil.JoinPath(dest, s.config.ImageName)
|
||||
f, err := m.Create(destPath)
|
||||
if err != nil {
|
||||
return coreerr.E("cdn.Download", "create destination file", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ package sources
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
goio "io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
|
@ -35,7 +35,7 @@ func TestCDNSource_LatestVersion_Good(t *testing.T) {
|
|||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/manifest.json" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = fmt.Fprint(w, `{"version": "1.2.3"}`)
|
||||
_, _ = goio.WriteString(w, `{"version": "1.2.3"}`)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ func TestCDNSource_Download_Good(t *testing.T) {
|
|||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/test.img" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = fmt.Fprint(w, content)
|
||||
_, _ = goio.WriteString(w, content)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
|
@ -80,9 +80,9 @@ func TestCDNSource_Download_Good(t *testing.T) {
|
|||
assert.True(t, progressCalled)
|
||||
|
||||
// Verify file content
|
||||
data, err := os.ReadFile(filepath.Join(dest, imageName))
|
||||
data, err := io.Local.Read(coreutil.JoinPath(dest, imageName))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, content, string(data))
|
||||
assert.Equal(t, content, data)
|
||||
}
|
||||
|
||||
func TestCDNSource_Download_Bad(t *testing.T) {
|
||||
|
|
@ -150,9 +150,9 @@ func TestCDNSource_LatestVersion_Bad_ServerError(t *testing.T) {
|
|||
func TestCDNSource_Download_Good_NoProgress(t *testing.T) {
|
||||
content := "test content"
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(content)))
|
||||
w.Header().Set("Content-Length", core.Sprintf("%d", len(content)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = fmt.Fprint(w, content)
|
||||
_, _ = goio.WriteString(w, content)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
|
@ -166,9 +166,9 @@ func TestCDNSource_Download_Good_NoProgress(t *testing.T) {
|
|||
err := src.Download(context.Background(), io.Local, dest, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(dest, "test.img"))
|
||||
data, err := io.Local.Read(coreutil.JoinPath(dest, "test.img"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, content, string(data))
|
||||
assert.Equal(t, content, data)
|
||||
}
|
||||
|
||||
func TestCDNSource_Download_Good_LargeFile(t *testing.T) {
|
||||
|
|
@ -179,7 +179,7 @@ func TestCDNSource_Download_Good_LargeFile(t *testing.T) {
|
|||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(content)))
|
||||
w.Header().Set("Content-Length", core.Sprintf("%d", len(content)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(content)
|
||||
}))
|
||||
|
|
@ -230,7 +230,7 @@ func TestCDNSource_Download_Bad_HTTPErrorCodes(t *testing.T) {
|
|||
|
||||
err := src.Download(context.Background(), io.Local, dest, nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), fmt.Sprintf("HTTP %d", tc.statusCode))
|
||||
assert.Contains(t, err.Error(), core.Sprintf("HTTP %d", tc.statusCode))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -269,12 +269,12 @@ func TestCDNSource_Download_Good_CreatesDestDir(t *testing.T) {
|
|||
content := "test content"
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = fmt.Fprint(w, content)
|
||||
_, _ = goio.WriteString(w, content)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
dest := filepath.Join(tmpDir, "nested", "dir")
|
||||
dest := coreutil.JoinPath(tmpDir, "nested", "dir")
|
||||
// dest doesn't exist yet
|
||||
|
||||
src := NewCDNSource(SourceConfig{
|
||||
|
|
@ -286,7 +286,7 @@ func TestCDNSource_Download_Good_CreatesDestDir(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// Verify nested dir was created
|
||||
info, err := os.Stat(dest)
|
||||
info, err := io.Local.Stat(dest)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, info.IsDir())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ package sources
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/container/internal/proc"
|
||||
)
|
||||
|
||||
// GitHubSource downloads images from GitHub Releases.
|
||||
|
|
@ -30,18 +30,18 @@ func (s *GitHubSource) Name() string {
|
|||
|
||||
// Available checks if gh CLI is installed and authenticated.
|
||||
func (s *GitHubSource) Available() bool {
|
||||
_, err := exec.LookPath("gh")
|
||||
_, err := proc.LookPath("gh")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Check if authenticated
|
||||
cmd := exec.Command("gh", "auth", "status")
|
||||
cmd := proc.NewCommand("gh", "auth", "status")
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
// LatestVersion returns the latest release tag.
|
||||
func (s *GitHubSource) LatestVersion(ctx context.Context) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, "gh", "release", "view",
|
||||
cmd := proc.NewCommandContext(ctx, "gh", "release", "view",
|
||||
"-R", s.config.GitHubRepo,
|
||||
"--json", "tagName",
|
||||
"-q", ".tagName",
|
||||
|
|
@ -50,20 +50,20 @@ func (s *GitHubSource) LatestVersion(ctx context.Context) (string, error) {
|
|||
if err != nil {
|
||||
return "", coreerr.E("github.LatestVersion", "failed", err)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
return core.Trim(string(out)), nil
|
||||
}
|
||||
|
||||
// Download downloads the image from the latest release.
|
||||
func (s *GitHubSource) Download(ctx context.Context, m io.Medium, dest string, progress func(downloaded, total int64)) error {
|
||||
// Get release assets to find our image
|
||||
cmd := exec.CommandContext(ctx, "gh", "release", "download",
|
||||
cmd := proc.NewCommandContext(ctx, "gh", "release", "download",
|
||||
"-R", s.config.GitHubRepo,
|
||||
"-p", s.config.ImageName,
|
||||
"-D", dest,
|
||||
"--clobber",
|
||||
)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = proc.Stdout
|
||||
cmd.Stderr = proc.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return coreerr.E("github.Download", "failed", err)
|
||||
|
|
|
|||
38
state.go
38
state.go
|
|
@ -1,12 +1,13 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"io/fs"
|
||||
"sync"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/io"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
)
|
||||
|
||||
// State manages persistent container state.
|
||||
|
|
@ -20,11 +21,11 @@ type State struct {
|
|||
|
||||
// DefaultStateDir returns the default directory for state files (~/.core).
|
||||
func DefaultStateDir() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
home := coreutil.HomeDir()
|
||||
if home == "" {
|
||||
return "", core.E("DefaultStateDir", "home directory not available", nil)
|
||||
}
|
||||
return filepath.Join(home, ".core"), nil
|
||||
return coreutil.JoinPath(home, ".core"), nil
|
||||
}
|
||||
|
||||
// DefaultStatePath returns the default path for the state file.
|
||||
|
|
@ -33,7 +34,7 @@ func DefaultStatePath() (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, "containers.json"), nil
|
||||
return coreutil.JoinPath(dir, "containers.json"), nil
|
||||
}
|
||||
|
||||
// DefaultLogsDir returns the default directory for container logs.
|
||||
|
|
@ -42,7 +43,7 @@ func DefaultLogsDir() (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, "logs"), nil
|
||||
return coreutil.JoinPath(dir, "logs"), nil
|
||||
}
|
||||
|
||||
// NewState creates a new State instance.
|
||||
|
|
@ -60,14 +61,15 @@ func LoadState(filePath string) (*State, error) {
|
|||
|
||||
dataStr, err := io.Local.Read(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if core.Is(err, fs.ErrNotExist) {
|
||||
return state, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(dataStr), state); err != nil {
|
||||
return nil, err
|
||||
result := core.JSONUnmarshalString(dataStr, state)
|
||||
if !result.OK {
|
||||
return nil, result.Value.(error)
|
||||
}
|
||||
|
||||
return state, nil
|
||||
|
|
@ -79,17 +81,17 @@ func (s *State) SaveState() error {
|
|||
defer s.mu.RUnlock()
|
||||
|
||||
// Ensure the directory exists
|
||||
dir := filepath.Dir(s.filePath)
|
||||
dir := core.PathDir(s.filePath)
|
||||
if err := io.Local.EnsureDir(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(s, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
result := core.JSONMarshal(s)
|
||||
if !result.OK {
|
||||
return result.Value.(error)
|
||||
}
|
||||
|
||||
return io.Local.Write(s.filePath, string(data))
|
||||
return io.Local.Write(s.filePath, string(result.Value.([]byte)))
|
||||
}
|
||||
|
||||
// Add adds a container to the state and persists it.
|
||||
|
|
@ -159,7 +161,7 @@ func LogPath(id string) (string, error) {
|
|||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(logsDir, id+".log"), nil
|
||||
return coreutil.JoinPath(logsDir, core.Concat(id, ".log")), nil
|
||||
}
|
||||
|
||||
// EnsureLogsDir ensures the logs directory exists.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/io"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -21,7 +23,7 @@ func TestNewState_Good(t *testing.T) {
|
|||
func TestLoadState_Good_NewFile(t *testing.T) {
|
||||
// Test loading from non-existent file
|
||||
tmpDir := t.TempDir()
|
||||
statePath := filepath.Join(tmpDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tmpDir, "containers.json")
|
||||
|
||||
state, err := LoadState(statePath)
|
||||
|
||||
|
|
@ -32,7 +34,7 @@ func TestLoadState_Good_NewFile(t *testing.T) {
|
|||
|
||||
func TestLoadState_Good_ExistingFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
statePath := filepath.Join(tmpDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tmpDir, "containers.json")
|
||||
|
||||
// Create a state file with data
|
||||
content := `{
|
||||
|
|
@ -47,7 +49,7 @@ func TestLoadState_Good_ExistingFile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}`
|
||||
err := os.WriteFile(statePath, []byte(content), 0644)
|
||||
err := io.Local.Write(statePath, content)
|
||||
require.NoError(t, err)
|
||||
|
||||
state, err := LoadState(statePath)
|
||||
|
|
@ -63,10 +65,10 @@ func TestLoadState_Good_ExistingFile(t *testing.T) {
|
|||
|
||||
func TestLoadState_Bad_InvalidJSON(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
statePath := filepath.Join(tmpDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tmpDir, "containers.json")
|
||||
|
||||
// Create invalid JSON
|
||||
err := os.WriteFile(statePath, []byte("invalid json{"), 0644)
|
||||
err := io.Local.Write(statePath, "invalid json{")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = LoadState(statePath)
|
||||
|
|
@ -75,7 +77,7 @@ func TestLoadState_Bad_InvalidJSON(t *testing.T) {
|
|||
|
||||
func TestState_Add_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
statePath := filepath.Join(tmpDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tmpDir, "containers.json")
|
||||
state := NewState(statePath)
|
||||
|
||||
container := &Container{
|
||||
|
|
@ -96,13 +98,12 @@ func TestState_Add_Good(t *testing.T) {
|
|||
assert.Equal(t, container.Name, c.Name)
|
||||
|
||||
// Verify file was created
|
||||
_, err = os.Stat(statePath)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, io.Local.IsFile(statePath))
|
||||
}
|
||||
|
||||
func TestState_Update_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
statePath := filepath.Join(tmpDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tmpDir, "containers.json")
|
||||
state := NewState(statePath)
|
||||
|
||||
container := &Container{
|
||||
|
|
@ -124,7 +125,7 @@ func TestState_Update_Good(t *testing.T) {
|
|||
|
||||
func TestState_Remove_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
statePath := filepath.Join(tmpDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tmpDir, "containers.json")
|
||||
state := NewState(statePath)
|
||||
|
||||
container := &Container{
|
||||
|
|
@ -148,7 +149,7 @@ func TestState_Get_Bad_NotFound(t *testing.T) {
|
|||
|
||||
func TestState_All_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
statePath := filepath.Join(tmpDir, "containers.json")
|
||||
statePath := coreutil.JoinPath(tmpDir, "containers.json")
|
||||
state := NewState(statePath)
|
||||
|
||||
_ = state.Add(&Container{ID: "aaa11111"})
|
||||
|
|
@ -161,7 +162,7 @@ func TestState_All_Good(t *testing.T) {
|
|||
|
||||
func TestState_SaveState_Good_CreatesDirectory(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
nestedPath := filepath.Join(tmpDir, "nested", "dir", "containers.json")
|
||||
nestedPath := coreutil.JoinPath(tmpDir, "nested", "dir", "containers.json")
|
||||
state := NewState(nestedPath)
|
||||
|
||||
_ = state.Add(&Container{ID: "abc12345"})
|
||||
|
|
@ -170,8 +171,7 @@ func TestState_SaveState_Good_CreatesDirectory(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Verify directory was created
|
||||
_, err = os.Stat(filepath.Dir(nestedPath))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, io.Local.IsDir(core.PathDir(nestedPath)))
|
||||
}
|
||||
|
||||
func TestDefaultStateDir_Good(t *testing.T) {
|
||||
|
|
@ -204,8 +204,7 @@ func TestEnsureLogsDir_Good(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
logsDir, _ := DefaultLogsDir()
|
||||
_, err = os.Stat(logsDir)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, io.Local.IsDir(logsDir))
|
||||
}
|
||||
|
||||
func TestGenerateID_Good(t *testing.T) {
|
||||
|
|
|
|||
41
templates.go
41
templates.go
|
|
@ -4,14 +4,14 @@ import (
|
|||
"embed"
|
||||
"iter"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
)
|
||||
|
||||
//go:embed templates/*.yml
|
||||
|
|
@ -87,7 +87,7 @@ func GetTemplate(name string) (string, error) {
|
|||
// Check user templates
|
||||
userTemplatesDir := getUserTemplatesDir()
|
||||
if userTemplatesDir != "" {
|
||||
templatePath := filepath.Join(userTemplatesDir, name+".yml")
|
||||
templatePath := coreutil.JoinPath(userTemplatesDir, core.Concat(name, ".yml"))
|
||||
if io.Local.IsFile(templatePath) {
|
||||
content, err := io.Local.Read(templatePath)
|
||||
if err != nil {
|
||||
|
|
@ -158,7 +158,7 @@ func ApplyVariables(content string, vars map[string]string) (string, error) {
|
|||
})
|
||||
|
||||
if len(missingVars) > 0 {
|
||||
return "", coreerr.E("ApplyVariables", "missing required variables: "+strings.Join(missingVars, ", "), nil)
|
||||
return "", coreerr.E("ApplyVariables", core.Concat("missing required variables: ", core.Join(", ", missingVars...)), nil)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
|
@ -206,21 +206,18 @@ func ExtractVariables(content string) (required []string, optional map[string]st
|
|||
// Returns empty string if the directory doesn't exist.
|
||||
func getUserTemplatesDir() string {
|
||||
// Try workspace-relative .core/linuxkit first
|
||||
cwd, err := os.Getwd()
|
||||
if err == nil {
|
||||
wsDir := filepath.Join(cwd, ".core", "linuxkit")
|
||||
if io.Local.IsDir(wsDir) {
|
||||
return wsDir
|
||||
}
|
||||
wsDir := coreutil.JoinPath(coreutil.CurrentDir(), ".core", "linuxkit")
|
||||
if io.Local.IsDir(wsDir) {
|
||||
return wsDir
|
||||
}
|
||||
|
||||
// Try home directory
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
home := coreutil.HomeDir()
|
||||
if home == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
homeDir := filepath.Join(home, ".core", "linuxkit")
|
||||
homeDir := coreutil.JoinPath(home, ".core", "linuxkit")
|
||||
if io.Local.IsDir(homeDir) {
|
||||
return homeDir
|
||||
}
|
||||
|
|
@ -243,12 +240,12 @@ func scanUserTemplates(dir string) []Template {
|
|||
}
|
||||
|
||||
name := entry.Name()
|
||||
if !strings.HasSuffix(name, ".yml") && !strings.HasSuffix(name, ".yaml") {
|
||||
if !core.HasSuffix(name, ".yml") && !core.HasSuffix(name, ".yaml") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract template name from filename
|
||||
templateName := strings.TrimSuffix(strings.TrimSuffix(name, ".yml"), ".yaml")
|
||||
templateName := core.TrimSuffix(core.TrimSuffix(name, ".yml"), ".yaml")
|
||||
|
||||
// Skip if this is a builtin template name (embedded takes precedence)
|
||||
isBuiltin := false
|
||||
|
|
@ -263,7 +260,7 @@ func scanUserTemplates(dir string) []Template {
|
|||
}
|
||||
|
||||
// Read file to extract description from comments
|
||||
description := extractTemplateDescription(filepath.Join(dir, name))
|
||||
description := extractTemplateDescription(coreutil.JoinPath(dir, name))
|
||||
if description == "" {
|
||||
description = "User-defined template"
|
||||
}
|
||||
|
|
@ -271,7 +268,7 @@ func scanUserTemplates(dir string) []Template {
|
|||
templates = append(templates, Template{
|
||||
Name: templateName,
|
||||
Description: description,
|
||||
Path: filepath.Join(dir, name),
|
||||
Path: coreutil.JoinPath(dir, name),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -286,14 +283,14 @@ func extractTemplateDescription(path string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
lines := core.Split(content, "\n")
|
||||
var descLines []string
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
trimmed := core.Trim(line)
|
||||
if core.HasPrefix(trimmed, "#") {
|
||||
// Remove the # and trim
|
||||
comment := strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
||||
comment := core.Trim(core.TrimPrefix(trimmed, "#"))
|
||||
if comment != "" {
|
||||
descLines = append(descLines, comment)
|
||||
// Only take the first meaningful comment line as description
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
core "dappco.re/go/core"
|
||||
"dappco.re/go/core/io"
|
||||
|
||||
"dappco.re/go/core/container/internal/coreutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -158,7 +159,7 @@ func TestApplyVariables_Bad_MultipleMissing(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "missing required variables")
|
||||
// Should mention both missing vars
|
||||
errStr := err.Error()
|
||||
assert.True(t, strings.Contains(errStr, "VAR1") || strings.Contains(errStr, "VAR3"))
|
||||
assert.True(t, core.Contains(errStr, "VAR1") || core.Contains(errStr, "VAR3"))
|
||||
}
|
||||
|
||||
func TestApplyTemplate_Good(t *testing.T) {
|
||||
|
|
@ -248,11 +249,11 @@ func TestScanUserTemplates_Good(t *testing.T) {
|
|||
kernel:
|
||||
image: linuxkit/kernel:6.6
|
||||
`
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "custom.yml"), []byte(templateContent), 0644)
|
||||
err := io.Local.Write(coreutil.JoinPath(tmpDir, "custom.yml"), templateContent)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a non-template file (should be ignored)
|
||||
err = os.WriteFile(filepath.Join(tmpDir, "readme.txt"), []byte("Not a template"), 0644)
|
||||
err = io.Local.Write(coreutil.JoinPath(tmpDir, "readme.txt"), "Not a template")
|
||||
require.NoError(t, err)
|
||||
|
||||
templates := scanUserTemplates(tmpDir)
|
||||
|
|
@ -266,9 +267,9 @@ func TestScanUserTemplates_Good_MultipleTemplates(t *testing.T) {
|
|||
tmpDir := t.TempDir()
|
||||
|
||||
// Create multiple template files
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "web.yml"), []byte("# Web Server\nkernel:"), 0644)
|
||||
err := io.Local.Write(coreutil.JoinPath(tmpDir, "web.yml"), "# Web Server\nkernel:")
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(tmpDir, "db.yaml"), []byte("# Database Server\nkernel:"), 0644)
|
||||
err = io.Local.Write(coreutil.JoinPath(tmpDir, "db.yaml"), "# Database Server\nkernel:")
|
||||
require.NoError(t, err)
|
||||
|
||||
templates := scanUserTemplates(tmpDir)
|
||||
|
|
@ -300,14 +301,14 @@ func TestScanUserTemplates_Bad_NonexistentDirectory(t *testing.T) {
|
|||
|
||||
func TestExtractTemplateDescription_Good(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "test.yml")
|
||||
path := coreutil.JoinPath(tmpDir, "test.yml")
|
||||
|
||||
content := `# My Template Description
|
||||
# More details here
|
||||
kernel:
|
||||
image: test
|
||||
`
|
||||
err := os.WriteFile(path, []byte(content), 0644)
|
||||
err := io.Local.Write(path, content)
|
||||
require.NoError(t, err)
|
||||
|
||||
desc := extractTemplateDescription(path)
|
||||
|
|
@ -317,12 +318,12 @@ kernel:
|
|||
|
||||
func TestExtractTemplateDescription_Good_NoComments(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "test.yml")
|
||||
path := coreutil.JoinPath(tmpDir, "test.yml")
|
||||
|
||||
content := `kernel:
|
||||
image: test
|
||||
`
|
||||
err := os.WriteFile(path, []byte(content), 0644)
|
||||
err := io.Local.Write(path, content)
|
||||
require.NoError(t, err)
|
||||
|
||||
desc := extractTemplateDescription(path)
|
||||
|
|
@ -388,11 +389,11 @@ func TestScanUserTemplates_Good_SkipsBuiltinNames(t *testing.T) {
|
|||
tmpDir := t.TempDir()
|
||||
|
||||
// Create a template with a builtin name (should be skipped)
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "core-dev.yml"), []byte("# Duplicate\nkernel:"), 0644)
|
||||
err := io.Local.Write(coreutil.JoinPath(tmpDir, "core-dev.yml"), "# Duplicate\nkernel:")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a unique template
|
||||
err = os.WriteFile(filepath.Join(tmpDir, "unique.yml"), []byte("# Unique\nkernel:"), 0644)
|
||||
err = io.Local.Write(coreutil.JoinPath(tmpDir, "unique.yml"), "# Unique\nkernel:")
|
||||
require.NoError(t, err)
|
||||
|
||||
templates := scanUserTemplates(tmpDir)
|
||||
|
|
@ -406,11 +407,11 @@ func TestScanUserTemplates_Good_SkipsDirectories(t *testing.T) {
|
|||
tmpDir := t.TempDir()
|
||||
|
||||
// Create a subdirectory (should be skipped)
|
||||
err := os.MkdirAll(filepath.Join(tmpDir, "subdir"), 0755)
|
||||
err := io.Local.EnsureDir(coreutil.JoinPath(tmpDir, "subdir"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a valid template
|
||||
err = os.WriteFile(filepath.Join(tmpDir, "valid.yml"), []byte("# Valid\nkernel:"), 0644)
|
||||
err = io.Local.Write(coreutil.JoinPath(tmpDir, "valid.yml"), "# Valid\nkernel:")
|
||||
require.NoError(t, err)
|
||||
|
||||
templates := scanUserTemplates(tmpDir)
|
||||
|
|
@ -423,9 +424,9 @@ func TestScanUserTemplates_Good_YamlExtension(t *testing.T) {
|
|||
tmpDir := t.TempDir()
|
||||
|
||||
// Create templates with both extensions
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "template1.yml"), []byte("# Template 1\nkernel:"), 0644)
|
||||
err := io.Local.Write(coreutil.JoinPath(tmpDir, "template1.yml"), "# Template 1\nkernel:")
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(tmpDir, "template2.yaml"), []byte("# Template 2\nkernel:"), 0644)
|
||||
err = io.Local.Write(coreutil.JoinPath(tmpDir, "template2.yaml"), "# Template 2\nkernel:")
|
||||
require.NoError(t, err)
|
||||
|
||||
templates := scanUserTemplates(tmpDir)
|
||||
|
|
@ -442,7 +443,7 @@ func TestScanUserTemplates_Good_YamlExtension(t *testing.T) {
|
|||
|
||||
func TestExtractTemplateDescription_Good_EmptyComment(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "test.yml")
|
||||
path := coreutil.JoinPath(tmpDir, "test.yml")
|
||||
|
||||
// First comment is empty, second has content
|
||||
content := `#
|
||||
|
|
@ -450,7 +451,7 @@ func TestExtractTemplateDescription_Good_EmptyComment(t *testing.T) {
|
|||
kernel:
|
||||
image: test
|
||||
`
|
||||
err := os.WriteFile(path, []byte(content), 0644)
|
||||
err := io.Local.Write(path, content)
|
||||
require.NoError(t, err)
|
||||
|
||||
desc := extractTemplateDescription(path)
|
||||
|
|
@ -460,7 +461,7 @@ kernel:
|
|||
|
||||
func TestExtractTemplateDescription_Good_MultipleEmptyComments(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "test.yml")
|
||||
path := coreutil.JoinPath(tmpDir, "test.yml")
|
||||
|
||||
// Multiple empty comments before actual content
|
||||
content := `#
|
||||
|
|
@ -470,7 +471,7 @@ func TestExtractTemplateDescription_Good_MultipleEmptyComments(t *testing.T) {
|
|||
kernel:
|
||||
image: test
|
||||
`
|
||||
err := os.WriteFile(path, []byte(content), 0644)
|
||||
err := io.Local.Write(path, content)
|
||||
require.NoError(t, err)
|
||||
|
||||
desc := extractTemplateDescription(path)
|
||||
|
|
@ -485,7 +486,7 @@ func TestScanUserTemplates_Good_DefaultDescription(t *testing.T) {
|
|||
content := `kernel:
|
||||
image: test
|
||||
`
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "nocomment.yml"), []byte(content), 0644)
|
||||
err := io.Local.Write(coreutil.JoinPath(tmpDir, "nocomment.yml"), content)
|
||||
require.NoError(t, err)
|
||||
|
||||
templates := scanUserTemplates(tmpDir)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue