Skip to content

Commit af18eb1

Browse files
reskerclaude
andcommitted
cli: add colima autostart to start at login or at boot
Lima can register an instance with launchd or systemd to start automatically, either when the user logs in or, on macOS, at system boot before anyone has logged in. The latter is what a headless machine needs, and it is what users have been hand-rolling LaunchAgent and LaunchDaemon plists to approximate (#74, #262, #1265, #1490). Expose it as `colima autostart enable|disable`, delegating to `limactl autostart`. `--boot` selects the system LaunchDaemon, which requires sudo to write to /Library/LaunchDaemons. This relies on the generated unit carrying LIMA_HOME (lima-vm/lima#5489), since a Colima instance lives under ~/.colima/_lima rather than the default ~/.lima, so Lima v2.3.0 or newer is required and the version is checked before registering, rather than letting limactl fail on an unknown flag. The unit runs `limactl start`, not `colima start`, so the provision scripts Colima runs itself -- those with mode after-boot or ready, per Provision.IsColimaMode -- do not run on an automatic start. Enabling autostart warns when the instance has any, and the limitation is noted in the FAQ and the command help. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Robert Esker <resker@gmail.com>
1 parent c3a5f91 commit af18eb1

4 files changed

Lines changed: 240 additions & 0 deletions

File tree

cmd/autostart.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package cmd
2+
3+
import (
4+
"github.com/abiosoft/colima/cmd/root"
5+
"github.com/abiosoft/colima/environment/vm/lima/limautil"
6+
"github.com/spf13/cobra"
7+
)
8+
9+
// autostartCmd represents the autostart command
10+
var autostartCmd = &cobra.Command{
11+
Use: "autostart",
12+
Short: "manage automatic startup",
13+
Long: `Manage automatic startup of Colima.`,
14+
}
15+
16+
var autostartCmdArgs struct {
17+
boot bool
18+
}
19+
20+
var autostartEnableCmd = &cobra.Command{
21+
Use: "enable",
22+
Short: "enable automatic startup",
23+
Long: `Enable automatic startup of Colima.
24+
25+
By default the instance is started when the user logs in. With --boot it is
26+
started at system boot instead, before any user logs in, which is what a
27+
headless machine needs. That installs a system LaunchDaemon and therefore
28+
requires sudo.
29+
30+
Use --profile to target an instance other than the default.
31+
32+
This registers the instance with Lima, so the unit runs "limactl start" rather
33+
than "colima start". Provision scripts configured with mode "afterBoot" or
34+
"ready" therefore do not run on an automatic start.
35+
36+
Requires Lima v2.3.0 or newer.`,
37+
Args: cobra.NoArgs,
38+
RunE: func(cmd *cobra.Command, _ []string) error {
39+
condition := limautil.AutostartLogin
40+
if autostartCmdArgs.boot {
41+
condition = limautil.AutostartBoot
42+
}
43+
return limautil.EnableAutostart(condition)
44+
},
45+
}
46+
47+
var autostartDisableCmd = &cobra.Command{
48+
Use: "disable",
49+
Short: "disable automatic startup",
50+
Long: `Disable automatic startup of Colima.`,
51+
Args: cobra.NoArgs,
52+
RunE: func(_ *cobra.Command, _ []string) error {
53+
return limautil.DisableAutostart()
54+
},
55+
}
56+
57+
func init() {
58+
root.Cmd().AddCommand(autostartCmd)
59+
autostartCmd.AddCommand(autostartEnableCmd)
60+
autostartCmd.AddCommand(autostartDisableCmd)
61+
62+
autostartEnableCmd.Flags().BoolVar(&autostartCmdArgs.boot, "boot", false,
63+
"start at system boot rather than at login (macOS only, requires sudo)")
64+
}

docs/FAQ.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,27 @@ If Colima has been installed using brew, the easiest way to autostart Colima is
113113
brew services start colima
114114
```
115115

116+
Colima can also register itself with Lima's own autostart support, which delegates to
117+
launchd on macOS and systemd on Linux.
118+
119+
```sh
120+
colima autostart enable # start when the user logs in
121+
colima autostart enable --boot # start at system boot, before any user logs in
122+
colima autostart disable
123+
```
124+
125+
`--boot` is macOS only and is the option a headless machine needs, since it does not
126+
require a user to log in. It installs a system LaunchDaemon under
127+
`/Library/LaunchDaemons`, so it prompts for sudo. Lima restarts the instance
128+
automatically if it exits unexpectedly.
129+
130+
Note that this registers the instance with Lima, so the unit runs `limactl start`
131+
rather than `colima start`. Provision scripts configured with mode `afterBoot` or
132+
`ready` therefore do not run on an automatic start. `brew services start colima` runs
133+
`colima start` and does not have that limitation, but it only covers Homebrew installs.
134+
135+
Requires Lima v2.3.0 or newer.
136+
116137
## Can config file be used instead of cli flags?
117138

118139
Yes, from v0.4.0, Colima support YAML configuration file.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package limautil
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/coreos/go-semver/semver"
9+
"github.com/sirupsen/logrus"
10+
11+
"github.com/abiosoft/colima/config"
12+
"github.com/abiosoft/colima/config/configmanager"
13+
)
14+
15+
// AutostartCondition is when an instance should be started automatically.
16+
type AutostartCondition string
17+
18+
const (
19+
// AutostartLogin starts the instance when the user logs in.
20+
AutostartLogin AutostartCondition = "login"
21+
// AutostartBoot starts the instance at system boot, before any user logs in.
22+
// macOS only, and requires privileges to write to /Library/LaunchDaemons.
23+
AutostartBoot AutostartCondition = "boot"
24+
)
25+
26+
// minAutostartVersion is the minimum Lima version usable for autostart.
27+
//
28+
// A Colima instance lives under ~/.colima/_lima rather than the default ~/.lima, so the
29+
// generated launchd/systemd unit has to carry LIMA_HOME. That was added in Lima v2.3.0
30+
// (lima-vm/lima#5489); before it, the unit resolved the default directory at boot and
31+
// never found the instance.
32+
var minAutostartVersion = *semver.New("2.3.0")
33+
34+
// parseVersion extracts the version from the output of `limactl --version`,
35+
// which is of the form "limactl version 2.3.0".
36+
func parseVersion(output string) (*semver.Version, error) {
37+
fields := strings.Fields(output)
38+
if len(fields) == 0 {
39+
return nil, fmt.Errorf("unexpected output from `%s --version`: %q", LimactlCommand, output)
40+
}
41+
return semver.NewVersion(strings.TrimPrefix(fields[len(fields)-1], "v"))
42+
}
43+
44+
// Version returns the version of the limactl binary.
45+
func Version() (*semver.Version, error) {
46+
var buf bytes.Buffer
47+
cmd := Limactl("--version")
48+
cmd.Stdout = &buf
49+
cmd.Stderr = nil
50+
51+
if err := cmd.Run(); err != nil {
52+
return nil, fmt.Errorf("error retrieving lima version: %w", err)
53+
}
54+
return parseVersion(buf.String())
55+
}
56+
57+
// checkAutostartSupported reports whether the installed Lima can autostart a Colima
58+
// instance. A development build is rejected, as its version sorts below the release.
59+
func checkAutostartSupported() error {
60+
version, err := Version()
61+
if err != nil {
62+
return err
63+
}
64+
if version.LessThan(minAutostartVersion) {
65+
return fmt.Errorf("autostart requires Lima v%s or newer, found v%s", minAutostartVersion, version)
66+
}
67+
return nil
68+
}
69+
70+
// warnColimaProvision warns when the instance has provision scripts that Colima runs
71+
// itself. The generated unit invokes `limactl start`, so only the scripts Colima passes
72+
// through to Lima run on an automatic start.
73+
func warnColimaProvision() {
74+
conf, err := configmanager.LoadInstance()
75+
if err != nil {
76+
return // not fatal, the instance may not have been started yet
77+
}
78+
for _, p := range conf.Provision {
79+
if p.IsColimaMode() {
80+
logrus.Warnf("provision scripts with mode %q or %q do not run on an automatic start, "+
81+
"as the instance is started by Lima rather than by Colima",
82+
config.ProvisionModeAfterBoot, config.ProvisionModeReady)
83+
return
84+
}
85+
}
86+
}
87+
88+
// EnableAutostart registers the instance of the current profile to start
89+
// automatically, on the given condition.
90+
func EnableAutostart(condition AutostartCondition) error {
91+
if err := checkAutostartSupported(); err != nil {
92+
return err
93+
}
94+
warnColimaProvision()
95+
cmd := Limactl("autostart", "enable", config.CurrentProfile().ID, "--condition", string(condition))
96+
if err := cmd.Run(); err != nil {
97+
return fmt.Errorf("error enabling autostart: %w", err)
98+
}
99+
return nil
100+
}
101+
102+
// DisableAutostart unregisters the instance of the current profile from
103+
// automatic startup.
104+
func DisableAutostart() error {
105+
cmd := Limactl("autostart", "disable", config.CurrentProfile().ID)
106+
if err := cmd.Run(); err != nil {
107+
return fmt.Errorf("error disabling autostart: %w", err)
108+
}
109+
return nil
110+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package limautil
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestParseVersion(t *testing.T) {
8+
tests := []struct {
9+
output string
10+
want string
11+
supported bool
12+
wantErr bool
13+
}{
14+
{output: "limactl version 2.3.0\n", want: "2.3.0", supported: true},
15+
{output: "limactl version 2.4.1\n", want: "2.4.1", supported: true},
16+
{output: "limactl version v2.3.0\n", want: "2.3.0", supported: true},
17+
{output: "limactl version 2.2.0\n", want: "2.2.0", supported: false},
18+
// development builds sort below the release they precede, and are rejected.
19+
{output: "limactl version 2.2.0-17-gcd1a4b23\n", want: "2.2.0-17-gcd1a4b23", supported: false},
20+
{output: "limactl version 2.3.0-beta.0\n", want: "2.3.0-beta.0", supported: false},
21+
{output: "", wantErr: true},
22+
{output: "limactl version not-a-version\n", wantErr: true},
23+
}
24+
25+
for _, tt := range tests {
26+
t.Run(tt.output, func(t *testing.T) {
27+
version, err := parseVersion(tt.output)
28+
if tt.wantErr {
29+
if err == nil {
30+
t.Fatalf("parseVersion(%q) expected an error, got %v", tt.output, version)
31+
}
32+
return
33+
}
34+
if err != nil {
35+
t.Fatalf("parseVersion(%q) returned an error: %v", tt.output, err)
36+
}
37+
if got := version.String(); got != tt.want {
38+
t.Errorf("parseVersion(%q) = %q, want %q", tt.output, got, tt.want)
39+
}
40+
if got := !version.LessThan(minAutostartVersion); got != tt.supported {
41+
t.Errorf("version %q supported = %v, want %v", tt.want, got, tt.supported)
42+
}
43+
})
44+
}
45+
}

0 commit comments

Comments
 (0)