Skip to content

Commit 3aa48ef

Browse files
author
architectureman
committed
feat: add native Linux mode (vmType=native) — bypass Lima/QEMU on Linux hosts
Add vmType 'native' that runs container runtimes (Docker, containerd, Incus, K3s) directly on the Linux host without any virtualization layer. Key changes: - New environment/vm/native/ package: VM interface via direct host execution - Config validation: native type auto-detected on Linux, skips VM-specific checks - App factory: NewWithVMType() solves config-before-VM-creation ordering - model/guest.go: centralized newGuest() replaces 13x hardcoded lima.New() - Command guards: skip limautil calls when in native mode - Docker daemon: fallback from host.lima.internal to ip route for gateway IP - Docker context: use /var/run/docker.sock directly in native mode - K3s/Kubeconfig: use host IP and primary interface - Incus: guard DiskProvisioned/MountPoint for native - colima model: blocked in native mode (GPU passthrough requires krunkit VM) Tested: - Cross-compiled GOOS=linux GOARCH=amd64: OK - macOS regression GOOS=darwin GOARCH=arm64: OK - go vet: OK - E2E in Colima VM: start → status → stop → delete lifecycle verified
1 parent 4546b6c commit 3aa48ef

25 files changed

Lines changed: 666 additions & 61 deletions

File tree

app/app.go

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/abiosoft/colima/environment/host"
2222
"github.com/abiosoft/colima/environment/vm/lima"
2323
"github.com/abiosoft/colima/environment/vm/lima/limautil"
24+
"github.com/abiosoft/colima/environment/vm/native"
2425
"github.com/abiosoft/colima/store"
2526
"github.com/abiosoft/colima/util"
2627
"github.com/docker/go-units"
@@ -42,11 +43,32 @@ type App interface {
4243

4344
var _ App = (*colimaApp)(nil)
4445

45-
// New creates a new app.
46+
// New creates a new app using the saved instance's VM type.
4647
func New() (App, error) {
47-
guest := lima.New(host.New())
48-
if err := host.IsInstalled(guest); err != nil {
49-
return nil, fmt.Errorf("dependency check failed for VM: %w", err)
48+
return NewWithVMType("")
49+
}
50+
51+
// NewWithVMType creates a new app with the specified VM type.
52+
// If vmType is empty, it loads from saved instance state or uses the default.
53+
func NewWithVMType(vmType string) (App, error) {
54+
h := host.New()
55+
56+
if vmType == "" {
57+
if conf, err := configmanager.LoadInstance(); err == nil && conf.VMType != "" {
58+
vmType = conf.VMType
59+
} else {
60+
vmType = environment.DefaultVMType()
61+
}
62+
}
63+
64+
var guest environment.VM
65+
if vmType == "native" && util.Linux() {
66+
guest = native.New(h)
67+
} else {
68+
guest = lima.New(h)
69+
if err := host.IsInstalled(guest); err != nil {
70+
return nil, fmt.Errorf("dependency check failed for VM: %w", err)
71+
}
5072
}
5173

5274
return &colimaApp{
@@ -281,9 +303,12 @@ func (c colimaApp) Delete(data, force bool) error {
281303

282304
// delete runtime disk if disk in use and data deletion is requested
283305
if diskInUse && data {
284-
log.Println("deleting container data")
285-
if err := limautil.DeleteDisk(); err != nil {
286-
return fmt.Errorf("error deleting container data: %w", err)
306+
conf, _ := configmanager.LoadInstance()
307+
if conf.VMType != "native" {
308+
log.Println("deleting container data")
309+
if err := limautil.DeleteDisk(); err != nil {
310+
return fmt.Errorf("error deleting container data: %w", err)
311+
}
287312
}
288313

289314
if err := store.Reset(); err != nil {
@@ -342,8 +367,7 @@ func (c colimaApp) SSH(args ...string) error {
342367
workDir = ""
343368
}
344369

345-
guest := lima.New(host.New())
346-
return guest.SSH(workDir, args...)
370+
return c.guest.SSH(workDir, args...)
347371
}
348372

349373
type statusInfo struct {
@@ -383,29 +407,49 @@ func (c colimaApp) getStatus() (status statusInfo, err error) {
383407
status.Arch = string(c.guest.Arch())
384408
status.Runtime = currentRuntime
385409
status.MountType = conf.MountType
386-
ipAddress := limautil.IPAddress(config.CurrentProfile().ID)
387-
if ipAddress != "127.0.0.1" {
388-
status.IPAddress = ipAddress
410+
if conf.VMType == "native" {
411+
status.IPAddress = native.HostIPAddress()
412+
cpu, mem, disk := native.HostResources()
413+
status.CPU = cpu
414+
status.Memory = mem
415+
status.Disk = disk
416+
} else {
417+
ipAddress := limautil.IPAddress(config.CurrentProfile().ID)
418+
if ipAddress != "127.0.0.1" {
419+
status.IPAddress = ipAddress
420+
}
421+
if inst, err := limautil.Instance(); err == nil {
422+
status.CPU = inst.CPU
423+
status.Memory = inst.Memory
424+
status.Disk = inst.Disk
425+
}
389426
}
390427
if currentRuntime == docker.Name {
391-
status.DockerSocket = "unix://" + docker.HostSocketFile()
392-
status.ContainerdSocket = "unix://" + containerd.HostSocketFiles().Containerd
428+
if conf.VMType == "native" {
429+
status.DockerSocket = "unix:///var/run/docker.sock"
430+
} else {
431+
status.DockerSocket = "unix://" + docker.HostSocketFile()
432+
status.ContainerdSocket = "unix://" + containerd.HostSocketFiles().Containerd
433+
}
393434
}
394435
if currentRuntime == containerd.Name {
395-
status.ContainerdSocket = "unix://" + containerd.HostSocketFiles().Containerd
396-
status.BuildkitdSocket = "unix://" + containerd.HostSocketFiles().Buildkitd
436+
if conf.VMType == "native" {
437+
status.ContainerdSocket = "unix:///run/containerd/containerd.sock"
438+
} else {
439+
status.ContainerdSocket = "unix://" + containerd.HostSocketFiles().Containerd
440+
status.BuildkitdSocket = "unix://" + containerd.HostSocketFiles().Buildkitd
441+
}
397442
}
398443
if currentRuntime == incus.Name {
399-
status.IncusSocket = "unix://" + incus.HostSocketFile()
444+
if conf.VMType == "native" {
445+
status.IncusSocket = "unix:///var/lib/incus/unix.socket"
446+
} else {
447+
status.IncusSocket = "unix://" + incus.HostSocketFile()
448+
}
400449
}
401450
if k, err := c.Kubernetes(); err == nil && k.Running(ctx) {
402451
status.Kubernetes = true
403452
}
404-
if inst, err := limautil.Instance(); err == nil {
405-
status.CPU = inst.CPU
406-
status.Memory = inst.Memory
407-
status.Disk = inst.Disk
408-
}
409453
return status, nil
410454
}
411455

@@ -625,6 +669,11 @@ func (c *colimaApp) Update() error {
625669
}
626670

627671
func generateSSHConfig(modifySSHConfig bool) error {
672+
// Skip SSH config generation for native mode (no VM to SSH into)
673+
if conf, err := configmanager.LoadInstance(); err == nil && conf.VMType == "native" {
674+
return nil
675+
}
676+
628677
instances, err := limautil.Instances()
629678
if err != nil {
630679
return fmt.Errorf("error retrieving instances: %w", err)

cmd/clone.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/abiosoft/colima/cli"
99
"github.com/abiosoft/colima/cmd/root"
1010
"github.com/abiosoft/colima/config"
11+
"github.com/abiosoft/colima/config/configmanager"
1112
"github.com/sirupsen/logrus"
1213
"github.com/spf13/cobra"
1314
)
@@ -23,15 +24,22 @@ var cloneCmd = &cobra.Command{
2324
to := config.ProfileFromName(args[1])
2425

2526
logrus.Infof("preparing to clone %s...", from.DisplayName)
26-
{
27-
// verify source profile exists
27+
28+
// Check if source is a native instance
29+
isNative := false
30+
if conf, err := configmanager.LoadFrom(from.StateFile()); err == nil && conf.VMType == "native" {
31+
isNative = true
32+
}
33+
34+
if !isNative {
35+
// VM mode: verify source profile exists and clone VM files
2836
if stat, err := os.Stat(from.LimaInstanceDir()); err != nil || !stat.IsDir() {
2937
return fmt.Errorf("colima profile '%s' does not exist", from.ShortName)
3038
}
3139

32-
// verify destination profile does not exists
40+
// verify destination profile does not exist
3341
if stat, err := os.Stat(to.LimaInstanceDir()); err == nil && stat.IsDir() {
34-
return fmt.Errorf("colima profile '%s' already exists, delete with `colima delete %s` and try again", to.ShortName, to.ShortName)
42+
return fmt.Errorf("colima profile '%s' already exists, delete with 'colima delete %s' and try again", to.ShortName, to.ShortName)
3543
}
3644

3745
// copy source to destination
@@ -49,6 +57,11 @@ var cloneCmd = &cobra.Command{
4957
).Run(); err != nil {
5058
return fmt.Errorf("error copying VM: %w", err)
5159
}
60+
} else {
61+
// Native mode: verify source config exists
62+
if _, err := os.Stat(from.ConfigDir()); err != nil {
63+
return fmt.Errorf("colima profile '%s' does not exist", from.ShortName)
64+
}
5265
}
5366

5467
{

cmd/prune.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ var pruneCmd = &cobra.Command{
4343
}
4444

4545
if pruneCmdArgs.all {
46+
// Lima prune is not applicable in native-only mode
4647
cmd := limautil.Limactl("prune")
4748
if err := cmd.Run(); err != nil {
48-
return fmt.Errorf("error during Lima prune: %w", err)
49+
logrus.Warnf("Lima prune skipped or failed: %v", err)
4950
}
5051
}
5152

cmd/restart.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ The state of the VM is persisted at stop. A start afterwards
2323
should return it back to its previous state.`,
2424
Args: cobra.MaximumNArgs(1),
2525
RunE: func(cmd *cobra.Command, args []string) error {
26-
// validate if the instance was previously created
27-
if _, err := limautil.Instance(); err != nil {
28-
return err
26+
// validate if the instance was previously created (skip for native mode)
27+
conf, _ := configmanager.LoadInstance()
28+
if conf.VMType != "native" {
29+
if _, err := limautil.Instance(); err != nil {
30+
return err
31+
}
2932
}
3033

3134
app := newApp()

cmd/ssh-config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/abiosoft/colima/cmd/root"
77
"github.com/abiosoft/colima/config"
8+
"github.com/abiosoft/colima/config/configmanager"
89
"github.com/abiosoft/colima/environment/vm/lima/limautil"
910
"github.com/spf13/cobra"
1011
)
@@ -16,6 +17,12 @@ var sshConfigCmd = &cobra.Command{
1617
Long: `Show configuration of the SSH connection to the VM.`,
1718
Args: cobra.MaximumNArgs(1),
1819
RunE: func(cmd *cobra.Command, args []string) error {
20+
// SSH config is not applicable for native mode
21+
if conf, err := configmanager.LoadInstance(); err == nil && conf.VMType == "native" {
22+
fmt.Println("# SSH config not applicable for native mode (no VM)")
23+
return nil
24+
}
25+
1926
resp, err := limautil.ShowSSH(config.CurrentProfile().ID)
2027
if err == nil {
2128
fmt.Println(resp.Output)

cmd/start.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Run 'colima template' to set the default configurations or 'colima start --edit'
5151
" colima start --kubernetes --k3s-arg='\"--disable=coredns,servicelb,traefik,local-storage,metrics-server\"'",
5252
Args: cobra.MaximumNArgs(1),
5353
RunE: func(cmd *cobra.Command, args []string) error {
54-
app := newApp()
54+
app := newAppWithVMType(startCmdArgs.VMType)
5555
conf := startCmdArgs.Config
5656

5757
if !startCmdArgs.Flags.Edit {
@@ -87,9 +87,11 @@ Run 'colima template' to set the default configurations or 'colima start --edit'
8787
return start(app, conf)
8888
},
8989
PreRunE: func(cmd *cobra.Command, args []string) error {
90-
// validate Lima version
91-
if err := core.LimaVersionSupported(); err != nil {
92-
return fmt.Errorf("lima compatibility error: %w", err)
90+
// validate Lima version (not needed for native mode)
91+
if startCmdArgs.VMType != "native" {
92+
if err := core.LimaVersionSupported(); err != nil {
93+
return fmt.Errorf("lima compatibility error: %w", err)
94+
}
9395
}
9496

9597
// combine args and current config file(if any)
@@ -173,6 +175,9 @@ func init() {
173175
if util.MacOS13OrNewerOnArm() {
174176
vmTypes = append(vmTypes, "krunkit")
175177
}
178+
if util.Linux() {
179+
vmTypes = append(vmTypes, "native")
180+
}
176181
types := strings.Join(vmTypes, ", ")
177182

178183
saveConfigDefault := true
@@ -229,6 +234,11 @@ func init() {
229234
}
230235
}
231236

237+
// vm type on Linux
238+
if util.Linux() {
239+
startCmd.Flags().StringVarP(&startCmdArgs.VMType, "vm-type", "t", defaultVMType, "virtual machine type ("+types+")")
240+
}
241+
232242
// Gateway Address
233243
startCmd.Flags().IPVar(&startCmdArgs.Network.GatewayAddress, "gateway-address", net.ParseIP("192.168.5.2"), "gateway address")
234244

cmd/util.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ func newApp() app.App {
2222
return colimaApp
2323
}
2424

25+
func newAppWithVMType(vmType string) app.App {
26+
colimaApp, err := app.NewWithVMType(vmType)
27+
if err != nil {
28+
logrus.Fatal("Error: ", err)
29+
}
30+
return colimaApp
31+
}
32+
2533
// waitForUserEdit launches a temporary file with content using editor,
2634
// and waits for the user to close the editor.
2735
// It returns the filename (if saved), empty file name (if aborted), and an error (if any).

config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ func (c Config) AutoActivate() bool {
142142
func (c Config) Empty() bool { return c.Runtime == "" } // this may be better but not really needed.
143143

144144
func (c Config) DriverLabel() string {
145+
if c.VMType == "native" {
146+
return "Native"
147+
}
145148
if util.MacOS13OrNewer() && c.VMType == "vz" {
146149
return "macOS Virtualization.Framework"
147150
} else if util.MacOS13OrNewerOnArm() && c.VMType == "krunkit" {

config/configmanager/configmanager.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ func LoadFrom(file string) (config.Config, error) {
4949

5050
// ValidateConfig validates config before we use it
5151
func ValidateConfig(c config.Config) error {
52+
// native mode: skip VM-specific validations entirely
53+
if c.VMType == "native" {
54+
if !util.Linux() {
55+
return fmt.Errorf("vmType 'native' is only available on Linux")
56+
}
57+
// Native mode doesn't use mount types, disk images, or port forwarders
58+
return nil
59+
}
60+
5261
validMountTypes := map[string]bool{"9p": true, "sshfs": true}
5362
validPortForwarders := map[string]bool{"grpc": true, "ssh": true, "none": true}
5463

@@ -65,6 +74,9 @@ func ValidateConfig(c config.Config) error {
6574
if util.MacOS13OrNewerOnArm() {
6675
validVMTypes["krunkit"] = true
6776
}
77+
if util.Linux() {
78+
validVMTypes["native"] = true
79+
}
6880
if c.VMType == "krunkit" && !util.MacOS13OrNewerOnArm() {
6981
return fmt.Errorf("vmType 'krunkit' is only available on macOS with Apple Silicon")
7082
}

environment/container/docker/context.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"path/filepath"
55

66
"github.com/abiosoft/colima/config"
7+
"github.com/abiosoft/colima/config/configmanager"
78
)
89

910
var configDir = func() string { return config.CurrentProfile().ConfigDir() }
@@ -25,9 +26,15 @@ func (d dockerRuntime) setupContext() error {
2526

2627
profile := config.CurrentProfile()
2728

29+
// In native mode, use the system Docker socket directly
30+
socketPath := HostSocketFile()
31+
if conf, err := configmanager.LoadInstance(); err == nil && conf.VMType == "native" {
32+
socketPath = "/var/run/docker.sock"
33+
}
34+
2835
return d.host.Run("docker", "context", "create", profile.ID,
2936
"--description", profile.DisplayName,
30-
"--docker", "host=unix://"+HostSocketFile(),
37+
"--docker", "host=unix://"+socketPath,
3138
)
3239
}
3340

0 commit comments

Comments
 (0)