- Move cmd/core/cmd/* to cmd/* (flatten directory structure) - Update module path from github.com/host-uk/core/cmd/core to github.com/host-uk/core - Remove go.mod files from pkg/* (single module now) - Simplify pkg/mcp to file operations only (no GUI deps) - GUI features (display, webview, process) stay in core-gui/pkg/mcp - Fix import aliases (sdkpkg) for package name conflicts - Remove old backup directory (cmdbk) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
370 lines
9.9 KiB
Go
370 lines
9.9 KiB
Go
// Package vm provides LinuxKit VM management commands.
|
|
package vm
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"text/tabwriter"
|
|
"time"
|
|
|
|
"github.com/host-uk/core/pkg/container"
|
|
"github.com/leaanthony/clir"
|
|
)
|
|
|
|
// AddContainerCommands adds container-related commands under 'vm' to the CLI.
|
|
func AddContainerCommands(parent *clir.Cli) {
|
|
vmCmd := parent.NewSubCommand("vm", "LinuxKit VM management")
|
|
vmCmd.LongDescription("Manage LinuxKit virtual machines.\n\n" +
|
|
"LinuxKit VMs are lightweight, immutable VMs built from YAML templates.\n" +
|
|
"They run using qemu or hyperkit depending on your system.\n\n" +
|
|
"Commands:\n" +
|
|
" run Run a VM from image or template\n" +
|
|
" ps List running VMs\n" +
|
|
" stop Stop a running VM\n" +
|
|
" logs View VM logs\n" +
|
|
" exec Execute command in VM\n" +
|
|
" templates Manage LinuxKit templates")
|
|
|
|
addVMRunCommand(vmCmd)
|
|
addVMPsCommand(vmCmd)
|
|
addVMStopCommand(vmCmd)
|
|
addVMLogsCommand(vmCmd)
|
|
addVMExecCommand(vmCmd)
|
|
addVMTemplatesCommand(vmCmd)
|
|
}
|
|
|
|
// addVMRunCommand adds the 'run' command under vm.
|
|
func addVMRunCommand(parent *clir.Command) {
|
|
var (
|
|
name string
|
|
detach bool
|
|
memory int
|
|
cpus int
|
|
sshPort int
|
|
templateName string
|
|
varFlags []string
|
|
)
|
|
|
|
runCmd := parent.NewSubCommand("run", "Run a LinuxKit image or template")
|
|
runCmd.LongDescription("Runs a LinuxKit image as a VM using the available hypervisor.\n\n" +
|
|
"Supported image formats: .iso, .qcow2, .vmdk, .raw\n\n" +
|
|
"You can also run from a template using --template, which will build and run\n" +
|
|
"the image automatically. Use --var to set template variables.\n\n" +
|
|
"Examples:\n" +
|
|
" core vm run image.iso\n" +
|
|
" core vm run -d image.qcow2\n" +
|
|
" core vm run --name myvm --memory 2048 --cpus 4 image.iso\n" +
|
|
" core vm run --template core-dev --var SSH_KEY=\"ssh-rsa AAAA...\"\n" +
|
|
" core vm run --template server-php --var SSH_KEY=\"...\" --var DOMAIN=example.com")
|
|
|
|
runCmd.StringFlag("name", "Name for the container", &name)
|
|
runCmd.BoolFlag("d", "Run in detached mode (background)", &detach)
|
|
runCmd.IntFlag("memory", "Memory in MB (default: 1024)", &memory)
|
|
runCmd.IntFlag("cpus", "Number of CPUs (default: 1)", &cpus)
|
|
runCmd.IntFlag("ssh-port", "SSH port for exec commands (default: 2222)", &sshPort)
|
|
runCmd.StringFlag("template", "Run from a LinuxKit template (build + run)", &templateName)
|
|
runCmd.StringsFlag("var", "Template variable in KEY=VALUE format (can be repeated)", &varFlags)
|
|
|
|
runCmd.Action(func() error {
|
|
opts := container.RunOptions{
|
|
Name: name,
|
|
Detach: detach,
|
|
Memory: memory,
|
|
CPUs: cpus,
|
|
SSHPort: sshPort,
|
|
}
|
|
|
|
// If template is specified, build and run from template
|
|
if templateName != "" {
|
|
vars := ParseVarFlags(varFlags)
|
|
return RunFromTemplate(templateName, vars, opts)
|
|
}
|
|
|
|
// Otherwise, require an image path
|
|
args := runCmd.OtherArgs()
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("image path is required (or use --template)")
|
|
}
|
|
image := args[0]
|
|
|
|
return runContainer(image, name, detach, memory, cpus, sshPort)
|
|
})
|
|
}
|
|
|
|
func runContainer(image, name string, detach bool, memory, cpus, sshPort int) error {
|
|
manager, err := container.NewLinuxKitManager()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize container manager: %w", err)
|
|
}
|
|
|
|
opts := container.RunOptions{
|
|
Name: name,
|
|
Detach: detach,
|
|
Memory: memory,
|
|
CPUs: cpus,
|
|
SSHPort: sshPort,
|
|
}
|
|
|
|
fmt.Printf("%s %s\n", dimStyle.Render("Image:"), image)
|
|
if name != "" {
|
|
fmt.Printf("%s %s\n", dimStyle.Render("Name:"), name)
|
|
}
|
|
fmt.Printf("%s %s\n", dimStyle.Render("Hypervisor:"), manager.Hypervisor().Name())
|
|
fmt.Println()
|
|
|
|
ctx := context.Background()
|
|
c, err := manager.Run(ctx, image, opts)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to run container: %w", err)
|
|
}
|
|
|
|
if detach {
|
|
fmt.Printf("%s %s\n", successStyle.Render("Started:"), c.ID)
|
|
fmt.Printf("%s %d\n", dimStyle.Render("PID:"), c.PID)
|
|
fmt.Println()
|
|
fmt.Printf("Use 'core vm logs %s' to view output\n", c.ID[:8])
|
|
fmt.Printf("Use 'core vm stop %s' to stop\n", c.ID[:8])
|
|
} else {
|
|
fmt.Printf("\n%s %s\n", dimStyle.Render("Container stopped:"), c.ID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// addVMPsCommand adds the 'ps' command under vm.
|
|
func addVMPsCommand(parent *clir.Command) {
|
|
var all bool
|
|
|
|
psCmd := parent.NewSubCommand("ps", "List running VMs")
|
|
psCmd.LongDescription("Lists all VMs. By default, only shows running VMs.\n\n" +
|
|
"Examples:\n" +
|
|
" core vm ps\n" +
|
|
" core vm ps -a")
|
|
|
|
psCmd.BoolFlag("a", "Show all containers (including stopped)", &all)
|
|
|
|
psCmd.Action(func() error {
|
|
return listContainers(all)
|
|
})
|
|
}
|
|
|
|
func listContainers(all bool) error {
|
|
manager, err := container.NewLinuxKitManager()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize container manager: %w", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
containers, err := manager.List(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list containers: %w", err)
|
|
}
|
|
|
|
// Filter if not showing all
|
|
if !all {
|
|
filtered := make([]*container.Container, 0)
|
|
for _, c := range containers {
|
|
if c.Status == container.StatusRunning {
|
|
filtered = append(filtered, c)
|
|
}
|
|
}
|
|
containers = filtered
|
|
}
|
|
|
|
if len(containers) == 0 {
|
|
if all {
|
|
fmt.Println("No containers")
|
|
} else {
|
|
fmt.Println("No running containers")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(w, "ID\tNAME\tIMAGE\tSTATUS\tSTARTED\tPID")
|
|
fmt.Fprintln(w, "--\t----\t-----\t------\t-------\t---")
|
|
|
|
for _, c := range containers {
|
|
// Shorten image path
|
|
imageName := c.Image
|
|
if len(imageName) > 30 {
|
|
imageName = "..." + imageName[len(imageName)-27:]
|
|
}
|
|
|
|
// Format duration
|
|
duration := formatDuration(time.Since(c.StartedAt))
|
|
|
|
// Status with color
|
|
status := string(c.Status)
|
|
switch c.Status {
|
|
case container.StatusRunning:
|
|
status = successStyle.Render(status)
|
|
case container.StatusStopped:
|
|
status = dimStyle.Render(status)
|
|
case container.StatusError:
|
|
status = errorStyle.Render(status)
|
|
}
|
|
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%d\n",
|
|
c.ID[:8], c.Name, imageName, status, duration, c.PID)
|
|
}
|
|
|
|
w.Flush()
|
|
return nil
|
|
}
|
|
|
|
func formatDuration(d time.Duration) string {
|
|
if d < time.Minute {
|
|
return fmt.Sprintf("%ds", int(d.Seconds()))
|
|
}
|
|
if d < time.Hour {
|
|
return fmt.Sprintf("%dm", int(d.Minutes()))
|
|
}
|
|
if d < 24*time.Hour {
|
|
return fmt.Sprintf("%dh", int(d.Hours()))
|
|
}
|
|
return fmt.Sprintf("%dd", int(d.Hours()/24))
|
|
}
|
|
|
|
// addVMStopCommand adds the 'stop' command under vm.
|
|
func addVMStopCommand(parent *clir.Command) {
|
|
stopCmd := parent.NewSubCommand("stop", "Stop a running VM")
|
|
stopCmd.LongDescription("Stops a running VM by ID.\n\n" +
|
|
"Examples:\n" +
|
|
" core vm stop abc12345\n" +
|
|
" core vm stop abc1")
|
|
|
|
stopCmd.Action(func() error {
|
|
args := stopCmd.OtherArgs()
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("container ID is required")
|
|
}
|
|
return stopContainer(args[0])
|
|
})
|
|
}
|
|
|
|
func stopContainer(id string) error {
|
|
manager, err := container.NewLinuxKitManager()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize container manager: %w", err)
|
|
}
|
|
|
|
// Support partial ID matching
|
|
fullID, err := resolveContainerID(manager, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("%s %s\n", dimStyle.Render("Stopping:"), fullID[:8])
|
|
|
|
ctx := context.Background()
|
|
if err := manager.Stop(ctx, fullID); err != nil {
|
|
return fmt.Errorf("failed to stop container: %w", err)
|
|
}
|
|
|
|
fmt.Printf("%s\n", successStyle.Render("Stopped"))
|
|
return nil
|
|
}
|
|
|
|
// resolveContainerID resolves a partial ID to a full ID.
|
|
func resolveContainerID(manager *container.LinuxKitManager, partialID string) (string, error) {
|
|
ctx := context.Background()
|
|
containers, err := manager.List(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var matches []*container.Container
|
|
for _, c := range containers {
|
|
if strings.HasPrefix(c.ID, partialID) || strings.HasPrefix(c.Name, partialID) {
|
|
matches = append(matches, c)
|
|
}
|
|
}
|
|
|
|
switch len(matches) {
|
|
case 0:
|
|
return "", fmt.Errorf("no container found matching: %s", partialID)
|
|
case 1:
|
|
return matches[0].ID, nil
|
|
default:
|
|
return "", fmt.Errorf("multiple containers match '%s', be more specific", partialID)
|
|
}
|
|
}
|
|
|
|
// addVMLogsCommand adds the 'logs' command under vm.
|
|
func addVMLogsCommand(parent *clir.Command) {
|
|
var follow bool
|
|
|
|
logsCmd := parent.NewSubCommand("logs", "View VM logs")
|
|
logsCmd.LongDescription("View logs from a VM.\n\n" +
|
|
"Examples:\n" +
|
|
" core vm logs abc12345\n" +
|
|
" core vm logs -f abc1")
|
|
|
|
logsCmd.BoolFlag("f", "Follow log output", &follow)
|
|
|
|
logsCmd.Action(func() error {
|
|
args := logsCmd.OtherArgs()
|
|
if len(args) == 0 {
|
|
return fmt.Errorf("container ID is required")
|
|
}
|
|
return viewLogs(args[0], follow)
|
|
})
|
|
}
|
|
|
|
func viewLogs(id string, follow bool) error {
|
|
manager, err := container.NewLinuxKitManager()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize container manager: %w", err)
|
|
}
|
|
|
|
fullID, err := resolveContainerID(manager, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := context.Background()
|
|
reader, err := manager.Logs(ctx, fullID, follow)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get logs: %w", err)
|
|
}
|
|
defer reader.Close()
|
|
|
|
_, err = io.Copy(os.Stdout, reader)
|
|
return err
|
|
}
|
|
|
|
// addVMExecCommand adds the 'exec' command under vm.
|
|
func addVMExecCommand(parent *clir.Command) {
|
|
execCmd := parent.NewSubCommand("exec", "Execute a command in a VM")
|
|
execCmd.LongDescription("Execute a command inside a running VM via SSH.\n\n" +
|
|
"Examples:\n" +
|
|
" core vm exec abc12345 ls -la\n" +
|
|
" core vm exec abc1 /bin/sh")
|
|
|
|
execCmd.Action(func() error {
|
|
args := execCmd.OtherArgs()
|
|
if len(args) < 2 {
|
|
return fmt.Errorf("container ID and command are required")
|
|
}
|
|
return execInContainer(args[0], args[1:])
|
|
})
|
|
}
|
|
|
|
func execInContainer(id string, cmd []string) error {
|
|
manager, err := container.NewLinuxKitManager()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize container manager: %w", err)
|
|
}
|
|
|
|
fullID, err := resolveContainerID(manager, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := context.Background()
|
|
return manager.Exec(ctx, fullID, cmd)
|
|
}
|