├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ └── feature_request.yaml ├── dependabot.yaml └── workflows │ ├── go.yml │ ├── golang-ci.yml │ └── integration.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── app └── app.go ├── cli ├── chain.go └── command.go ├── cmd ├── clone.go ├── colima │ └── main.go ├── completion.go ├── daemon │ ├── cmd.go │ ├── daemon.go │ └── daemon_test.go ├── delete.go ├── kubernetes.go ├── list.go ├── nerdctl.go ├── prune.go ├── restart.go ├── root │ └── root.go ├── ssh-config.go ├── ssh.go ├── start.go ├── start_test.go ├── status.go ├── stop.go ├── template.go ├── update.go ├── util.go └── version.go ├── colima.gif ├── colima.nix ├── colima.png ├── config ├── config.go ├── configmanager │ └── configmanager.go ├── files.go └── profile.go ├── core └── core.go ├── daemon ├── daemon.go └── process │ ├── inotify │ ├── events.go │ ├── inotify.go │ ├── volumes.go │ ├── volumes_test.go │ └── watch.go │ ├── process.go │ └── vmnet │ ├── deps.go │ └── vmnet.go ├── default.nix ├── docs ├── FAQ.md └── INSTALL.md ├── embedded ├── defaults │ ├── abort.yaml │ ├── colima.yaml │ └── template.yaml ├── embed.go ├── images │ ├── images.txt │ └── images_sha.sh ├── k3s │ └── flannel.json └── network │ ├── networks.yaml │ ├── sudo.txt │ ├── vmnet_arm64.tar.gz │ └── vmnet_x86_64.tar.gz ├── environment ├── container.go ├── container │ ├── containerd │ │ ├── buildkitd.toml │ │ └── containerd.go │ ├── docker │ │ ├── context.go │ │ ├── daemon.go │ │ ├── docker.go │ │ └── proxy.go │ ├── incus │ │ ├── config.yaml │ │ └── incus.go │ └── kubernetes │ │ ├── cni.go │ │ ├── k3s.go │ │ ├── kubeconfig.go │ │ └── kubernetes.go ├── environment.go ├── host.go ├── host │ └── host.go ├── vm.go └── vm │ └── lima │ ├── certs.go │ ├── config.go │ ├── daemon.go │ ├── file.go │ ├── lima.go │ ├── limaconfig │ └── config.go │ ├── limautil │ ├── disk.go │ ├── files.go │ ├── image.go │ ├── instance.go │ ├── limautil.go │ ├── network.go │ └── ssh.go │ ├── network.go │ ├── shell.go │ ├── yaml.go │ └── yaml_test.go ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum ├── integration └── Dockerfile ├── scripts ├── build_vmnet.sh └── integration.sh ├── shell.nix └── util ├── debutil └── debutil.go ├── downloader ├── download.go └── sha.go ├── fsutil └── fs.go ├── macos.go ├── osutil └── os.go ├── qemu.go ├── shautil └── sha.go ├── template.go ├── terminal ├── output.go └── terminal.go ├── util.go └── yamlutil ├── yaml.go └── yaml_test.go /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.go] 2 | indent_style = tab -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: abiosoft 2 | custom: 3 | - "https://buymeacoffee.com/abiosoft" 4 | patreon: colima 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Report a bug or issue 3 | body: 4 | - type: textarea 5 | attributes: 6 | label: Description 7 | description: A clear and concise description of what the issue is. 8 | - type: textarea 9 | attributes: 10 | label: Version 11 | description: Please show the output of `colima version && limactl --version && qemu-img --version`. 12 | - type: checkboxes 13 | attributes: 14 | label: Operating System 15 | description: Which Operating System/Architecture does this issue happen on? Check all that apply. 16 | options: 17 | - label: macOS Intel <= 13 (Ventura) 18 | required: false 19 | - label: macOS Intel >= 14 (Sonoma) 20 | required: false 21 | - label: Apple Silicon <= 13 (Ventura) 22 | required: false 23 | - label: Apple Silicon >= 14 (Sonoma) 24 | required: false 25 | - label: Linux 26 | required: false 27 | - type: textarea 28 | attributes: 29 | label: Output of `colima status` 30 | description: The output of `colima status` or `colima status -p ` tells us what vm-type and mount type, etc. 31 | value: 32 | - type: textarea 33 | attributes: 34 | label: Reproduction Steps 35 | description: Kindly walk us through the steps to reproduce this behaviour. 36 | value: | 37 | 1. 38 | 2. 39 | 3. 40 | - type: textarea 41 | attributes: 42 | label: Expected behaviour 43 | description: A clear and concise description of what you expected to happen. 44 | - type: textarea 45 | attributes: 46 | label: Additional context 47 | description: Add any other context about the problem here. 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Request a missing feature 3 | body: 4 | - type: textarea 5 | attributes: 6 | label: Description 7 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | tags: ["v*"] 6 | paths-ignore: 7 | - "**/*.md" 8 | - "**/*.nix" 9 | - "**/*.lock" 10 | pull_request: 11 | branches: [main] 12 | paths-ignore: 13 | - "**/*.md" 14 | - "**/*.nix" 15 | - "**/*.lock" 16 | 17 | permissions: write-all 18 | 19 | jobs: 20 | build-linux: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 27 | with: 28 | go-version: "1.23" 29 | 30 | - name: Build 31 | run: go build -v ./... 32 | 33 | - name: Test 34 | run: go test -v ./... 35 | 36 | build-macos: 37 | runs-on: macos-13 38 | steps: 39 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 40 | 41 | - name: Set up Go 42 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 43 | with: 44 | go-version: "1.23" 45 | 46 | - name: Build 47 | run: go build -v ./... 48 | 49 | - name: Test 50 | run: go test -v ./... 51 | 52 | binaries-linux: 53 | needs: "build-linux" 54 | runs-on: ubuntu-latest 55 | 56 | steps: 57 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 58 | 59 | - name: Set up Go 60 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 61 | with: 62 | go-version: "1.23" 63 | 64 | - name: install gcc-aarch64-linux-gnu 65 | run: | 66 | sudo apt-get update 67 | sudo apt-get install -y gcc-aarch64-linux-gnu 68 | 69 | - name: generate binaries 70 | run: | 71 | OS=Linux ARCH=x86_64 make 72 | OS=Linux ARCH=aarch64 make 73 | 74 | - name: upload artifacts 75 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 76 | with: 77 | name: artifacts-linux 78 | path: _output/binaries/ 79 | 80 | binaries-macos: 81 | needs: "build-macos" 82 | runs-on: macos-13 83 | 84 | steps: 85 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 86 | 87 | - name: Set up Go 88 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 89 | with: 90 | go-version: "1.23" 91 | 92 | - name: generate binaries 93 | run: | 94 | CGO_ENABLED=1 OS=Darwin ARCH=x86_64 make 95 | CGO_ENABLED=1 OS=Darwin ARCH=arm64 make 96 | 97 | - name: upload artifacts 98 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 99 | with: 100 | name: artifacts-macos 101 | path: _output/binaries/ 102 | 103 | 104 | release: 105 | needs: ["binaries-linux", "binaries-macos"] 106 | runs-on: ubuntu-latest 107 | 108 | steps: 109 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 110 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 111 | with: 112 | name: artifacts-linux 113 | path: _output/binaries/ 114 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 115 | with: 116 | name: artifacts-macos 117 | path: _output/binaries/ 118 | - name: create release 119 | if: github.event_name != 'pull_request' 120 | env: 121 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 122 | run: > 123 | tag="${GITHUB_REF##*/}" 124 | 125 | gh release create "${tag}" --draft --title "${tag}" 126 | _output/binaries/colima-Darwin-x86_64 127 | _output/binaries/colima-Darwin-x86_64.sha256sum 128 | _output/binaries/colima-Darwin-arm64 129 | _output/binaries/colima-Darwin-arm64.sha256sum 130 | _output/binaries/colima-Linux-x86_64 131 | _output/binaries/colima-Linux-x86_64.sha256sum 132 | _output/binaries/colima-Linux-aarch64 133 | _output/binaries/colima-Linux-aarch64.sha256sum 134 | -------------------------------------------------------------------------------- /.github/workflows/golang-ci.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: [v*] 5 | branches: [main] 6 | paths-ignore: 7 | - "**/*.md" 8 | - "**/*.nix" 9 | - "**/*.lock" 10 | pull_request: 11 | paths-ignore: 12 | - "**/*.md" 13 | - "**/*.nix" 14 | - "**/*.lock" 15 | jobs: 16 | golangci: 17 | name: lint 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | - name: Set up Go 22 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 23 | with: 24 | go-version: "1.23" 25 | - name: golangci-lint 26 | uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6.5.2 27 | with: 28 | version: v1.61.0 29 | args: --timeout 3m0s 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .fleet/ 3 | .vscode/ 4 | _output/ 5 | _build/ 6 | bin/ 7 | result 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - gocritic 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Abiola Ibrahim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | OS ?= $(shell uname) 3 | ARCH ?= $(shell uname -m) 4 | 5 | GOOS ?= $(shell echo "$(OS)" | tr '[:upper:]' '[:lower:]') 6 | GOARCH_x86_64 = amd64 7 | GOARCH_aarch64 = arm64 8 | GOARCH_arm64 = arm64 9 | GOARCH ?= $(shell echo "$(GOARCH_$(ARCH))") 10 | 11 | VERSION := $(shell git describe --tags --always) 12 | REVISION := $(shell git rev-parse HEAD) 13 | PACKAGE := github.com/abiosoft/colima/config 14 | VERSION_VARIABLES := -X $(PACKAGE).appVersion=$(VERSION) -X $(PACKAGE).revision=$(REVISION) 15 | 16 | OUTPUT_DIR := _output/binaries 17 | OUTPUT_BIN := colima-$(OS)-$(ARCH) 18 | INSTALL_DIR := /usr/local/bin 19 | BIN_NAME := colima 20 | 21 | LDFLAGS := $(VERSION_VARIABLES) 22 | 23 | .PHONY: all 24 | all: build 25 | 26 | .PHONY: clean 27 | clean: 28 | rm -rf _output _build 29 | 30 | .PHONY: gopath 31 | gopath: 32 | go get -v ./cmd/colima 33 | 34 | .PHONY: fmt 35 | fmt: 36 | go fmt ./... 37 | goimports -w . 38 | 39 | .PHONY: build 40 | build: 41 | GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags="$(LDFLAGS)" -o $(OUTPUT_DIR)/$(OUTPUT_BIN) ./cmd/colima 42 | ifeq ($(GOOS),darwin) 43 | codesign -s - $(OUTPUT_DIR)/$(OUTPUT_BIN) 44 | endif 45 | cd $(OUTPUT_DIR) && openssl sha256 -r -out $(OUTPUT_BIN).sha256sum $(OUTPUT_BIN) 46 | 47 | .PHONY: test 48 | test: 49 | go test -v -ldflags="$(LD_FLAGS)" ./... 50 | 51 | .PHONY: vmnet 52 | vmnet: 53 | sh scripts/build_vmnet.sh 54 | 55 | .PHONY: install 56 | install: 57 | mkdir -p $(INSTALL_DIR) 58 | rm -f $(INSTALL_DIR)/$(BIN_NAME) 59 | cp $(OUTPUT_DIR)/colima-$(OS)-$(ARCH) $(INSTALL_DIR)/$(BIN_NAME) 60 | chmod +x $(INSTALL_DIR)/$(BIN_NAME) 61 | 62 | .PHONY: lint 63 | lint: ## Assumes that golangci-lint is installed and in the path. To install: https://golangci-lint.run/usage/install/ 64 | golangci-lint --timeout 3m run 65 | 66 | .PHONY: print-binary-name 67 | print-binary-name: 68 | @echo $(OUTPUT_DIR)/$(OUTPUT_BIN) 69 | 70 | .PHONY: nix-derivation-shell 71 | nix-derivation-shell: 72 | $(eval DERIVATION=$(shell nix-build)) 73 | echo $(DERIVATION) | grep ^/nix 74 | nix-shell -p $(DERIVATION) 75 | 76 | .PHONY: integration 77 | integration: build 78 | GOARCH=$(GOARCH) COLIMA_BINARY=$(OUTPUT_DIR)/$(OUTPUT_BIN) scripts/integration.sh 79 | 80 | .PHONY: images-sha 81 | images-sha: 82 | bash embedded/images/images_sha.sh 83 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Thanks for helping make Colima safe for everyone. 2 | 3 | ## Security 4 | 5 | We take the security of Colima seriously. 6 | 7 | We will ensure that your finding gets passed along to the appropriate maintainers for remediation. 8 | 9 | ## Reporting Security Issues 10 | 11 | If you believe you have found a security vulnerability in this repository, please report it to us through coordinated disclosure. 12 | 13 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 14 | 15 | Instead, please send an email to git[@]abiosoft.com. 16 | 17 | Please include as much of the information listed below as you can to help us better understand and resolve the issue: 18 | 19 | * The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) 20 | * Full paths of source file(s) related to the manifestation of the issue 21 | * The location of the affected source code (tag/branch/commit or direct URL) 22 | * Any special configuration required to reproduce the issue 23 | * Step-by-step instructions to reproduce the issue 24 | * Proof-of-concept or exploit code (if possible) 25 | * Impact of the issue, including how an attacker might exploit the issue 26 | 27 | This information will help us triage your report more quickly. 28 | -------------------------------------------------------------------------------- /cli/chain.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "time" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // CtxKeyQuiet is the context key to mute the chain. 13 | var CtxKeyQuiet = struct{ key string }{key: "quiet"} 14 | 15 | // errNonFatal is a non fatal error 16 | type errNonFatal struct { 17 | err error 18 | } 19 | 20 | // Error implements error 21 | func (e errNonFatal) Error() string { return e.err.Error() } 22 | 23 | // ErrNonFatal creates a non-fatal error for a command chain. 24 | // A warning would be printed instead of terminating the chain. 25 | func ErrNonFatal(err error) error { 26 | return errNonFatal{err} 27 | } 28 | 29 | // New creates a new runner instance. 30 | func New(name string) CommandChain { 31 | return &namedCommandChain{ 32 | name: name, 33 | } 34 | } 35 | 36 | type cFunc struct { 37 | f func() error 38 | s string 39 | } 40 | 41 | // CommandChain is a chain of commands. 42 | // commands are executed in order. 43 | type CommandChain interface { 44 | // Init initiates a new runner using the current instance. 45 | Init(ctx context.Context) *ActiveCommandChain 46 | // Logger returns the instance logger. 47 | Logger(ctx context.Context) *log.Entry 48 | } 49 | 50 | var _ CommandChain = (*namedCommandChain)(nil) 51 | 52 | type namedCommandChain struct { 53 | name string 54 | log *log.Entry 55 | } 56 | 57 | func (n namedCommandChain) Logger(ctx context.Context) *log.Entry { 58 | if quiet, _ := ctx.Value(CtxKeyQuiet).(bool); quiet { 59 | l := log.New() 60 | l.SetOutput(io.Discard) 61 | return l.WithContext(ctx) 62 | } 63 | if n.log == nil { 64 | n.log = log.WithField("context", n.name).WithContext(ctx) 65 | } 66 | return n.log 67 | } 68 | 69 | func (n namedCommandChain) Init(ctx context.Context) *ActiveCommandChain { 70 | return &ActiveCommandChain{ 71 | log: n.Logger(ctx), 72 | } 73 | } 74 | 75 | // ActiveCommandChain is an active command chain. 76 | type ActiveCommandChain struct { 77 | funcs []cFunc 78 | lastStage string 79 | log *log.Entry 80 | 81 | executing bool 82 | } 83 | 84 | // Logger returns the logger for the command chain. 85 | func (a *ActiveCommandChain) Logger() *log.Entry { return a.log } 86 | 87 | // Add adds a new function to the runner. 88 | func (a *ActiveCommandChain) Add(f func() error) { 89 | a.funcs = append(a.funcs, cFunc{f: f}) 90 | } 91 | 92 | // Stage sets the current stage of the runner. 93 | func (a *ActiveCommandChain) Stage(s string) { 94 | if a.executing { 95 | a.log.Println(s, "...") 96 | return 97 | } 98 | a.funcs = append(a.funcs, cFunc{s: s}) 99 | } 100 | 101 | // Stagef is like stage with string format. 102 | func (a *ActiveCommandChain) Stagef(format string, s ...interface{}) { 103 | f := fmt.Sprintf(format, s...) 104 | a.Stage(f) 105 | } 106 | 107 | // Exec executes the command chain. 108 | // The first errored function terminates the chain and the 109 | // error is returned. Otherwise, returns nil. 110 | func (a *ActiveCommandChain) Exec() error { 111 | a.executing = true 112 | defer func() { a.executing = false }() 113 | 114 | for _, f := range a.funcs { 115 | if f.f == nil { 116 | if f.s != "" { 117 | a.log.Println(f.s, "...") 118 | a.lastStage = f.s 119 | } 120 | continue 121 | } 122 | 123 | // success 124 | err := f.f() 125 | if err == nil { 126 | continue 127 | } 128 | 129 | // warning 130 | if _, ok := err.(errNonFatal); ok { 131 | if a.lastStage == "" { 132 | a.log.Warnln(err) 133 | } else { 134 | a.log.Warnln(fmt.Errorf("error at '%s': %w", a.lastStage, err)) 135 | } 136 | continue 137 | } 138 | 139 | // error 140 | if a.lastStage == "" { 141 | return err 142 | } 143 | return fmt.Errorf("error at '%s': %w", a.lastStage, err) 144 | } 145 | return nil 146 | } 147 | 148 | // Retry retries `f` up to `count` times at interval. 149 | // If after `count` attempts there is an error, the command chain is terminated with the final error. 150 | // retryCount starts from 1. 151 | func (a *ActiveCommandChain) Retry(stage string, interval time.Duration, count int, f func(retryCount int) error) { 152 | a.Add(func() (err error) { 153 | var i int 154 | for err = f(i + 1); i < count && err != nil; i, err = i+1, f(i+1) { 155 | if stage != "" { 156 | a.log.Println(stage, "...") 157 | } 158 | time.Sleep(interval) 159 | } 160 | return err 161 | }) 162 | } 163 | -------------------------------------------------------------------------------- /cli/command.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "strconv" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var runner commandRunner = &defaultCommandRunner{} 13 | 14 | // Settings is global cli settings 15 | var Settings = struct { 16 | // Verbose toggles verbose output for commands. 17 | Verbose bool 18 | }{} 19 | 20 | // Command creates a new command. 21 | func Command(command string, args ...string) *exec.Cmd { return runner.Command(command, args...) } 22 | 23 | // CommandInteractive creates a new interactive command. 24 | func CommandInteractive(command string, args ...string) *exec.Cmd { 25 | return runner.CommandInteractive(command, args...) 26 | } 27 | 28 | type commandRunner interface { 29 | Command(command string, args ...string) *exec.Cmd 30 | CommandInteractive(command string, args ...string) *exec.Cmd 31 | } 32 | 33 | var _ commandRunner = (*defaultCommandRunner)(nil) 34 | 35 | type defaultCommandRunner struct{} 36 | 37 | func (d defaultCommandRunner) Command(command string, args ...string) *exec.Cmd { 38 | cmd := exec.Command(command, args...) 39 | cmd.Stdout = os.Stdout 40 | cmd.Stderr = os.Stderr 41 | 42 | log.Trace("cmd ", quotedArgs(cmd.Args)) 43 | 44 | return cmd 45 | } 46 | 47 | func (d defaultCommandRunner) CommandInteractive(command string, args ...string) *exec.Cmd { 48 | cmd := exec.Command(command, args...) 49 | cmd.Stdin = os.Stdin 50 | cmd.Stdout = os.Stdout 51 | cmd.Stderr = os.Stderr 52 | 53 | log.Trace("cmd int ", quotedArgs(cmd.Args)) 54 | 55 | return cmd 56 | } 57 | 58 | func quotedArgs(args []string) string { 59 | var q []string 60 | for _, s := range args { 61 | q = append(q, strconv.Quote(s)) 62 | } 63 | return fmt.Sprintf("%v", q) 64 | } 65 | 66 | // Prompt prompts for input with a question. It returns true only if answer is y or Y. 67 | func Prompt(question string) bool { 68 | fmt.Print(question) 69 | fmt.Print("? [y/N] ") 70 | fmt.Print("\033[0m") // reset all formatting modes (if any) used by the question string 71 | 72 | var answer string 73 | _, _ = fmt.Scanln(&answer) 74 | 75 | if answer == "" { 76 | return false 77 | } 78 | 79 | return answer[0] == 'Y' || answer[0] == 'y' 80 | } 81 | -------------------------------------------------------------------------------- /cmd/clone.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/abiosoft/colima/cli" 9 | "github.com/abiosoft/colima/cmd/root" 10 | "github.com/abiosoft/colima/config" 11 | "github.com/sirupsen/logrus" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // stopCmd represents the stop command 16 | var cloneCmd = &cobra.Command{ 17 | Use: "clone ", 18 | Short: "clone Colima profile", 19 | Long: `Clone the Colima profile.`, 20 | Args: cobra.ExactArgs(2), 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | from := config.ProfileFromName(args[0]) 23 | to := config.ProfileFromName(args[1]) 24 | 25 | logrus.Infof("preparing to clone %s...", from.DisplayName) 26 | { 27 | // verify source profile exists 28 | if stat, err := os.Stat(from.LimaInstanceDir()); err != nil || !stat.IsDir() { 29 | return fmt.Errorf("colima profile '%s' does not exist", from.ShortName) 30 | } 31 | 32 | // verify destination profile does not exists 33 | 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) 35 | } 36 | 37 | // copy source to destination 38 | logrus.Info("cloning virtual machine...") 39 | if err := cli.Command("mkdir", "-p", to.LimaInstanceDir()).Run(); err != nil { 40 | return fmt.Errorf("error preparing to copy VM: %w", err) 41 | } 42 | 43 | if err := cli.Command("cp", 44 | filepath.Join(from.LimaInstanceDir(), "basedisk"), 45 | filepath.Join(from.LimaInstanceDir(), "diffdisk"), 46 | filepath.Join(from.LimaInstanceDir(), "cidata.iso"), 47 | filepath.Join(from.LimaInstanceDir(), "lima.yaml"), 48 | to.LimaInstanceDir(), 49 | ).Run(); err != nil { 50 | return fmt.Errorf("error copying VM: %w", err) 51 | } 52 | } 53 | 54 | { 55 | logrus.Info("copying config...") 56 | // verify source config exists 57 | if _, err := os.Stat(from.LimaInstanceDir()); err != nil { 58 | return fmt.Errorf("config missing for colima profile '%s': %w", from.ShortName, err) 59 | } 60 | 61 | // ensure destination config directory 62 | if err := cli.Command("mkdir", "-p", filepath.Dir(to.LimaInstanceDir())).Run(); err != nil { 63 | return fmt.Errorf("cannot copy config to new profile '%s': %w", to.ShortName, err) 64 | } 65 | 66 | if err := cli.Command("cp", from.LimaInstanceDir(), to.LimaInstanceDir()).Run(); err != nil { 67 | return fmt.Errorf("error copying VM config: %w", err) 68 | } 69 | } 70 | 71 | logrus.Info("clone successful") 72 | logrus.Infof("run `colima start %s` to start the newly cloned profile", to.ShortName) 73 | return nil 74 | }, 75 | } 76 | 77 | func init() { 78 | root.Cmd().AddCommand(cloneCmd) 79 | cloneCmd.Hidden = true 80 | 81 | } 82 | -------------------------------------------------------------------------------- /cmd/colima/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "github.com/abiosoft/colima/cmd" // for other commands 5 | _ "github.com/abiosoft/colima/cmd/daemon" // for vmnet daemon 6 | _ "github.com/abiosoft/colima/embedded" // for embedded assets 7 | 8 | "github.com/abiosoft/colima/cmd/root" 9 | ) 10 | 11 | func main() { 12 | root.Execute() 13 | } 14 | -------------------------------------------------------------------------------- /cmd/completion.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/abiosoft/colima/cmd/root" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // completionCmd represents the completion command 11 | func completionCmd() *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "completion [bash|zsh|fish|powershell]", 14 | Short: "Generate completion script", 15 | Long: `To load completions: 16 | Bash: 17 | 18 | $ source <(colima completion bash) 19 | 20 | # To load completions for each session, execute once: 21 | # Linux: 22 | $ colima completion bash > /etc/bash_completion.d/colima 23 | # macOS: 24 | $ colima completion bash > /usr/local/etc/bash_completion.d/colima 25 | 26 | Zsh: 27 | 28 | # If shell completion is not already enabled in your environment, 29 | # you will need to enable it. You can execute the following once: 30 | 31 | $ echo "autoload -U compinit; compinit" >> ~/.zshrc 32 | 33 | # To load completions for each session, execute once: 34 | $ colima completion zsh > "${fpath[1]}/_colima" 35 | 36 | # You will need to start a new shell for this setup to take effect. 37 | 38 | fish: 39 | 40 | $ colima completion fish | source 41 | 42 | # To load completions for each session, execute once: 43 | $ colima completion fish > ~/.config/fish/completions/colima.fish 44 | 45 | PowerShell: 46 | 47 | PS> colima completion powershell | Out-String | Invoke-Expression 48 | 49 | # To load completions for every new session, run: 50 | PS> colima completion powershell > colima.ps1 51 | # and source this file from your PowerShell profile. 52 | `, 53 | DisableFlagsInUseLine: true, 54 | ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, 55 | Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), 56 | Run: func(cmd *cobra.Command, args []string) { 57 | switch args[0] { 58 | case "bash": 59 | _ = cmd.Root().GenBashCompletion(os.Stdout) 60 | case "zsh": 61 | _ = cmd.Root().GenZshCompletion(os.Stdout) 62 | case "fish": 63 | _ = cmd.Root().GenFishCompletion(os.Stdout, true) 64 | case "powershell": 65 | _ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) 66 | } 67 | }, 68 | } 69 | return cmd 70 | } 71 | 72 | func init() { 73 | root.Cmd().AddCommand(completionCmd()) 74 | } 75 | -------------------------------------------------------------------------------- /cmd/daemon/cmd.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/abiosoft/colima/cmd/root" 8 | "github.com/abiosoft/colima/config" 9 | "github.com/abiosoft/colima/daemon/process" 10 | "github.com/abiosoft/colima/daemon/process/inotify" 11 | "github.com/abiosoft/colima/daemon/process/vmnet" 12 | "github.com/abiosoft/colima/environment/host" 13 | "github.com/abiosoft/colima/environment/vm/lima" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | var daemonCmd = &cobra.Command{ 18 | Use: "daemon", 19 | Short: "daemon", 20 | Long: `runner for background daemons.`, 21 | Hidden: true, 22 | } 23 | 24 | var startCmd = &cobra.Command{ 25 | Use: "start [profile]", 26 | Short: "start daemon", 27 | Long: `start the daemon`, 28 | Args: cobra.ExactArgs(1), 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | config.SetProfile(args[0]) 31 | ctx := cmd.Context() 32 | 33 | var processes []process.Process 34 | if daemonArgs.vmnet { 35 | processes = append(processes, vmnet.New()) 36 | } 37 | if daemonArgs.inotify.enabled { 38 | processes = append(processes, inotify.New()) 39 | guest := lima.New(host.New()) 40 | args := inotify.Args{ 41 | GuestActions: guest, 42 | Runtime: daemonArgs.inotify.runtime, 43 | Dirs: daemonArgs.inotify.dirs, 44 | } 45 | ctx = context.WithValue(ctx, inotify.CtxKeyArgs(), args) 46 | } 47 | 48 | return start(ctx, processes) 49 | }, 50 | } 51 | 52 | var stopCmd = &cobra.Command{ 53 | Use: "stop [profile]", 54 | Short: "stop daemon", 55 | Long: `stop the daemon`, 56 | Args: cobra.ExactArgs(1), 57 | RunE: func(cmd *cobra.Command, args []string) error { 58 | config.SetProfile(args[0]) 59 | 60 | // wait for 60 seconds 61 | timeout := time.Second * 60 62 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 63 | defer cancel() 64 | 65 | return stop(ctx) 66 | }, 67 | } 68 | 69 | var statusCmd = &cobra.Command{ 70 | Use: "status", 71 | Short: "status of the daemon", 72 | Long: `status of the daemon`, 73 | Args: cobra.ExactArgs(1), 74 | RunE: func(cmd *cobra.Command, args []string) error { 75 | config.SetProfile(args[0]) 76 | 77 | return status() 78 | }, 79 | } 80 | 81 | var daemonArgs struct { 82 | vmnet bool 83 | inotify struct { 84 | enabled bool 85 | dirs []string 86 | runtime string 87 | } 88 | 89 | verbose bool 90 | } 91 | 92 | func init() { 93 | root.Cmd().AddCommand(daemonCmd) 94 | 95 | daemonCmd.AddCommand(startCmd) 96 | daemonCmd.AddCommand(stopCmd) 97 | daemonCmd.AddCommand(statusCmd) 98 | 99 | startCmd.Flags().BoolVar(&daemonArgs.vmnet, "vmnet", false, "start vmnet") 100 | startCmd.Flags().BoolVar(&daemonArgs.inotify.enabled, "inotify", false, "start inotify") 101 | startCmd.Flags().StringSliceVar(&daemonArgs.inotify.dirs, "inotify-dir", nil, "set inotify directories") 102 | startCmd.Flags().StringVar(&daemonArgs.inotify.runtime, "inotify-runtime", "docker", "set runtime") 103 | } 104 | -------------------------------------------------------------------------------- /cmd/daemon/daemon.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "path/filepath" 9 | "strconv" 10 | "sync" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/abiosoft/colima/cli" 15 | "github.com/abiosoft/colima/daemon/process" 16 | "github.com/abiosoft/colima/util/fsutil" 17 | godaemon "github.com/sevlyar/go-daemon" 18 | "github.com/sirupsen/logrus" 19 | ) 20 | 21 | var dir = process.Dir 22 | 23 | // daemonize creates the daemon and returns if this is a child process 24 | func daemonize() (ctx *godaemon.Context, child bool, err error) { 25 | dir := dir() 26 | if err := fsutil.MkdirAll(dir, 0755); err != nil { 27 | return nil, false, fmt.Errorf("cannot make dir: %w", err) 28 | } 29 | 30 | info := Info() 31 | 32 | ctx = &godaemon.Context{ 33 | PidFileName: info.PidFile, 34 | PidFilePerm: 0644, 35 | LogFileName: info.LogFile, 36 | LogFilePerm: 0644, 37 | } 38 | 39 | d, err := ctx.Reborn() 40 | if err != nil { 41 | return ctx, false, fmt.Errorf("error starting daemon: %w", err) 42 | } 43 | if d != nil { 44 | return ctx, false, nil 45 | } 46 | 47 | logrus.Info("- - - - - - - - - - - - - - -") 48 | logrus.Info("daemon started by colima") 49 | logrus.Infof("Run `/usr/bin/pkill -F %s` to kill the daemon", info.PidFile) 50 | 51 | return ctx, true, nil 52 | } 53 | 54 | func start(ctx context.Context, processes []process.Process) error { 55 | if status() == nil { 56 | logrus.Info("daemon already running, startup ignored") 57 | return nil 58 | } 59 | 60 | { 61 | ctx, child, err := daemonize() 62 | if err != nil { 63 | return err 64 | } 65 | 66 | if ctx != nil { 67 | defer func() { 68 | _ = ctx.Release() 69 | }() 70 | } 71 | 72 | if !child { 73 | return nil 74 | } 75 | } 76 | 77 | ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) 78 | defer stop() 79 | 80 | return RunProcesses(ctx, processes...) 81 | } 82 | 83 | func stop(ctx context.Context) error { 84 | if status() != nil { 85 | // not running 86 | return nil 87 | } 88 | 89 | info := Info() 90 | 91 | if err := cli.CommandInteractive("/usr/bin/pkill", "-F", info.PidFile).Run(); err != nil { 92 | return fmt.Errorf("error sending sigterm to daemon: %w", err) 93 | } 94 | 95 | logrus.Info("waiting for process to terminate") 96 | 97 | for { 98 | alive := status() == nil 99 | if !alive { 100 | return nil 101 | } 102 | select { 103 | case <-ctx.Done(): 104 | return ctx.Err() 105 | default: 106 | time.Sleep(time.Second * 1) 107 | } 108 | } 109 | 110 | } 111 | 112 | func status() error { 113 | info := Info() 114 | if _, err := os.Stat(info.PidFile); err != nil { 115 | return fmt.Errorf("pid file not found: %w", err) 116 | } 117 | 118 | // check if process is actually running 119 | p, err := os.ReadFile(info.PidFile) 120 | if err != nil { 121 | return fmt.Errorf("error reading pid file: %w", err) 122 | } 123 | pid, _ := strconv.Atoi(string(p)) 124 | if pid == 0 { 125 | return fmt.Errorf("invalid pid: %v", string(p)) 126 | } 127 | 128 | process, err := os.FindProcess(pid) 129 | if err != nil { 130 | return fmt.Errorf("process not found: %v", err) 131 | } 132 | 133 | if err := process.Signal(syscall.Signal(0)); err != nil { 134 | return fmt.Errorf("process signal(0) returned error: %w", err) 135 | } 136 | 137 | return nil 138 | } 139 | 140 | const ( 141 | pidFileName = "daemon.pid" 142 | logFileName = "daemon.log" 143 | ) 144 | 145 | func Info() struct { 146 | PidFile string 147 | LogFile string 148 | } { 149 | dir := dir() 150 | return struct { 151 | PidFile string 152 | LogFile string 153 | }{ 154 | PidFile: filepath.Join(dir, pidFileName), 155 | LogFile: filepath.Join(dir, logFileName), 156 | } 157 | } 158 | 159 | // Run runs the daemon with background processes. 160 | // NOTE: this must be called from the program entrypoint with minimal intermediary logic 161 | // due to the creation of the daemon. 162 | func RunProcesses(ctx context.Context, processes ...process.Process) error { 163 | ctx, stop := context.WithCancel(ctx) 164 | defer stop() 165 | 166 | var wg sync.WaitGroup 167 | wg.Add(len(processes)) 168 | 169 | for _, bg := range processes { 170 | go func(bg process.Process) { 171 | err := bg.Start(ctx) 172 | if err != nil { 173 | logrus.Error(fmt.Errorf("error starting %s: %w", bg.Name(), err)) 174 | stop() 175 | } 176 | wg.Done() 177 | }(bg) 178 | } 179 | 180 | <-ctx.Done() 181 | logrus.Info("terminate signal received") 182 | 183 | wg.Wait() 184 | 185 | return ctx.Err() 186 | } 187 | -------------------------------------------------------------------------------- /cmd/daemon/daemon_test.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | "time" 9 | 10 | "github.com/abiosoft/colima/daemon/process" 11 | ) 12 | 13 | var testDir string 14 | 15 | func setDir(t *testing.T) { 16 | if testDir == "" { 17 | testDir = t.TempDir() 18 | } 19 | dir = func() string { return testDir } 20 | } 21 | 22 | func getProcesses() []process.Process { 23 | var addresses = []string{ 24 | "localhost", 25 | "127.0.0.1", 26 | } 27 | 28 | var processes []process.Process 29 | for _, add := range addresses { 30 | processes = append(processes, &pinger{address: add}) 31 | } 32 | 33 | return processes 34 | } 35 | 36 | func TestStart(t *testing.T) { 37 | setDir(t) 38 | info := Info() 39 | 40 | processes := getProcesses() 41 | 42 | t.Log("pidfile", info.PidFile) 43 | 44 | timeout := time.Second * 5 45 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 46 | defer cancel() 47 | 48 | // start the processes 49 | if err := start(ctx, processes); err != nil { 50 | t.Fatal(err) 51 | } 52 | t.Log("start successful") 53 | 54 | { 55 | loop: 56 | for { 57 | select { 58 | case <-ctx.Done(): 59 | t.Skipf("daemon not supported: %v", ctx.Err()) 60 | default: 61 | if p, err := os.ReadFile(info.PidFile); err == nil && len(p) > 0 { 62 | break loop 63 | } else if err != nil { 64 | t.Logf("encountered err: %v", err) 65 | } 66 | time.Sleep(1 * time.Second) 67 | } 68 | } 69 | } 70 | 71 | // verify the processes are running 72 | if err := status(); err != nil { 73 | t.Error(err) 74 | return 75 | } 76 | 77 | // stop the processes 78 | if err := stop(ctx); err != nil { 79 | t.Error(err) 80 | } 81 | 82 | // verify the processes are no longer running 83 | if err := status(); err == nil { 84 | t.Errorf("process with pidFile %s is still running", info.PidFile) 85 | return 86 | } 87 | 88 | } 89 | 90 | func TestRunProcesses(t *testing.T) { 91 | processes := getProcesses() 92 | 93 | timeout := time.Second * 5 94 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 95 | 96 | // start the processes 97 | done := make(chan error, 1) 98 | go func() { 99 | done <- RunProcesses(ctx, processes...) 100 | }() 101 | 102 | cancel() 103 | 104 | select { 105 | case <-ctx.Done(): 106 | if err := ctx.Err(); err != context.Canceled { 107 | t.Error(err) 108 | } 109 | case err := <-done: 110 | t.Error(err) 111 | } 112 | 113 | } 114 | 115 | var _ process.Process = (*pinger)(nil) 116 | 117 | type pinger struct { 118 | address string 119 | } 120 | 121 | func (p pinger) Alive(ctx context.Context) error { 122 | return nil 123 | } 124 | 125 | // Name implements BgProcess 126 | func (pinger) Name() string { return "pinger" } 127 | 128 | // Start implements BgProcess 129 | func (p *pinger) Start(ctx context.Context) error { 130 | return p.run(ctx, "ping", "-c10", p.address) 131 | } 132 | 133 | // Start implements BgProcess 134 | func (p *pinger) Dependencies() ([]process.Dependency, bool) { return nil, false } 135 | 136 | func (p *pinger) run(ctx context.Context, command string, args ...string) error { 137 | cmd := exec.CommandContext(ctx, command, args...) 138 | cmd.Stdout = os.Stdout 139 | cmd.Stderr = os.Stderr 140 | return cmd.Run() 141 | } 142 | -------------------------------------------------------------------------------- /cmd/delete.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/abiosoft/colima/cli" 5 | "github.com/abiosoft/colima/cmd/root" 6 | "github.com/abiosoft/colima/config" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var deleteCmdArgs struct { 11 | force bool 12 | } 13 | 14 | // deleteCmd represents the delete command 15 | var deleteCmd = &cobra.Command{ 16 | Use: "delete [profile]", 17 | Short: "delete and teardown Colima", 18 | Long: `Delete and teardown Colima and all settings. 19 | 20 | Use with caution. This deletes everything and a startup afterwards is like the 21 | initial startup of Colima. 22 | 23 | If you simply want to reset the Kubernetes cluster, run 'colima kubernetes reset'.`, 24 | Args: cobra.MaximumNArgs(1), 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | if !deleteCmdArgs.force { 27 | y := cli.Prompt("are you sure you want to delete " + config.CurrentProfile().DisplayName + " and all settings") 28 | if !y { 29 | return nil 30 | } 31 | yy := cli.Prompt("\033[31m\033[1mthis will delete ALL container data. Are you sure you want to continue") 32 | if !yy { 33 | return nil 34 | } 35 | } 36 | 37 | return newApp().Delete() 38 | }, 39 | } 40 | 41 | func init() { 42 | root.Cmd().AddCommand(deleteCmd) 43 | 44 | deleteCmd.Flags().BoolVarP(&deleteCmdArgs.force, "force", "f", false, "do not prompt for yes/no") 45 | } 46 | -------------------------------------------------------------------------------- /cmd/kubernetes.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/abiosoft/colima/cmd/root" 8 | "github.com/abiosoft/colima/config" 9 | "github.com/abiosoft/colima/environment/container/kubernetes" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // kubernetesCmd represents the kubernetes command 15 | var kubernetesCmd = &cobra.Command{ 16 | Use: "kubernetes", 17 | Aliases: []string{"kube", "k8s", "k3s", "k"}, 18 | Short: "manage Kubernetes cluster", 19 | Long: `Manage the Kubernetes cluster`, 20 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 21 | // cobra overrides PersistentPreRunE when redeclared. 22 | // re-run rootCmd's. 23 | if err := root.Cmd().PersistentPreRunE(cmd, args); err != nil { 24 | return err 25 | } 26 | if !newApp().Active() { 27 | return fmt.Errorf("%s is not running", config.CurrentProfile().DisplayName) 28 | } 29 | return nil 30 | }, 31 | } 32 | 33 | // kubernetesStartCmd represents the kubernetes start command 34 | var kubernetesStartCmd = &cobra.Command{ 35 | Use: "start", 36 | Short: "start the Kubernetes cluster", 37 | Long: `Start the Kubernetes cluster.`, 38 | Args: cobra.NoArgs, 39 | RunE: func(cmd *cobra.Command, args []string) error { 40 | app := newApp() 41 | k, err := app.Kubernetes() 42 | if err != nil { 43 | return err 44 | } 45 | 46 | if err := k.Provision(context.Background()); err != nil { 47 | return err 48 | } 49 | 50 | return k.Start(context.Background()) 51 | }, 52 | } 53 | 54 | // kubernetesStopCmd represents the kubernetes stop command 55 | var kubernetesStopCmd = &cobra.Command{ 56 | Use: "stop", 57 | Short: "stop the Kubernetes cluster", 58 | Long: `Stop the Kubernetes cluster.`, 59 | Args: cobra.NoArgs, 60 | RunE: func(cmd *cobra.Command, args []string) error { 61 | ctx := cmd.Context() 62 | app := newApp() 63 | k, err := app.Kubernetes() 64 | if err != nil { 65 | return err 66 | } 67 | if !k.Running(ctx) { 68 | return fmt.Errorf("%s is not enabled", kubernetes.Name) 69 | } 70 | 71 | return k.Stop(ctx) 72 | }, 73 | } 74 | 75 | // kubernetesDeleteCmd represents the kubernetes delete command 76 | var kubernetesDeleteCmd = &cobra.Command{ 77 | Use: "delete", 78 | Short: "delete the Kubernetes cluster", 79 | Long: `Delete the Kubernetes cluster.`, 80 | Args: cobra.NoArgs, 81 | RunE: func(cmd *cobra.Command, args []string) error { 82 | ctx := cmd.Context() 83 | app := newApp() 84 | k, err := app.Kubernetes() 85 | if err != nil { 86 | return err 87 | } 88 | if !k.Running(ctx) { 89 | return fmt.Errorf("%s is not enabled", kubernetes.Name) 90 | } 91 | 92 | return k.Teardown(ctx) 93 | }, 94 | } 95 | 96 | // kubernetesResetCmd represents the kubernetes reset command 97 | var kubernetesResetCmd = &cobra.Command{ 98 | Use: "reset", 99 | Short: "reset the Kubernetes cluster", 100 | Long: `Reset the Kubernetes cluster. 101 | 102 | This resets the Kubernetes cluster and all Kubernetes objects 103 | will be deleted. 104 | 105 | The Kubernetes images are cached making the startup (after reset) much faster.`, 106 | Args: cobra.NoArgs, 107 | RunE: func(cmd *cobra.Command, args []string) error { 108 | app := newApp() 109 | k, err := app.Kubernetes() 110 | if err != nil { 111 | return err 112 | } 113 | 114 | if err := k.Teardown(context.Background()); err != nil { 115 | return fmt.Errorf("error deleting %s: %w", kubernetes.Name, err) 116 | } 117 | 118 | ctx := context.Background() 119 | if err := k.Provision(ctx); err != nil { 120 | return err 121 | } 122 | 123 | if err := k.Start(ctx); err != nil { 124 | return fmt.Errorf("error starting %s: %w", kubernetes.Name, err) 125 | } 126 | 127 | return nil 128 | }, 129 | } 130 | 131 | func init() { 132 | root.Cmd().AddCommand(kubernetesCmd) 133 | kubernetesCmd.AddCommand(kubernetesStartCmd) 134 | kubernetesCmd.AddCommand(kubernetesStopCmd) 135 | kubernetesCmd.AddCommand(kubernetesDeleteCmd) 136 | kubernetesCmd.AddCommand(kubernetesResetCmd) 137 | } 138 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "text/tabwriter" 7 | 8 | "github.com/abiosoft/colima/cmd/root" 9 | "github.com/abiosoft/colima/config" 10 | "github.com/abiosoft/colima/environment/vm/lima/limautil" 11 | "github.com/docker/go-units" 12 | "github.com/sirupsen/logrus" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var listCmdArgs struct { 17 | json bool 18 | } 19 | 20 | // listCmd represents the version command 21 | var listCmd = &cobra.Command{ 22 | Use: "list", 23 | Aliases: []string{"ls"}, 24 | Short: "list instances", 25 | Long: `List all created instances. 26 | 27 | A new instance can be created during 'colima start' by specifying the '--profile' flag.`, 28 | Args: cobra.NoArgs, 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | profile := []string{} 31 | if cmd.Flag("profile").Changed { 32 | profile = append(profile, config.CurrentProfile().ID) 33 | } 34 | 35 | instances, err := limautil.Instances(profile...) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | if listCmdArgs.json { 41 | encoder := json.NewEncoder(cmd.OutOrStdout()) 42 | // print instance per line to conform with Lima's output 43 | for _, instance := range instances { 44 | // dir should be hidden from the output 45 | instance.Dir = "" 46 | if err := encoder.Encode(instance); err != nil { 47 | return err 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0) 54 | _, _ = fmt.Fprintln(w, "PROFILE\tSTATUS\tARCH\tCPUS\tMEMORY\tDISK\tRUNTIME\tADDRESS") 55 | 56 | if len(instances) == 0 { 57 | logrus.Warn("No instance found. Run `colima start` to create an instance.") 58 | } 59 | 60 | for _, inst := range instances { 61 | _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s\t%s\t%s\t%s\n", 62 | inst.Name, 63 | inst.Status, 64 | inst.Arch, 65 | inst.CPU, 66 | units.BytesSize(float64(inst.Memory)), 67 | units.BytesSize(float64(inst.Disk)), 68 | inst.Runtime, 69 | inst.IPAddress, 70 | ) 71 | } 72 | 73 | return w.Flush() 74 | }, 75 | } 76 | 77 | func init() { 78 | root.Cmd().AddCommand(listCmd) 79 | 80 | listCmd.Flags().BoolVarP(&listCmdArgs.json, "json", "j", false, "print json output") 81 | } 82 | -------------------------------------------------------------------------------- /cmd/prune.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | 9 | "github.com/abiosoft/colima/cli" 10 | "github.com/abiosoft/colima/cmd/root" 11 | "github.com/abiosoft/colima/config" 12 | "github.com/abiosoft/colima/environment/vm/lima/limautil" 13 | "github.com/sirupsen/logrus" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | var pruneCmdArgs struct { 18 | force bool 19 | all bool 20 | } 21 | 22 | // pruneCmd represents the prune command 23 | var pruneCmd = &cobra.Command{ 24 | Use: "prune", 25 | Short: "prune cached downloaded assets", 26 | Long: `Prune cached downloaded assets`, 27 | Args: cobra.MaximumNArgs(1), 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | colimaCacheDir := config.CacheDir() 30 | limaCacheDir := filepath.Join(filepath.Dir(colimaCacheDir), "lima") 31 | if !pruneCmdArgs.force { 32 | msg := "'" + colimaCacheDir + "' will be emptied, are you sure" 33 | if pruneCmdArgs.all { 34 | msg = "'" + colimaCacheDir + "' and '" + limaCacheDir + "' will be emptied, are you sure" 35 | } 36 | if y := cli.Prompt(msg); !y { 37 | return nil 38 | } 39 | } 40 | logrus.Info("Pruning ", strconv.Quote(config.CacheDir())) 41 | if err := os.RemoveAll(config.CacheDir()); err != nil { 42 | return fmt.Errorf("error during prune: %w", err) 43 | } 44 | 45 | if pruneCmdArgs.all { 46 | cmd := limautil.Limactl("prune") 47 | if err := cmd.Run(); err != nil { 48 | return fmt.Errorf("error during Lima prune: %w", err) 49 | } 50 | } 51 | 52 | return nil 53 | }, 54 | } 55 | 56 | func init() { 57 | root.Cmd().AddCommand(pruneCmd) 58 | 59 | pruneCmd.Flags().BoolVarP(&pruneCmdArgs.force, "force", "f", false, "do not prompt for yes/no") 60 | pruneCmd.Flags().BoolVarP(&pruneCmdArgs.all, "all", "a", false, "include Lima assets") 61 | } 62 | -------------------------------------------------------------------------------- /cmd/restart.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/abiosoft/colima/cmd/root" 7 | "github.com/abiosoft/colima/config/configmanager" 8 | "github.com/abiosoft/colima/environment/vm/lima/limautil" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var restartCmdArgs struct { 13 | force bool 14 | } 15 | 16 | // restartCmd represents the restart command 17 | var restartCmd = &cobra.Command{ 18 | Use: "restart [profile]", 19 | Short: "restart Colima", 20 | Long: `Stop and then starts Colima. 21 | 22 | The state of the VM is persisted at stop. A start afterwards 23 | should return it back to its previous state.`, 24 | Args: cobra.MaximumNArgs(1), 25 | 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 29 | } 30 | 31 | app := newApp() 32 | 33 | if err := app.Stop(restartCmdArgs.force); err != nil { 34 | return err 35 | } 36 | 37 | // delay a bit before starting 38 | time.Sleep(time.Second * 3) 39 | 40 | config, err := configmanager.Load() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | return app.Start(config) 46 | }, 47 | } 48 | 49 | func init() { 50 | root.Cmd().AddCommand(restartCmd) 51 | 52 | restartCmd.Flags().BoolVarP(&restartCmdArgs.force, "force", "f", false, "during restart, do stop without graceful shutdown") 53 | } 54 | -------------------------------------------------------------------------------- /cmd/root/root.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/abiosoft/colima/cli" 7 | "github.com/abiosoft/colima/config" 8 | "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var versionInfo = config.AppVersion() 13 | 14 | // rootCmd represents the base command when called without any subcommands 15 | var rootCmd = &cobra.Command{ 16 | Use: "colima", 17 | Short: "container runtimes on macOS with minimal setup", 18 | Long: `Colima provides container runtimes on macOS with minimal setup.`, 19 | Version: versionInfo.Version, 20 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 21 | 22 | switch cmd.Name() { 23 | 24 | // special case handling for commands directly interacting with the VM 25 | // start, stop, restart, delete, status, version, update, ssh-config 26 | case "start", 27 | "stop", 28 | "restart", 29 | "delete", 30 | "status", 31 | "version", 32 | "update", 33 | "ssh-config": 34 | 35 | // if an arg is passed, assume it to be the profile (provided --profile is unset) 36 | // i.e. colima start docker == colima start --profile=docker 37 | if len(args) > 0 && !cmd.Flag("profile").Changed { 38 | rootCmdArgs.Profile = args[0] 39 | } 40 | } 41 | if rootCmdArgs.Profile != "" { 42 | config.SetProfile(rootCmdArgs.Profile) 43 | } 44 | if err := initLog(); err != nil { 45 | return err 46 | } 47 | 48 | cmd.SilenceUsage = true 49 | cmd.SilenceErrors = true 50 | return nil 51 | }, 52 | } 53 | 54 | // Cmd returns the root command. 55 | func Cmd() *cobra.Command { 56 | return rootCmd 57 | } 58 | 59 | // rootCmdArgs holds all flags configured in root Cmd 60 | var rootCmdArgs struct { 61 | Profile string 62 | Verbose bool 63 | VeryVerbose bool 64 | } 65 | 66 | // Execute adds all child commands to the root command and sets flags appropriately. 67 | // This is called by main.main(). It only needs to happen once to the rootCmd. 68 | func Execute() { 69 | if err := rootCmd.Execute(); err != nil { 70 | logrus.Fatal(err) 71 | } 72 | } 73 | 74 | func init() { 75 | rootCmd.PersistentFlags().BoolVarP(&rootCmdArgs.Verbose, "verbose", "v", rootCmdArgs.Verbose, "enable verbose log") 76 | rootCmd.PersistentFlags().BoolVar(&rootCmdArgs.VeryVerbose, "very-verbose", rootCmdArgs.VeryVerbose, "enable more verbose log") 77 | rootCmd.PersistentFlags().StringVarP(&rootCmdArgs.Profile, "profile", "p", "default", "profile name, for multiple instances") 78 | } 79 | 80 | func initLog() error { 81 | if rootCmdArgs.Verbose { 82 | cli.Settings.Verbose = true 83 | logrus.SetLevel(logrus.DebugLevel) 84 | } 85 | if rootCmdArgs.VeryVerbose { 86 | cli.Settings.Verbose = true 87 | logrus.SetLevel(logrus.TraceLevel) 88 | } 89 | 90 | // general log output 91 | log.SetOutput(logrus.StandardLogger().Writer()) 92 | log.SetFlags(0) 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /cmd/ssh-config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/abiosoft/colima/cmd/root" 7 | "github.com/abiosoft/colima/config" 8 | "github.com/abiosoft/colima/environment/vm/lima/limautil" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // statusCmd represents the status command 13 | var sshConfigCmd = &cobra.Command{ 14 | Use: "ssh-config [profile]", 15 | Short: "show SSH connection config", 16 | Long: `Show configuration of the SSH connection to the VM.`, 17 | Args: cobra.MaximumNArgs(1), 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | resp, err := limautil.ShowSSH(config.CurrentProfile().ID) 20 | if err == nil { 21 | fmt.Println(resp.Output) 22 | } 23 | return err 24 | }, 25 | } 26 | 27 | func init() { 28 | root.Cmd().AddCommand(sshConfigCmd) 29 | } 30 | -------------------------------------------------------------------------------- /cmd/ssh.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/abiosoft/colima/cmd/root" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | // sshCmd represents the ssh command 9 | var sshCmd = &cobra.Command{ 10 | Use: "ssh", 11 | Aliases: []string{"exec", "x"}, 12 | Short: "SSH into the VM", 13 | Long: `SSH into the VM. 14 | 15 | Appending additional command runs the command instead. 16 | e.g. 'colima ssh -- htop' will run htop. 17 | 18 | It is recommended to specify '--' to differentiate from colima flags.`, 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | return newApp().SSH(args...) 21 | }, 22 | } 23 | 24 | func init() { 25 | root.Cmd().AddCommand(sshCmd) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/start_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/abiosoft/colima/config" 9 | ) 10 | 11 | func Test_mountsFromFlag(t *testing.T) { 12 | tests := []struct { 13 | mounts []string 14 | want []config.Mount 15 | }{ 16 | { 17 | mounts: []string{ 18 | "~:w", 19 | }, 20 | want: []config.Mount{ 21 | {Location: "~", Writable: true}, 22 | }, 23 | }, 24 | { 25 | mounts: []string{ 26 | "~", 27 | }, 28 | want: []config.Mount{ 29 | {Location: "~"}, 30 | }, 31 | }, 32 | { 33 | mounts: []string{ 34 | "/home/users", "/home/another:w", "/tmp", 35 | }, 36 | want: []config.Mount{ 37 | {Location: "/home/users"}, 38 | {Location: "/home/another", Writable: true}, 39 | {Location: "/tmp"}, 40 | }, 41 | }, 42 | { 43 | mounts: []string{ 44 | "/home/users:/home/users", "/home/another:w", "/tmp:/users/tmp", "/tmp:/users/tmp:w", 45 | }, 46 | want: []config.Mount{ 47 | {Location: "/home/users", MountPoint: "/home/users"}, 48 | {Location: "/home/another", Writable: true}, 49 | {Location: "/tmp", MountPoint: "/users/tmp"}, 50 | {Location: "/tmp", MountPoint: "/users/tmp", Writable: true}, 51 | }, 52 | }, 53 | } 54 | for i, tt := range tests { 55 | t.Run(strconv.Itoa(i), func(t *testing.T) { 56 | if got := mountsFromFlag(tt.mounts); !reflect.DeepEqual(got, tt.want) { 57 | t.Errorf("mountsFromFlag() = %+v, want %+v", got, tt.want) 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /cmd/status.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/abiosoft/colima/cmd/root" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var statusCmdArgs struct { 9 | extended bool 10 | json bool 11 | } 12 | 13 | // statusCmd represents the status command 14 | var statusCmd = &cobra.Command{ 15 | Use: "status [profile]", 16 | Short: "show the status of Colima", 17 | Long: `Show the status of Colima`, 18 | Args: cobra.MaximumNArgs(1), 19 | RunE: func(cmd *cobra.Command, args []string) error { 20 | return newApp().Status(statusCmdArgs.extended, statusCmdArgs.json) 21 | }, 22 | } 23 | 24 | func init() { 25 | root.Cmd().AddCommand(statusCmd) 26 | 27 | statusCmd.Flags().BoolVarP(&statusCmdArgs.extended, "extended", "e", false, "include additional details") 28 | statusCmd.Flags().BoolVarP(&statusCmdArgs.json, "json", "j", false, "print json output") 29 | } 30 | -------------------------------------------------------------------------------- /cmd/stop.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/abiosoft/colima/cmd/root" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var stopCmdArgs struct { 9 | force bool 10 | } 11 | 12 | // stopCmd represents the stop command 13 | var stopCmd = &cobra.Command{ 14 | Use: "stop [profile]", 15 | Short: "stop Colima", 16 | Long: `Stop Colima to free up resources. 17 | 18 | The state of the VM is persisted at stop. A start afterwards 19 | should return it back to its previous state.`, 20 | Args: cobra.MaximumNArgs(1), 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | return newApp().Stop(stopCmdArgs.force) 23 | }, 24 | } 25 | 26 | func init() { 27 | root.Cmd().AddCommand(stopCmd) 28 | 29 | stopCmd.Flags().BoolVarP(&stopCmdArgs.force, "force", "f", false, "stop without graceful shutdown") 30 | } 31 | -------------------------------------------------------------------------------- /cmd/template.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/abiosoft/colima/cmd/root" 10 | "github.com/abiosoft/colima/config" 11 | "github.com/abiosoft/colima/config/configmanager" 12 | "github.com/abiosoft/colima/embedded" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // templateCmd represents the template command 17 | var templateCmd = &cobra.Command{ 18 | Use: "template", 19 | Aliases: []string{"tmpl", "tpl", "t"}, 20 | Short: "edit the template for default configurations", 21 | Long: `Edit the template for default configurations of new instances. 22 | `, 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | if templateCmdArgs.Print { 25 | fmt.Println(templateFile()) 26 | return nil 27 | } 28 | // there are unwarranted []byte to string overheads. 29 | // not a big deal in this case 30 | 31 | abort, err := embedded.ReadString("defaults/abort.yaml") 32 | if err != nil { 33 | return fmt.Errorf("error reading embedded file: %w", err) 34 | } 35 | info, err := embedded.ReadString("defaults/template.yaml") 36 | if err != nil { 37 | return fmt.Errorf("error reading embedded file: %w", err) 38 | } 39 | template, err := templateFileOrDefault() 40 | if err != nil { 41 | return fmt.Errorf("error reading template file: %w", err) 42 | } 43 | 44 | tmpFile, err := waitForUserEdit(templateCmdArgs.Editor, []byte(abort+"\n"+info+"\n"+template)) 45 | if err != nil { 46 | return fmt.Errorf("error editing template file: %w", err) 47 | } 48 | if tmpFile == "" { 49 | return fmt.Errorf("empty file, template edit aborted") 50 | } 51 | defer func() { 52 | _ = os.Remove(tmpFile) 53 | }() 54 | 55 | // load and resave template to ensure the format is correct 56 | cf, err := configmanager.LoadFrom(tmpFile) 57 | if err != nil { 58 | return fmt.Errorf("error in template: %w", err) 59 | } 60 | if err := configmanager.SaveToFile(cf, templateFile()); err != nil { 61 | return fmt.Errorf("error saving template: %w", err) 62 | } 63 | 64 | log.Println("configurations template saved") 65 | 66 | return nil 67 | }, 68 | } 69 | 70 | func templateFile() string { return filepath.Join(config.TemplatesDir(), "default.yaml") } 71 | 72 | func templateFileOrDefault() (string, error) { 73 | tFile := templateFile() 74 | if _, err := os.Stat(tFile); err == nil { 75 | b, err := os.ReadFile(tFile) 76 | if err == nil { 77 | return string(b), nil 78 | } 79 | } 80 | 81 | return embedded.ReadString("defaults/colima.yaml") 82 | } 83 | 84 | var templateCmdArgs struct { 85 | Editor string 86 | Print bool 87 | } 88 | 89 | func init() { 90 | root.Cmd().AddCommand(templateCmd) 91 | 92 | templateCmd.Flags().StringVar(&templateCmdArgs.Editor, "editor", "", `editor to use for edit e.g. vim, nano, code (default "$EDITOR" env var)`) 93 | templateCmd.Flags().BoolVar(&templateCmdArgs.Print, "print", false, `print out the configuration file path, without editing`) 94 | } 95 | -------------------------------------------------------------------------------- /cmd/update.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/abiosoft/colima/cmd/root" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | // statusCmd represents the status command 9 | var updateCmd = &cobra.Command{ 10 | Use: "update [profile]", 11 | Aliases: []string{"u", "up"}, 12 | Short: "update the container runtime", 13 | Long: `Update the current container runtime.`, 14 | Args: cobra.MaximumNArgs(1), 15 | RunE: func(cmd *cobra.Command, args []string) error { 16 | return newApp().Update() 17 | }, 18 | } 19 | 20 | func init() { 21 | root.Cmd().AddCommand(updateCmd) 22 | } 23 | -------------------------------------------------------------------------------- /cmd/util.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/abiosoft/colima/app" 13 | "github.com/abiosoft/colima/cli" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | func newApp() app.App { 18 | colimaApp, err := app.New() 19 | if err != nil { 20 | logrus.Fatal("Error: ", err) 21 | } 22 | return colimaApp 23 | } 24 | 25 | // waitForUserEdit launches a temporary file with content using editor, 26 | // and waits for the user to close the editor. 27 | // It returns the filename (if saved), empty file name (if aborted), and an error (if any). 28 | func waitForUserEdit(editor string, content []byte) (string, error) { 29 | tmp, err := os.CreateTemp("", "colima-*.yaml") 30 | if err != nil { 31 | return "", fmt.Errorf("error creating temporary file: %w", err) 32 | } 33 | if _, err := tmp.Write(content); err != nil { 34 | return "", fmt.Errorf("error writing temporary file: %w", err) 35 | } 36 | if err := tmp.Close(); err != nil { 37 | return "", fmt.Errorf("error closing temporary file: %w", err) 38 | } 39 | 40 | if err := launchEditor(editor, tmp.Name()); err != nil { 41 | return "", err 42 | } 43 | 44 | // aborted 45 | if f, err := os.ReadFile(tmp.Name()); err == nil && len(bytes.TrimSpace(f)) == 0 { 46 | return "", nil 47 | } 48 | 49 | return tmp.Name(), nil 50 | } 51 | 52 | var editors = []string{ 53 | "vim", 54 | "code --wait --new-window", 55 | "nano", 56 | } 57 | 58 | func launchEditor(editor string, file string) error { 59 | if editor != "" { 60 | log.Println("editing in", editor) 61 | } 62 | // if not specified, prefer vscode if this a vscode terminal 63 | if editor == "" { 64 | if os.Getenv("TERM_PROGRAM") == "vscode" { 65 | log.Println("vscode detected, editing in vscode") 66 | editor = "code --wait" 67 | } 68 | } 69 | 70 | // if not found, check the EDITOR env var 71 | if editor == "" { 72 | if e := os.Getenv("EDITOR"); e != "" { 73 | log.Println("editing in", e, "from", "$EDITOR environment variable") 74 | editor = e 75 | } 76 | } 77 | 78 | // if not found, check the preferred editors 79 | if editor == "" { 80 | for _, e := range editors { 81 | s := strings.Fields(e) 82 | if _, err := exec.LookPath(s[0]); err == nil { 83 | editor = e 84 | log.Println("editing in", e) 85 | break 86 | } 87 | } 88 | } 89 | 90 | // if still not found, abort 91 | if editor == "" { 92 | return fmt.Errorf("no editor found in $PATH, kindly set $EDITOR environment variable and try again") 93 | } 94 | 95 | // some editors need the wait flag, let us add it if the user has not. 96 | switch editor { 97 | case "code", "code-insiders", "code-oss", "codium", "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code": 98 | editor = strconv.Quote(editor) + " --wait --new-window" 99 | case "mate", "/Applications/TextMate 2.app/Contents/MacOS/mate", "/Applications/TextMate 2.app/Contents/MacOS/TextMate": 100 | editor = strconv.Quote(editor) + " --wait" 101 | } 102 | 103 | return cli.CommandInteractive("sh", "-c", editor+" "+file).Run() 104 | } 105 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/abiosoft/colima/app" 7 | "github.com/abiosoft/colima/cmd/root" 8 | "github.com/abiosoft/colima/config" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // versionCmd represents the version command 13 | var versionCmd = &cobra.Command{ 14 | Use: "version [profile]", 15 | Short: "print the version of Colima", 16 | Long: `Print the version of Colima`, 17 | Args: cobra.MaximumNArgs(1), 18 | Run: func(cmd *cobra.Command, args []string) { 19 | version := config.AppVersion() 20 | fmt.Println(config.AppName, "version", version.Version) 21 | fmt.Println("git commit:", version.Revision) 22 | 23 | if colimaApp, err := app.New(); err == nil { 24 | _ = colimaApp.Version() 25 | } 26 | }, 27 | } 28 | 29 | func init() { 30 | root.Cmd().AddCommand(versionCmd) 31 | } 32 | -------------------------------------------------------------------------------- /colima.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abiosoft/colima/e674521ec4b5a6c1b06c078a5d8b5229e65eec70/colima.gif -------------------------------------------------------------------------------- /colima.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import }: 2 | 3 | with pkgs; 4 | 5 | buildGo120Module { 6 | name = "colima"; 7 | pname = "colima"; 8 | src = ./.; 9 | nativeBuildInputs = [ installShellFiles makeWrapper git ]; 10 | vendorSha256 = "sha256-7DIhSjHpaCyHyXKhR8KWQc2YGaD8CMq+BZHF4zIkL50="; 11 | CGO_ENABLED = 1; 12 | 13 | subPackages = [ "cmd/colima" ]; 14 | 15 | # `nix-build` has .git folder but `nix build` does not, this caters for both cases 16 | preConfigure = '' 17 | export VERSION="$(git describe --tags --always || echo nix-build-at-"$(date +%s)")" 18 | export REVISION="$(git rev-parse HEAD || echo nix-unknown)" 19 | ldflags="-X github.com/abiosoft/colima/config.appVersion=$VERSION 20 | -X github.com/abiosoft/colima/config.revision=$REVISION" 21 | ''; 22 | 23 | postInstall = '' 24 | wrapProgram $out/bin/colima \ 25 | --prefix PATH : ${lib.makeBinPath [ qemu lima ]} 26 | installShellCompletion --cmd colima \ 27 | --bash <($out/bin/colima completion bash) \ 28 | --fish <($out/bin/colima completion fish) \ 29 | --zsh <($out/bin/colima completion zsh) 30 | ''; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /colima.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abiosoft/colima/e674521ec4b5a6c1b06c078a5d8b5229e65eec70/colima.png -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "net" 5 | "path/filepath" 6 | 7 | "github.com/abiosoft/colima/util" 8 | ) 9 | 10 | const ( 11 | AppName = "colima" 12 | ) 13 | 14 | // VersionInfo is the application version info. 15 | type VersionInfo struct { 16 | Version string 17 | Revision string 18 | } 19 | 20 | func AppVersion() VersionInfo { return VersionInfo{Version: appVersion, Revision: revision} } 21 | 22 | var ( 23 | appVersion = "development" 24 | revision = "unknown" 25 | ) 26 | 27 | // Config is the application config. 28 | type Config struct { 29 | CPU int `yaml:"cpu,omitempty"` 30 | Disk int `yaml:"disk,omitempty"` 31 | Memory float32 `yaml:"memory,omitempty"` 32 | Arch string `yaml:"arch,omitempty"` 33 | CPUType string `yaml:"cpuType,omitempty"` 34 | Network Network `yaml:"network,omitempty"` 35 | Env map[string]string `yaml:"env,omitempty"` // environment variables 36 | Hostname string `yaml:"hostname"` 37 | 38 | // SSH 39 | SSHPort int `yaml:"sshPort,omitempty"` 40 | ForwardAgent bool `yaml:"forwardAgent,omitempty"` 41 | SSHConfig bool `yaml:"sshConfig,omitempty"` // config generation 42 | 43 | // VM 44 | VMType string `yaml:"vmType,omitempty"` 45 | VZRosetta bool `yaml:"rosetta,omitempty"` 46 | NestedVirtualization bool `yaml:"nestedVirtualization,omitempty"` 47 | DiskImage string `yaml:"diskImage,omitempty"` 48 | 49 | // volume mounts 50 | Mounts []Mount `yaml:"mounts,omitempty"` 51 | MountType string `yaml:"mountType,omitempty"` 52 | MountINotify bool `yaml:"mountInotify,omitempty"` 53 | 54 | // Runtime is one of docker, containerd. 55 | Runtime string `yaml:"runtime,omitempty"` 56 | ActivateRuntime *bool `yaml:"autoActivate,omitempty"` 57 | 58 | // Kubernetes configuration 59 | Kubernetes Kubernetes `yaml:"kubernetes,omitempty"` 60 | 61 | // Docker configuration 62 | Docker map[string]any `yaml:"docker,omitempty"` 63 | 64 | // provision scripts 65 | Provision []Provision `yaml:"provision,omitempty"` 66 | } 67 | 68 | // Kubernetes is kubernetes configuration 69 | type Kubernetes struct { 70 | Enabled bool `yaml:"enabled"` 71 | Version string `yaml:"version"` 72 | K3sArgs []string `yaml:"k3sArgs"` 73 | } 74 | 75 | // Network is VM network configuration 76 | type Network struct { 77 | Address bool `yaml:"address"` 78 | DNSResolvers []net.IP `yaml:"dns"` 79 | DNSHosts map[string]string `yaml:"dnsHosts"` 80 | HostAddresses bool `yaml:"hostAddresses"` 81 | } 82 | 83 | // Mount is volume mount 84 | type Mount struct { 85 | Location string `yaml:"location"` 86 | MountPoint string `yaml:"mountPoint,omitempty"` 87 | Writable bool `yaml:"writable"` 88 | } 89 | 90 | type Provision struct { 91 | Mode string `yaml:"mode"` 92 | Script string `yaml:"script"` 93 | } 94 | 95 | func (c Config) MountsOrDefault() []Mount { 96 | if len(c.Mounts) > 0 { 97 | return c.Mounts 98 | } 99 | 100 | return []Mount{ 101 | {Location: util.HomeDir(), Writable: true}, 102 | {Location: filepath.Join("/tmp", CurrentProfile().ID), Writable: true}, 103 | } 104 | } 105 | 106 | // AutoActivate returns if auto-activation of host client config is enabled. 107 | func (c Config) AutoActivate() bool { 108 | if c.ActivateRuntime == nil { 109 | return true 110 | } 111 | return *c.ActivateRuntime 112 | } 113 | 114 | // Empty checks if the configuration is empty. 115 | func (c Config) Empty() bool { return c.Runtime == "" } // this may be better but not really needed. 116 | 117 | func (c Config) DriverLabel() string { 118 | if util.MacOS13OrNewer() && c.VMType == "vz" { 119 | return "macOS Virtualization.Framework" 120 | } 121 | return "QEMU" 122 | } 123 | 124 | // CtxKey returns the context key for config. 125 | func CtxKey() any { 126 | return struct{ name string }{name: "colima_config"} 127 | } 128 | -------------------------------------------------------------------------------- /config/configmanager/configmanager.go: -------------------------------------------------------------------------------- 1 | package configmanager 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/abiosoft/colima/cli" 10 | "github.com/abiosoft/colima/config" 11 | "github.com/abiosoft/colima/util" 12 | "github.com/abiosoft/colima/util/yamlutil" 13 | "github.com/sirupsen/logrus" 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | // Save saves the config. 18 | func Save(c config.Config) error { 19 | return yamlutil.Save(c, config.CurrentProfile().File()) 20 | } 21 | 22 | // SaveFromFile loads configuration from file and save as config. 23 | func SaveFromFile(file string) error { 24 | c, err := LoadFrom(file) 25 | if err != nil { 26 | return err 27 | } 28 | return Save(c) 29 | } 30 | 31 | // SaveToFile saves configuration to file. 32 | func SaveToFile(c config.Config, file string) error { 33 | return yamlutil.Save(c, file) 34 | } 35 | 36 | // oldConfigFile returns the path to config file of versions <0.4.0. 37 | // TODO: remove later, only for backward compatibility 38 | func oldConfigFile() string { 39 | _, configFileName := filepath.Split(config.CurrentProfile().File()) 40 | return filepath.Join(os.Getenv("HOME"), "."+config.CurrentProfile().ID, configFileName) 41 | } 42 | 43 | // LoadFrom loads config from file. 44 | func LoadFrom(file string) (config.Config, error) { 45 | var c config.Config 46 | b, err := os.ReadFile(file) 47 | if err != nil { 48 | return c, fmt.Errorf("could not load config from file: %w", err) 49 | } 50 | 51 | err = yaml.Unmarshal(b, &c) 52 | if err != nil { 53 | return c, fmt.Errorf("could not load config from file: %w", err) 54 | } 55 | 56 | return c, nil 57 | } 58 | 59 | // ValidateConfig validates config before we use it 60 | func ValidateConfig(c config.Config) error { 61 | validMountTypes := map[string]bool{"9p": true, "sshfs": true} 62 | if util.MacOS13OrNewer() { 63 | validMountTypes["virtiofs"] = true 64 | } 65 | if _, ok := validMountTypes[c.MountType]; !ok { 66 | return fmt.Errorf("invalid mountType: '%s'", c.MountType) 67 | } 68 | validVMTypes := map[string]bool{"qemu": true} 69 | if util.MacOS13OrNewer() { 70 | validVMTypes["vz"] = true 71 | } 72 | if _, ok := validVMTypes[c.VMType]; !ok { 73 | return fmt.Errorf("invalid vmType: '%s'", c.VMType) 74 | } 75 | if c.VMType == "qemu" { 76 | if err := util.AssertQemuImg(); err != nil { 77 | return fmt.Errorf("cannot use vmType: '%s', error: %w", c.VMType, err) 78 | } 79 | } 80 | 81 | if c.DiskImage != "" { 82 | if strings.HasPrefix(c.DiskImage, "http://") || strings.HasPrefix(c.DiskImage, "https://") { 83 | return fmt.Errorf("cannot use diskImage: remote URLs not supported, only local files can be specified") 84 | } 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // Load loads the config. 91 | // Error is only returned if the config file exists but could not be loaded. 92 | // No error is returned if the config file does not exist. 93 | func Load() (config.Config, error) { 94 | f := config.CurrentProfile().File() 95 | if _, err := os.Stat(f); err != nil { 96 | oldF := oldConfigFile() 97 | 98 | // config file does not exist, check older version for backward compatibility 99 | if _, err := os.Stat(oldF); err != nil { 100 | return config.Config{}, nil 101 | } 102 | 103 | // older version exists 104 | logrus.Infof("settings from older %s version detected and copied", config.AppName) 105 | if err := cli.Command("cp", oldF, f).Run(); err != nil { 106 | logrus.Warn(fmt.Errorf("error copying config: %w, proceeding with defaults", err)) 107 | return config.Config{}, nil 108 | } 109 | } 110 | 111 | return LoadFrom(f) 112 | } 113 | 114 | // LoadInstance is like Load but returns the config of the currently running instance. 115 | func LoadInstance() (config.Config, error) { 116 | return LoadFrom(config.CurrentProfile().StateFile()) 117 | } 118 | 119 | // Teardown deletes the config. 120 | func Teardown() error { 121 | dir := config.CurrentProfile().ConfigDir() 122 | if _, err := os.Stat(dir); err == nil { 123 | return os.RemoveAll(dir) 124 | } 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /config/files.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "sync" 8 | 9 | "github.com/abiosoft/colima/util" 10 | "github.com/abiosoft/colima/util/fsutil" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // requiredDir is a directory that must exist on the filesystem 15 | type requiredDir struct { 16 | once sync.Once 17 | 18 | // dir is a func to enable deferring the value of the directory 19 | // until execution time. 20 | // if dir() returns an error, a fatal error is triggered. 21 | dir func() (string, error) 22 | 23 | computedDir *string 24 | } 25 | 26 | // Dir returns the directory path. 27 | // It ensures the directory is created on the filesystem by calling 28 | // `mkdir` prior to returning the directory path. 29 | func (r *requiredDir) Dir() string { 30 | if r.computedDir != nil { 31 | return *r.computedDir 32 | } 33 | 34 | dir, err := r.dir() 35 | if err != nil { 36 | logrus.Fatal(fmt.Errorf("cannot fetch required directory: %w", err)) 37 | } 38 | 39 | r.once.Do(func() { 40 | if err := fsutil.MkdirAll(dir, 0755); err != nil { 41 | logrus.Fatal(fmt.Errorf("cannot make required directory: %w", err)) 42 | } 43 | }) 44 | 45 | r.computedDir = &dir 46 | return dir 47 | } 48 | 49 | var ( 50 | configBaseDir = requiredDir{ 51 | dir: func() (string, error) { 52 | // colima home explicit config 53 | dir := os.Getenv("COLIMA_HOME") 54 | if _, err := os.Stat(dir); err == nil { 55 | return dir, nil 56 | } 57 | 58 | // user home directory 59 | homeDir, err := os.UserHomeDir() 60 | if err != nil { 61 | return "", err 62 | } 63 | // colima's config directory based on home directory 64 | dir = filepath.Join(homeDir, ".colima") 65 | // validate existence of colima's config directory 66 | _, err = os.Stat(dir) 67 | 68 | // extra xdg config directory 69 | xdgDir, xdg := os.LookupEnv("XDG_CONFIG_HOME") 70 | 71 | if err == nil { 72 | // ~/.colima is found but xdg dir is set 73 | if xdg { 74 | logrus.Warnln("found ~/.colima, ignoring $XDG_CONFIG_HOME...") 75 | logrus.Warnln("delete ~/.colima to use $XDG_CONFIG_HOME as config directory") 76 | logrus.Warnf("or run `mv ~/.colima \"%s\"`", filepath.Join(xdgDir, "colima")) 77 | } 78 | return dir, nil 79 | } else { 80 | // ~/.colima is missing and xdg dir is set 81 | if xdg { 82 | return filepath.Join(xdgDir, "colima"), nil 83 | } 84 | } 85 | 86 | // macOS users are accustomed to ~/.colima 87 | if util.MacOS() { 88 | return dir, nil 89 | } 90 | 91 | // other environments fall back to user config directory 92 | dir, err = os.UserConfigDir() 93 | if err != nil { 94 | return "", err 95 | } 96 | 97 | return filepath.Join(dir, "colima"), nil 98 | }, 99 | } 100 | 101 | cacheDir = requiredDir{ 102 | dir: func() (string, error) { 103 | dir := os.Getenv("XDG_CACHE_HOME") 104 | if dir != "" { 105 | return filepath.Join(dir, "colima"), nil 106 | } 107 | // else 108 | dir, err := os.UserCacheDir() 109 | if err != nil { 110 | return "", err 111 | } 112 | return filepath.Join(dir, "colima"), nil 113 | }, 114 | } 115 | 116 | templatesDir = requiredDir{ 117 | dir: func() (string, error) { 118 | dir, err := configBaseDir.dir() 119 | if err != nil { 120 | return "", err 121 | } 122 | return filepath.Join(dir, "_templates"), nil 123 | }, 124 | } 125 | 126 | limaDir = requiredDir{ 127 | dir: func() (string, error) { 128 | // if LIMA_HOME env var is set, obey it. 129 | if dir := os.Getenv("LIMA_HOME"); dir != "" { 130 | return dir, nil 131 | } 132 | 133 | dir, err := configBaseDir.dir() 134 | if err != nil { 135 | return "", err 136 | } 137 | return filepath.Join(dir, "_lima"), nil 138 | }, 139 | } 140 | ) 141 | 142 | // CacheDir returns the cache directory. 143 | func CacheDir() string { return cacheDir.Dir() } 144 | 145 | // TemplatesDir returns the templates' directory. 146 | func TemplatesDir() string { return templatesDir.Dir() } 147 | 148 | // LimaDir returns Lima directory. 149 | func LimaDir() string { return limaDir.Dir() } 150 | 151 | const configFileName = "colima.yaml" 152 | 153 | // SSHConfigFile returns the path to generated ssh config. 154 | func SSHConfigFile() string { return filepath.Join(configBaseDir.Dir(), "ssh_config") } 155 | -------------------------------------------------------------------------------- /config/profile.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | ) 7 | 8 | var profile = &Profile{ID: AppName, DisplayName: AppName, ShortName: "default"} 9 | 10 | // SetProfile sets the profile name for the application. 11 | // This is an avenue to test Colima without breaking an existing stable setup. 12 | // Not perfect, but good enough for testing. 13 | func SetProfile(profileName string) { 14 | profile = ProfileFromName(profileName) 15 | } 16 | 17 | // ProfileFromName retrieves profile given name. 18 | func ProfileFromName(name string) *Profile { 19 | var i Profile 20 | 21 | switch name { 22 | case "", AppName, "default": 23 | i.ID = AppName 24 | i.DisplayName = AppName 25 | i.ShortName = "default" 26 | return &i 27 | } 28 | 29 | // sanitize 30 | name = strings.TrimPrefix(name, "colima-") 31 | 32 | // if custom profile is specified, 33 | // use a prefix to prevent possible name clashes 34 | i.ID = "colima-" + name 35 | i.DisplayName = "colima [profile=" + name + "]" 36 | i.ShortName = name 37 | return &i 38 | } 39 | 40 | // CurrentProfile returns the current running profile. 41 | func CurrentProfile() *Profile { return profile } 42 | 43 | // Profile is colima profile. 44 | type Profile struct { 45 | ID string 46 | DisplayName string 47 | ShortName string 48 | 49 | configDir *requiredDir 50 | } 51 | 52 | // ConfigDir returns the configuration directory. 53 | func (p *Profile) ConfigDir() string { 54 | if p.configDir == nil { 55 | p.configDir = &requiredDir{ 56 | dir: func() (string, error) { 57 | return filepath.Join(configBaseDir.Dir(), p.ShortName), nil 58 | }, 59 | } 60 | } 61 | return p.configDir.Dir() 62 | } 63 | 64 | // LimaInstanceDir returns the directory for the Lima instance. 65 | func (p *Profile) LimaInstanceDir() string { 66 | return filepath.Join(limaDir.Dir(), p.ID) 67 | } 68 | 69 | // File returns the path to the config file. 70 | func (p *Profile) File() string { 71 | return filepath.Join(p.ConfigDir(), configFileName) 72 | } 73 | 74 | // LimaFile returns the path to the lima config file. 75 | func (p *Profile) LimaFile() string { 76 | return filepath.Join(p.LimaInstanceDir(), "lima.yaml") 77 | } 78 | 79 | // StateFile returns the path to the state file. 80 | func (p *Profile) StateFile() string { 81 | return filepath.Join(p.LimaInstanceDir(), configFileName) 82 | } 83 | 84 | var _ ProfileInfo = (*Profile)(nil) 85 | 86 | // ProfileInfo is the information about a profile. 87 | type ProfileInfo interface { 88 | // ConfigDir returns the configuration directory. 89 | ConfigDir() string 90 | 91 | // LimaInstanceDir returns the directory for the Lima instance. 92 | LimaInstanceDir() string 93 | 94 | // File returns the path to the config file. 95 | File() string 96 | 97 | // LimaFile returns the path to the lima config file. 98 | LimaFile() string 99 | 100 | // StateFile returns the path to the state file. 101 | StateFile() string 102 | } 103 | -------------------------------------------------------------------------------- /core/core.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/sirupsen/logrus" 10 | 11 | "github.com/abiosoft/colima/cli" 12 | "github.com/abiosoft/colima/environment" 13 | "github.com/coreos/go-semver/semver" 14 | ) 15 | 16 | const limaVersion = "v0.18.0" // minimum Lima version supported 17 | 18 | type ( 19 | hostActions = environment.HostActions 20 | guestActions = environment.GuestActions 21 | ) 22 | 23 | // SetupBinfmt downloads and install binfmt 24 | func SetupBinfmt(host hostActions, guest guestActions, arch environment.Arch) error { 25 | qemuArch := environment.AARCH64 26 | if arch.Value().GoArch() == "arm64" { 27 | qemuArch = environment.X8664 28 | } 29 | 30 | install := func() error { 31 | if err := guest.Run("sh", "-c", "sudo QEMU_PRESERVE_ARGV0=1 /usr/bin/binfmt --install 386,"+qemuArch.GoArch()); err != nil { 32 | return fmt.Errorf("error installing binfmt: %w", err) 33 | } 34 | return nil 35 | } 36 | 37 | // validate binfmt 38 | if err := guest.RunQuiet("command", "-v", "binfmt"); err != nil { 39 | return fmt.Errorf("binfmt not found: %w", err) 40 | } 41 | 42 | return install() 43 | } 44 | 45 | // LimaVersionSupported checks if the currently installed Lima version is supported. 46 | func LimaVersionSupported() error { 47 | var values struct { 48 | Version string `json:"version"` 49 | } 50 | var buf bytes.Buffer 51 | cmd := cli.Command("limactl", "info") 52 | cmd.Stdout = &buf 53 | 54 | if err := cmd.Run(); err != nil { 55 | return fmt.Errorf("error checking Lima version: %w", err) 56 | } 57 | 58 | if err := json.NewDecoder(&buf).Decode(&values); err != nil { 59 | return fmt.Errorf("error decoding 'limactl info' json: %w", err) 60 | } 61 | // remove pre-release hyphen 62 | parts := strings.SplitN(values.Version, "-", 2) 63 | if len(parts) > 0 { 64 | values.Version = parts[0] 65 | } 66 | 67 | if parts[0] == "HEAD" { 68 | logrus.Warnf("to avoid compatibility issues, ensure lima development version (%s) in use is not lower than %s", values.Version, limaVersion) 69 | return nil 70 | } 71 | 72 | min := semver.New(strings.TrimPrefix(limaVersion, "v")) 73 | current, err := semver.NewVersion(strings.TrimPrefix(values.Version, "v")) 74 | if err != nil { 75 | return fmt.Errorf("invalid semver version for Lima: %w", err) 76 | } 77 | 78 | if min.Compare(*current) > 0 { 79 | return fmt.Errorf("minimum Lima version supported is %s, current version is %s", limaVersion, values.Version) 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /daemon/daemon.go: -------------------------------------------------------------------------------- 1 | package daemon 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/abiosoft/colima/cli" 8 | "github.com/abiosoft/colima/config" 9 | "github.com/abiosoft/colima/daemon/process" 10 | "github.com/abiosoft/colima/daemon/process/inotify" 11 | "github.com/abiosoft/colima/daemon/process/vmnet" 12 | "github.com/abiosoft/colima/environment" 13 | "github.com/abiosoft/colima/util" 14 | "github.com/abiosoft/colima/util/fsutil" 15 | "github.com/abiosoft/colima/util/osutil" 16 | ) 17 | 18 | // Manager handles running background processes. 19 | type Manager interface { 20 | Start(context.Context, config.Config) error 21 | Stop(context.Context, config.Config) error 22 | Running(context.Context, config.Config) (Status, error) 23 | Dependencies(context.Context, config.Config) (deps process.Dependency, root bool) 24 | } 25 | 26 | type Status struct { 27 | // Parent process 28 | Running bool 29 | // Subprocesses 30 | Processes []processStatus 31 | } 32 | type processStatus struct { 33 | Name string 34 | Running bool 35 | Error error 36 | } 37 | 38 | // NewManager creates a new process manager. 39 | func NewManager(host environment.HostActions) Manager { 40 | return &processManager{ 41 | host: host, 42 | } 43 | } 44 | 45 | func CtxKey(s string) any { return struct{ key string }{key: s} } 46 | 47 | var _ Manager = (*processManager)(nil) 48 | 49 | type processManager struct { 50 | host environment.HostActions 51 | } 52 | 53 | func (l processManager) Dependencies(ctx context.Context, conf config.Config) (deps process.Dependency, root bool) { 54 | processes := processesFromConfig(conf) 55 | return process.Dependencies(processes...) 56 | } 57 | 58 | func (l processManager) init() error { 59 | // dependencies for network 60 | if err := fsutil.MkdirAll(process.Dir(), 0755); err != nil { 61 | return fmt.Errorf("error preparing vmnet: %w", err) 62 | } 63 | return nil 64 | } 65 | 66 | func (l processManager) Running(ctx context.Context, conf config.Config) (s Status, err error) { 67 | err = l.host.RunQuiet(osutil.Executable(), "daemon", "status", config.CurrentProfile().ShortName) 68 | if err != nil { 69 | return 70 | } 71 | s.Running = true 72 | 73 | ctx = context.WithValue(ctx, process.CtxKeyDaemon(), s.Running) 74 | 75 | for _, p := range processesFromConfig(conf) { 76 | pErr := p.Alive(ctx) 77 | s.Processes = append(s.Processes, processStatus{ 78 | Name: p.Name(), 79 | Running: pErr == nil, 80 | Error: pErr, 81 | }) 82 | } 83 | return 84 | } 85 | 86 | func (l processManager) Start(ctx context.Context, conf config.Config) error { 87 | _ = l.Stop(ctx, conf) // this is safe, nothing is done when not running 88 | 89 | if err := l.init(); err != nil { 90 | return fmt.Errorf("error preparing daemon directory: %w", err) 91 | } 92 | 93 | args := []string{osutil.Executable(), "daemon", "start", config.CurrentProfile().ShortName} 94 | 95 | if conf.Network.Address { 96 | args = append(args, "--vmnet") 97 | } 98 | if conf.MountINotify { 99 | args = append(args, "--inotify") 100 | args = append(args, "--inotify-runtime", conf.Runtime) 101 | for _, mount := range conf.MountsOrDefault() { 102 | p, err := util.CleanPath(mount.Location) 103 | if err != nil { 104 | return fmt.Errorf("error sanitising mount path for inotify: %w", err) 105 | } 106 | args = append(args, "--inotify-dir", p) 107 | } 108 | } 109 | 110 | if cli.Settings.Verbose { 111 | args = append(args, "--very-verbose") 112 | } 113 | 114 | host := l.host.WithDir(util.HomeDir()) 115 | return host.RunQuiet(args...) 116 | } 117 | func (l processManager) Stop(ctx context.Context, conf config.Config) error { 118 | if s, err := l.Running(ctx, conf); err != nil || !s.Running { 119 | return nil 120 | } 121 | return l.host.RunQuiet(osutil.Executable(), "daemon", "stop", config.CurrentProfile().ShortName) 122 | } 123 | 124 | func processesFromConfig(conf config.Config) []process.Process { 125 | var processes []process.Process 126 | 127 | if conf.Network.Address { 128 | processes = append(processes, vmnet.New()) 129 | } 130 | if conf.MountINotify { 131 | processes = append(processes, inotify.New()) 132 | } 133 | 134 | return processes 135 | } 136 | -------------------------------------------------------------------------------- /daemon/process/inotify/events.go: -------------------------------------------------------------------------------- 1 | package inotify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/fs" 7 | "time" 8 | ) 9 | 10 | type modEvent struct { 11 | path string // filename 12 | fs.FileMode 13 | } 14 | 15 | func (m modEvent) Mode() string { return fmt.Sprintf("%o", m.FileMode) } 16 | 17 | func (f *inotifyProcess) handleEvents(ctx context.Context, watcher dirWatcher) error { 18 | log := f.log 19 | log.Trace("begin inotify event handler") 20 | 21 | mod := make(chan modEvent) 22 | vols := make(chan []string) 23 | 24 | if err := f.monitorContainerVolumes(ctx, vols); err != nil { 25 | return fmt.Errorf("error watching container volumes: %w", err) 26 | } 27 | 28 | var last time.Time 29 | var cancelWatch context.CancelFunc 30 | var currentVols []string 31 | 32 | volsChanged := func(vols []string) bool { 33 | if len(currentVols) != len(vols) { 34 | return true 35 | } 36 | for i := range vols { 37 | if vols[i] != currentVols[i] { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | 44 | cache := map[string]struct{}{} 45 | 46 | for { 47 | select { 48 | 49 | // exit signal 50 | case <-ctx.Done(): 51 | close(mod) 52 | return ctx.Err() 53 | 54 | // watch only container volumes 55 | case vols := <-vols: 56 | if !volsChanged(vols) { 57 | continue 58 | } 59 | log.Tracef("volumes changed from: %+v, to: %+v", currentVols, vols) 60 | 61 | currentVols = vols 62 | 63 | if cancel := cancelWatch; cancel != nil { 64 | // delay a bit to avoid zero downtime 65 | time.AfterFunc(time.Second*1, cancel) 66 | } 67 | 68 | ctx, cancel := context.WithCancel(ctx) 69 | cancelWatch = cancel 70 | 71 | go func(ctx context.Context, vols []string, mod chan<- modEvent) { 72 | if err := watcher.Watch(ctx, vols, mod); err != nil { 73 | log.Error(fmt.Errorf("error running watcher: %w", err)) 74 | } 75 | }(ctx, vols, mod) 76 | 77 | // handle modification events 78 | case ev := <-mod: 79 | now := time.Now() 80 | 81 | // rate limit, handle at most 50 unique items every 500 ms 82 | if now.Sub(last) < time.Millisecond*500 { 83 | if _, ok := cache[ev.path]; ok { 84 | continue // handled, ignore 85 | } 86 | if len(cache) > 50 { 87 | continue 88 | } 89 | } else { 90 | last = now 91 | cache = map[string]struct{}{} // >500ms, reset unique cache 92 | } 93 | 94 | // cache current event 95 | cache[ev.path] = struct{}{} 96 | 97 | // validate that file exists 98 | if err := f.guest.RunQuiet("stat", ev.path); err != nil { 99 | log.Trace(fmt.Errorf("cannot stat '%s': %w", ev.path, err)) 100 | continue 101 | } 102 | 103 | log.Infof("syncing inotify event for %s ", ev.path) 104 | if err := f.guest.RunQuiet("sudo", "/bin/chmod", ev.Mode(), ev.path); err != nil { 105 | log.Trace(fmt.Errorf("error syncing inotify event: %w", err)) 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /daemon/process/inotify/inotify.go: -------------------------------------------------------------------------------- 1 | package inotify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/abiosoft/colima/daemon/process" 9 | "github.com/abiosoft/colima/environment" 10 | "github.com/abiosoft/colima/environment/vm/lima/limautil" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | const Name = "inotify" 15 | const volumesInterval = 5 * time.Second 16 | 17 | type Args struct { 18 | environment.GuestActions 19 | Dirs []string 20 | Runtime string 21 | } 22 | 23 | func CtxKeyArgs() any { return struct{ name string }{name: "inotify_args"} } 24 | 25 | // New returns inotify process. 26 | func New() process.Process { 27 | return &inotifyProcess{ 28 | log: logrus.WithField("context", "inotify"), 29 | } 30 | } 31 | 32 | var _ process.Process = (*inotifyProcess)(nil) 33 | 34 | type inotifyProcess struct { 35 | vmVols []string 36 | guest environment.GuestActions 37 | runtime string 38 | 39 | log *logrus.Entry 40 | } 41 | 42 | // Alive implements process.Process 43 | func (f *inotifyProcess) Alive(ctx context.Context) error { 44 | daemonRunning, _ := ctx.Value(process.CtxKeyDaemon()).(bool) 45 | 46 | // if the parent is active, we can assume inotify is active. 47 | if daemonRunning { 48 | return nil 49 | } 50 | return fmt.Errorf("inotify not running") 51 | } 52 | 53 | // Dependencies implements process.Process 54 | func (*inotifyProcess) Dependencies() (deps []process.Dependency, root bool) { 55 | return nil, false 56 | } 57 | 58 | // Name implements process.Process 59 | func (*inotifyProcess) Name() string { 60 | return Name 61 | } 62 | 63 | // Start implements process.Process 64 | func (f *inotifyProcess) Start(ctx context.Context) error { 65 | args, ok := ctx.Value(CtxKeyArgs()).(Args) 66 | if !ok { 67 | return fmt.Errorf("args missing in context") 68 | } 69 | f.vmVols = omitChildrenDirectories(args.Dirs) 70 | 71 | f.guest = args.GuestActions 72 | f.runtime = args.Runtime 73 | log := f.log 74 | 75 | log.Info("waiting for VM to start") 76 | f.waitForLima(ctx) 77 | log.Info("VM started") 78 | 79 | watcher := &defaultWatcher{log: log} 80 | 81 | return f.handleEvents(ctx, watcher) 82 | } 83 | 84 | // waitForLima waits until lima starts and sets the directory to watch. 85 | func (f *inotifyProcess) waitForLima(ctx context.Context) { 86 | log := f.log 87 | 88 | // wait for Lima to finish starting 89 | for { 90 | log.Info("waiting 5 secs for VM") 91 | 92 | // 5 second interval 93 | after := time.After(time.Second * 5) 94 | 95 | select { 96 | case <-ctx.Done(): 97 | return 98 | case <-after: 99 | i, err := limautil.Instance() 100 | if err != nil || !i.Running() { 101 | continue 102 | } 103 | if err := f.guest.RunQuiet("uname", "-a"); err == nil { 104 | return 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /daemon/process/inotify/volumes.go: -------------------------------------------------------------------------------- 1 | package inotify 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "sort" 9 | "strings" 10 | "time" 11 | 12 | "github.com/abiosoft/colima/environment/container/containerd" 13 | "github.com/abiosoft/colima/environment/container/docker" 14 | ) 15 | 16 | func (f *inotifyProcess) monitorContainerVolumes(ctx context.Context, c chan<- []string) error { 17 | log := f.log 18 | 19 | if f.runtime == "" { 20 | return fmt.Errorf("empty runtime") 21 | } 22 | 23 | fetch := func() ([]string, error) { 24 | var vols []string 25 | 26 | // docker 27 | if f.runtime != containerd.Name { 28 | vols, err := f.fetchVolumes(docker.Name) 29 | if err != nil { 30 | return nil, fmt.Errorf("error fetching docker volumes: %w", err) 31 | } 32 | return vols, nil 33 | } 34 | 35 | // containerd 36 | var namespaces []string 37 | out, err := f.guest.RunOutput("sudo", "nerdctl", "namespace", "list", "-q") 38 | if err != nil { 39 | return nil, fmt.Errorf("error retrieving containerd namespaces: %w", err) 40 | } 41 | if out != "" { 42 | namespaces = strings.Fields(out) 43 | } 44 | 45 | for _, ns := range namespaces { 46 | v, err := f.fetchVolumes("sudo", "nerdctl", "--namespace", ns) 47 | if err != nil { 48 | return nil, fmt.Errorf("error retrieving containerd volumes: %w", err) 49 | } 50 | if len(v) > 0 { 51 | vols = append(vols, v...) 52 | } 53 | } 54 | 55 | return vols, nil 56 | } 57 | 58 | go func() { 59 | for { 60 | select { 61 | case <-ctx.Done(): 62 | log.Trace("stop signal received") 63 | err := ctx.Err() 64 | if err != nil { 65 | log.Trace(fmt.Errorf("error during stop: %w", err)) 66 | } 67 | case <-time.After(volumesInterval): 68 | if vols, err := fetch(); err != nil { 69 | log.Error(err) 70 | } else { 71 | c <- vols 72 | } 73 | } 74 | } 75 | }() 76 | 77 | return nil 78 | } 79 | 80 | func (f *inotifyProcess) fetchVolumes(cmdArgs ...string) ([]string, error) { 81 | log := f.log 82 | 83 | // fetch all containers 84 | var containers []string 85 | { 86 | args := append([]string{}, cmdArgs...) 87 | args = append(args, "ps", "-q") 88 | out, err := f.guest.RunOutput(args...) 89 | if err != nil { 90 | return nil, fmt.Errorf("error listing containers: %w", err) 91 | } 92 | containers = strings.Fields(out) 93 | if len(containers) == 0 { 94 | return nil, nil 95 | } 96 | } 97 | 98 | log.Tracef("found containers %+v", containers) 99 | 100 | // fetch volumes 101 | var resp []struct { 102 | Mounts []struct { 103 | Source string `json:"Source"` 104 | } `json:"Mounts"` 105 | } 106 | { 107 | args := append([]string{}, cmdArgs...) 108 | args = append(args, "inspect") 109 | args = append(args, containers...) 110 | 111 | var buf bytes.Buffer 112 | if err := f.guest.RunWith(nil, &buf, args...); err != nil { 113 | return nil, fmt.Errorf("error inspecting containers: %w", err) 114 | } 115 | if err := json.NewDecoder(&buf).Decode(&resp); err != nil { 116 | return nil, fmt.Errorf("error decoding docker response") 117 | } 118 | } 119 | 120 | // process and discard redundant volumes 121 | vols := []string{} 122 | { 123 | shouldMount := func(child string) bool { 124 | // ignore all invalid directories. 125 | // i.e. directories not within the mounted VM directories 126 | for _, parent := range f.vmVols { 127 | if strings.HasPrefix(child, parent) { 128 | return true 129 | } 130 | } 131 | return false 132 | } 133 | 134 | for _, r := range resp { 135 | for _, mount := range r.Mounts { 136 | if shouldMount(mount.Source) { 137 | vols = append(vols, mount.Source) 138 | } 139 | } 140 | } 141 | 142 | vols = omitChildrenDirectories(vols) 143 | log.Tracef("found volumes %+v", vols) 144 | } 145 | 146 | return vols, nil 147 | } 148 | 149 | func omitChildrenDirectories(dirs []string) []string { 150 | sort.Strings(dirs) // sort to put the parent directories first 151 | 152 | // keep track for uniqueness 153 | set := map[string]struct{}{} 154 | 155 | var newVols []string 156 | 157 | omitted := map[int]struct{}{} 158 | for i := 0; i < len(dirs); i++ { 159 | // if the index is omitted, skip 160 | if _, ok := omitted[i]; ok { 161 | continue 162 | } 163 | 164 | parent := dirs[i] 165 | if _, ok := set[parent]; !ok { 166 | newVols = append(newVols, parent) 167 | set[parent] = struct{}{} 168 | } 169 | 170 | for j := i + 1; j < len(dirs); j++ { 171 | child := dirs[j] 172 | if strings.HasPrefix(child, strings.TrimSuffix(parent, "/")+"/") { 173 | omitted[j] = struct{}{} 174 | } 175 | } 176 | } 177 | 178 | return newVols 179 | } 180 | -------------------------------------------------------------------------------- /daemon/process/inotify/volumes_test.go: -------------------------------------------------------------------------------- 1 | package inotify 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | func Test_omitChildrenDirectories(t *testing.T) { 10 | tests := []struct { 11 | args []string 12 | want []string 13 | }{ 14 | { 15 | args: []string{"/", "/user", "/user/someone", "/a", "/a/ee", "/a/bb"}, 16 | want: []string{"/"}, 17 | }, 18 | { 19 | args: []string{"/someone", "/user", "/user/someone", "/a", "/a/ee", "/a/bb", "/a"}, 20 | want: []string{"/a", "/someone", "/user"}, 21 | }, 22 | { 23 | args: []string{"/someone", "/user/colima/projects/myworks", "/user/colima/projects", "/user/colima/projects/myworks", "/user/colima/projects", "/someone"}, 24 | want: []string{"/someone", "/user/colima/projects"}, 25 | }, 26 | { 27 | args: []string{"/someone", "/user/colima/projects/myworks", "/user/colima/projects"}, 28 | want: []string{"/someone", "/user/colima/projects"}, 29 | }, 30 | { 31 | args: []string{"/user/colima/projects"}, 32 | want: []string{"/user/colima/projects"}, 33 | }, 34 | } 35 | for i, tt := range tests { 36 | t.Run(strconv.Itoa(i), func(t *testing.T) { 37 | if got := omitChildrenDirectories(tt.args); !reflect.DeepEqual(got, tt.want) { 38 | t.Errorf("omitChildrenDirectories() = %v, want %v", got, tt.want) 39 | } 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /daemon/process/inotify/watch.go: -------------------------------------------------------------------------------- 1 | package inotify 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/abiosoft/colima/util" 9 | "github.com/rjeczalik/notify" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type dirWatcher interface { 14 | // Watch watches directories recursively for changes and sends message via c on 15 | // modifications to files within the watched directories. 16 | // 17 | // Watch returns immediately and runs the watcher in the background. 18 | // An error is returned when the watcher can not be started in background. 19 | // 20 | // The watcher terminates on fatal error or when ctx is done. 21 | Watch(ctx context.Context, dirs []string, c chan<- modEvent) error 22 | } 23 | 24 | type defaultWatcher struct { 25 | log *logrus.Entry 26 | } 27 | 28 | // Watch implements dirWatcher 29 | func (d *defaultWatcher) Watch(ctx context.Context, dirs []string, mod chan<- modEvent) error { 30 | log := d.log 31 | c := make(chan notify.EventInfo, 1) 32 | 33 | for _, dir := range dirs { 34 | dir, err := util.CleanPath(dir) 35 | if err != nil { 36 | return fmt.Errorf("invalid directory: %w", err) 37 | } 38 | err = notify.Watch(dir+"...", c, notify.Write) 39 | if err != nil { 40 | return fmt.Errorf("error watching directory recursively '%s': %w", dir, err) 41 | } 42 | } 43 | 44 | go func(ctx context.Context, c chan notify.EventInfo, mod chan<- modEvent) { 45 | for { 46 | select { 47 | 48 | case <-ctx.Done(): 49 | notify.Stop(c) 50 | log.Trace("stopping watcher") 51 | if err := ctx.Err(); err != nil { 52 | log.Trace(fmt.Errorf("error found in ctx: %w", err)) 53 | return 54 | } 55 | 56 | case e := <-c: 57 | path := e.Path() 58 | 59 | log.Tracef("received event %s for %s", e.Event().String(), path) 60 | 61 | stat, err := os.Stat(path) 62 | if err != nil { 63 | log.Trace(fmt.Errorf("unable to stat inotify file '%s': %w", path, err)) 64 | continue 65 | } 66 | 67 | if stat.IsDir() { 68 | log.Tracef("'%s' is directory, ignoring.", path) 69 | continue 70 | } 71 | 72 | // send modification event 73 | mod <- modEvent{path: path, FileMode: stat.Mode()} 74 | } 75 | } 76 | }(ctx, c, mod) 77 | 78 | return nil 79 | } 80 | 81 | var _ dirWatcher = (*defaultWatcher)(nil) 82 | -------------------------------------------------------------------------------- /daemon/process/process.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/abiosoft/colima/config" 9 | 10 | "github.com/abiosoft/colima/environment" 11 | ) 12 | 13 | func CtxKeyDaemon() any { return struct{ key string }{key: "colima_daemon"} } 14 | 15 | // Process is a background process managed by the daemon. 16 | type Process interface { 17 | // Name for the background process 18 | Name() string 19 | // Start starts the background process. 20 | // The process is expected to terminate when ctx is done. 21 | Start(ctx context.Context) error 22 | // Alive checks if the process is the alive. 23 | Alive(ctx context.Context) error 24 | // Dependencies are requirements for start to succeed. 25 | // root should be true if root access is required for 26 | // installing any of the dependencies. 27 | Dependencies() (deps []Dependency, root bool) 28 | } 29 | 30 | // Dir is the directory for daemon files. 31 | func Dir() string { return filepath.Join(config.CurrentProfile().ConfigDir(), "daemon") } 32 | 33 | // Dependency is a requirement to be fulfilled before a process can be started. 34 | type Dependency interface { 35 | Installed() bool 36 | Install(environment.HostActions) error 37 | } 38 | 39 | // Dependencies returns the dependencies for the processes. 40 | // root returns if root access is required 41 | func Dependencies(processes ...Process) (deps Dependency, root bool) { 42 | // check rootful for user info message 43 | rootful := false 44 | for _, p := range processes { 45 | deps, root := p.Dependencies() 46 | for _, dep := range deps { 47 | if !dep.Installed() && root { 48 | rootful = true 49 | break 50 | } 51 | } 52 | } 53 | 54 | return processDeps(processes), rootful 55 | } 56 | 57 | type processDeps []Process 58 | 59 | func (p processDeps) Installed() bool { 60 | for _, process := range p { 61 | deps, _ := process.Dependencies() 62 | for _, d := range deps { 63 | if !d.Installed() { 64 | return false 65 | } 66 | } 67 | } 68 | 69 | return true 70 | } 71 | 72 | func (p processDeps) Install(host environment.HostActions) error { 73 | for _, process := range p { 74 | deps, _ := process.Dependencies() 75 | for _, d := range deps { 76 | if !d.Installed() { 77 | if err := d.Install(host); err != nil { 78 | return fmt.Errorf("error occurred installing dependencies for '%s': %w", process.Name(), err) 79 | } 80 | } 81 | } 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /daemon/process/vmnet/deps.go: -------------------------------------------------------------------------------- 1 | package vmnet 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/abiosoft/colima/daemon/process" 12 | "github.com/abiosoft/colima/embedded" 13 | "github.com/abiosoft/colima/environment" 14 | ) 15 | 16 | var _ process.Dependency = sudoerFile{} 17 | 18 | type sudoerFile struct{} 19 | 20 | // Installed implements Dependency 21 | func (s sudoerFile) Installed() bool { 22 | if _, err := os.Stat(s.path()); err != nil { 23 | return false 24 | } 25 | b, err := os.ReadFile(s.path()) 26 | if err != nil { 27 | return false 28 | } 29 | txt, err := embedded.Read(s.embeddedPath()) 30 | if err != nil { 31 | return false 32 | } 33 | return bytes.Contains(b, txt) 34 | } 35 | 36 | func (s sudoerFile) path() string { return "/etc/sudoers.d/colima" } 37 | func (s sudoerFile) embeddedPath() string { return "network/sudo.txt" } 38 | func (s sudoerFile) Install(host environment.HostActions) error { 39 | // read embedded file contents 40 | txt, err := embedded.ReadString("network/sudo.txt") 41 | if err != nil { 42 | return fmt.Errorf("error retrieving embedded sudo file: %w", err) 43 | } 44 | // ensure parent directory exists 45 | dir := filepath.Dir(s.path()) 46 | if err := host.RunInteractive("sudo", "mkdir", "-p", dir); err != nil { 47 | return fmt.Errorf("error preparing sudoers directory: %w", err) 48 | } 49 | // persist file to desired location 50 | stdin := strings.NewReader(txt) 51 | stdout := &bytes.Buffer{} 52 | if err := host.RunWith(stdin, stdout, "sudo", "sh", "-c", "cat > "+s.path()); err != nil { 53 | return fmt.Errorf("error writing sudoers file, stderr: %s, err: %w", stdout.String(), err) 54 | } 55 | return nil 56 | } 57 | 58 | var _ process.Dependency = vmnetFile{} 59 | 60 | const BinaryPath = "/opt/colima/bin/socket_vmnet" 61 | const ClientBinaryPath = "/opt/colima/bin/socket_vmnet_client" 62 | 63 | type vmnetFile struct{} 64 | 65 | // Installed implements Dependency 66 | func (v vmnetFile) Installed() bool { 67 | for _, bin := range v.bins() { 68 | if _, err := os.Stat(bin); err != nil { 69 | return false 70 | } 71 | } 72 | return true 73 | } 74 | 75 | func (v vmnetFile) bins() []string { 76 | return []string{BinaryPath, ClientBinaryPath} 77 | } 78 | func (v vmnetFile) Install(host environment.HostActions) error { 79 | arch := "x86_64" 80 | if runtime.GOARCH != "amd64" { 81 | arch = "arm64" 82 | } 83 | 84 | // read the embedded file 85 | gz, err := embedded.Read("network/vmnet_" + arch + ".tar.gz") 86 | if err != nil { 87 | return fmt.Errorf("error retrieving embedded vmnet file: %w", err) 88 | } 89 | 90 | // write tar to tmp directory 91 | f, err := os.CreateTemp("", "vmnet.tar.gz") 92 | if err != nil { 93 | return fmt.Errorf("error creating temp file: %w", err) 94 | } 95 | if _, err := f.Write(gz); err != nil { 96 | return fmt.Errorf("error writing temp file: %w", err) 97 | } 98 | _ = f.Close() // not a fatal error 99 | 100 | defer func() { 101 | _ = os.Remove(f.Name()) 102 | }() 103 | 104 | // extract tar to desired location 105 | dir := optDir 106 | if err := host.RunInteractive("sudo", "mkdir", "-p", dir); err != nil { 107 | return fmt.Errorf("error preparing colima privileged dir: %w", err) 108 | } 109 | if err := host.RunInteractive("sudo", "sh", "-c", fmt.Sprintf("cd %s && tar xfz %s 2>/dev/null", dir, f.Name())); err != nil { 110 | return fmt.Errorf("error extracting vmnet archive: %w", err) 111 | } 112 | 113 | return nil 114 | } 115 | 116 | var _ process.Dependency = vmnetRunDir{} 117 | 118 | type vmnetRunDir struct{} 119 | 120 | // Install implements Dependency 121 | func (v vmnetRunDir) Install(host environment.HostActions) error { 122 | return host.RunInteractive("sudo", "mkdir", "-p", runDir()) 123 | } 124 | 125 | // Installed implements Dependency 126 | func (v vmnetRunDir) Installed() bool { 127 | stat, err := os.Stat(runDir()) 128 | return err == nil && stat.IsDir() 129 | } 130 | 131 | const optDir = "/opt/colima" 132 | 133 | // runDir is the directory to the rootful daemon run related files. e.g. pid files 134 | func runDir() string { return filepath.Join(optDir, "run") } 135 | -------------------------------------------------------------------------------- /daemon/process/vmnet/vmnet.go: -------------------------------------------------------------------------------- 1 | package vmnet 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | 11 | "github.com/abiosoft/colima/cli" 12 | "github.com/abiosoft/colima/config" 13 | "github.com/abiosoft/colima/daemon/process" 14 | "github.com/abiosoft/colima/util/osutil" 15 | "github.com/sirupsen/logrus" 16 | ) 17 | 18 | const Name = "vmnet" 19 | 20 | const ( 21 | SubProcessEnvVar = "COLIMA_VMNET" 22 | 23 | NetGateway = "192.168.106.1" 24 | NetDHCPEnd = "192.168.106.254" 25 | ) 26 | 27 | var _ process.Process = (*vmnetProcess)(nil) 28 | 29 | func New() process.Process { return &vmnetProcess{} } 30 | 31 | type vmnetProcess struct{} 32 | 33 | func (*vmnetProcess) Alive(ctx context.Context) error { 34 | info := Info() 35 | pidFile := info.PidFile 36 | socketFile := info.Socket.File() 37 | 38 | if _, err := os.Stat(pidFile); err == nil { 39 | cmd := exec.CommandContext(ctx, "sudo", "/usr/bin/pkill", "-0", "-F", pidFile) 40 | if err := cmd.Run(); err != nil { 41 | return fmt.Errorf("error checking vmnet process: %w", err) 42 | } 43 | } 44 | 45 | if _, err := os.Stat(socketFile); err != nil { 46 | return fmt.Errorf("vmnet socket file not found error: %w", err) 47 | } 48 | if n, err := net.Dial("unix", socketFile); err != nil { 49 | return fmt.Errorf("vmnet socket file error: %w", err) 50 | } else { 51 | if err := n.Close(); err != nil { 52 | logrus.Debugln(fmt.Errorf("error closing ping socket connection: %w", err)) 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | 59 | // Name implements process.BgProcess 60 | func (*vmnetProcess) Name() string { return Name } 61 | 62 | // Start implements process.BgProcess 63 | func (*vmnetProcess) Start(ctx context.Context) error { 64 | info := Info() 65 | socket := info.Socket.File() 66 | pid := info.PidFile 67 | 68 | // delete existing sockets if exist 69 | // errors ignored on purpose 70 | _ = forceDeleteFileIfExists(socket) 71 | 72 | done := make(chan error, 1) 73 | 74 | go func() { 75 | // rootfully start the vmnet daemon 76 | command := cli.CommandInteractive("sudo", BinaryPath, 77 | "--vmnet-mode", "shared", 78 | "--socket-group", "staff", 79 | "--vmnet-gateway", NetGateway, 80 | "--vmnet-dhcp-end", NetDHCPEnd, 81 | "--pidfile", pid, 82 | socket, 83 | ) 84 | 85 | if cli.Settings.Verbose { 86 | command.Env = append(command.Env, os.Environ()...) 87 | command.Env = append(command.Env, "DEBUG=1") 88 | } 89 | 90 | done <- command.Run() 91 | }() 92 | 93 | select { 94 | case <-ctx.Done(): 95 | if err := stop(pid); err != nil { 96 | return fmt.Errorf("error stopping vmnet: %w", err) 97 | } 98 | case err := <-done: 99 | if err != nil { 100 | return fmt.Errorf("error running vmnet: %w", err) 101 | } 102 | } 103 | 104 | return nil 105 | } 106 | 107 | func (vmnetProcess) Dependencies() (deps []process.Dependency, root bool) { 108 | return []process.Dependency{ 109 | sudoerFile{}, 110 | vmnetFile{}, 111 | vmnetRunDir{}, 112 | }, true 113 | } 114 | 115 | func stop(pidFile string) error { 116 | // rootfully kill the vmnet process. 117 | // process is only assumed alive if the pidfile exists 118 | if _, err := os.Stat(pidFile); err == nil { 119 | if err := cli.CommandInteractive("sudo", "/usr/bin/pkill", "-F", pidFile).Run(); err != nil { 120 | return fmt.Errorf("error killing vmnet process: %w", err) 121 | } 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func forceDeleteFileIfExists(name string) error { 128 | if stat, err := os.Stat(name); err == nil && !stat.IsDir() { 129 | return os.Remove(name) 130 | } 131 | return nil 132 | } 133 | 134 | func Info() struct { 135 | PidFile string 136 | Socket osutil.Socket 137 | } { 138 | return struct { 139 | PidFile string 140 | Socket osutil.Socket 141 | }{ 142 | PidFile: filepath.Join(runDir(), "vmnet-"+config.CurrentProfile().ShortName+".pid"), 143 | Socket: osutil.Socket(filepath.Join(process.Dir(), "vmnet.sock")), 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | with import { }; 2 | callPackage (import ./colima.nix) { } 3 | -------------------------------------------------------------------------------- /docs/INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installation Options 2 | 3 | ## Homebrew 4 | 5 | Stable Version 6 | 7 | ``` 8 | brew install colima 9 | ``` 10 | 11 | Development Version 12 | 13 | ``` 14 | brew install --HEAD colima 15 | ``` 16 | 17 | ## MacPorts 18 | 19 | Stable version 20 | 21 | ``` 22 | sudo port install colima 23 | ``` 24 | 25 | ## Nix 26 | 27 | Only stable Version 28 | 29 | ``` 30 | nix-env -i colima 31 | ``` 32 | 33 | Or using solely in a `nix-shell` 34 | 35 | ``` 36 | nix-shell -p colima 37 | ``` 38 | 39 | ## Arch 40 | 41 | Install dependencies 42 | ``` 43 | sudo pacman -S qemu-base go docker 44 | ``` 45 | Install Lima and Colima from Aur 46 | ``` 47 | yay -S lima-bin colima-bin 48 | ``` 49 | 50 | 51 | ## Binary 52 | 53 | Binaries are available with every release on the [releases page](https://github.com/abiosoft/colima/releases). 54 | 55 | ```sh 56 | # download binary 57 | curl -LO https://github.com/abiosoft/colima/releases/latest/download/colima-$(uname)-$(uname -m) 58 | 59 | # install in $PATH 60 | sudo install colima-$(uname)-$(uname -m) /usr/local/bin/colima 61 | ``` 62 | 63 | ## Building from Source 64 | 65 | Requires [Go](https://golang.org). 66 | 67 | ```sh 68 | # clone repo and cd into it 69 | git clone https://github.com/abiosoft/colima 70 | cd colima 71 | make 72 | sudo make install 73 | ``` 74 | -------------------------------------------------------------------------------- /embedded/defaults/abort.yaml: -------------------------------------------------------------------------------- 1 | # ============================================================================================ # 2 | # To abort, delete the contents of this file including the comments and save as an empty file 3 | # ============================================================================================ # 4 | -------------------------------------------------------------------------------- /embedded/defaults/template.yaml: -------------------------------------------------------------------------------- 1 | # New instances will be created with the following configurations. 2 | -------------------------------------------------------------------------------- /embedded/embed.go: -------------------------------------------------------------------------------- 1 | package embedded 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed network k3s defaults images 8 | var fs embed.FS 9 | 10 | // FS returns the underlying embed.FS 11 | func FS() embed.FS { return fs } 12 | 13 | func read(file string) ([]byte, error) { return fs.ReadFile(file) } 14 | 15 | // Read reads the content of file 16 | func Read(file string) ([]byte, error) { return read(file) } 17 | 18 | // ReadString reads the content of file as string 19 | func ReadString(file string) (string, error) { 20 | b, err := read(file) 21 | return string(b), err 22 | } 23 | -------------------------------------------------------------------------------- /embedded/images/images.txt: -------------------------------------------------------------------------------- 1 | arm64 none https://github.com/abiosoft/colima-core/releases/download/v0.8.1/ubuntu-24.04-minimal-cloudimg-arm64-none.qcow2 7aea3d020a41212420b93008f45f7ad340b15a9b86a8b55780546ae4d63566bce7839cb419f610aa9273fafd0fb5c7b240846d2b24199fe76cbd387536c068f6 ubuntu-24.04-minimal-cloudimg-arm64-none.qcow2 2 | arm64 docker https://github.com/abiosoft/colima-core/releases/download/v0.8.1/ubuntu-24.04-minimal-cloudimg-arm64-docker.qcow2 90ffaa39abec6e65665ed82f9a71e11f43c458c5ea401b8e1bdbdfedcf5ddad572445ad8aaf2790899e2d9547337ff0bd9e9ba1c03b200b54998f0cdb0ef14c0 ubuntu-24.04-minimal-cloudimg-arm64-docker.qcow2 3 | arm64 containerd https://github.com/abiosoft/colima-core/releases/download/v0.8.1/ubuntu-24.04-minimal-cloudimg-arm64-containerd.qcow2 3f4562b6f4ca27ec98e630e97949301fb6ee30c6ec94c34a2bbe6961ddbae3cfe0c478f2178036d2d52e33e6bfad237ffe59c4436fce715b1416e8c4ecf96ab5 ubuntu-24.04-minimal-cloudimg-arm64-containerd.qcow2 4 | arm64 incus https://github.com/abiosoft/colima-core/releases/download/v0.8.1/ubuntu-24.04-minimal-cloudimg-arm64-incus.qcow2 cbb8be17f298fecb39d4dd28f0176df3a81f85ee53c92a1337c12a2eec15b7ad1dd6faef1bbe6611ba8a82b31054c1c9ac2a0803a7ff848b7638929215efc334 ubuntu-24.04-minimal-cloudimg-arm64-incus.qcow2 5 | amd64 none https://github.com/abiosoft/colima-core/releases/download/v0.8.1/ubuntu-24.04-minimal-cloudimg-amd64-none.qcow2 13bca37b959fe941679e05799ebe10deb9bfac4849ce41d64915286f6160b6a2e5f0d85bc505ec163faf0c4c0b598578e5b915a2c17f9895dbfe5c436ba8b7ab ubuntu-24.04-minimal-cloudimg-amd64-none.qcow2 6 | amd64 docker https://github.com/abiosoft/colima-core/releases/download/v0.8.1/ubuntu-24.04-minimal-cloudimg-amd64-docker.qcow2 0b3ae72577efeb3e658fe9a149ba74e1c388629b1ce8da36b9162b8306d6c0c0c4f6d77379ac03ff41e8fffb0bc31e1f670fcf0e5139ca30402fc5dfb2e82ac1 ubuntu-24.04-minimal-cloudimg-amd64-docker.qcow2 7 | amd64 containerd https://github.com/abiosoft/colima-core/releases/download/v0.8.1/ubuntu-24.04-minimal-cloudimg-amd64-containerd.qcow2 62841fd2df09c13fb24c6a687908c18da6452e451f5ae9bb47979a7a46aa4a2de702f4140f36d0fb68f65c92a549f2e017765cedd7b88461b235468d9ae4504e ubuntu-24.04-minimal-cloudimg-amd64-containerd.qcow2 8 | amd64 incus https://github.com/abiosoft/colima-core/releases/download/v0.8.1/ubuntu-24.04-minimal-cloudimg-amd64-incus.qcow2 67b7e804cd1cbb09890470c044b6eccdf9abec364c712294c7d94706b9bced507bf158a1a303480ec2900344644d54eeafdcd994fcbf9b808675f086143e6934 ubuntu-24.04-minimal-cloudimg-amd64-incus.qcow2 9 | -------------------------------------------------------------------------------- /embedded/images/images_sha.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | BASE_URL=https://github.com/abiosoft/colima-core/releases/download 6 | BASE_FILENAME=ubuntu-24.04-minimal-cloudimg 7 | VERSION=v0.8.1 8 | RUNTIMES="none docker containerd incus" 9 | ARCHS="arm64 amd64" 10 | 11 | DIR="$(dirname $0)" 12 | FILE="${DIR}/images.txt" 13 | 14 | # reset output files 15 | echo -n >$FILE 16 | 17 | for arch in ${ARCHS}; do 18 | for runtime in ${RUNTIMES}; do 19 | URL="${BASE_URL}/${VERSION}/${BASE_FILENAME}-${arch}-${runtime}.qcow2" 20 | SHA="$(curl -sL ${URL}.sha512sum)" 21 | echo "$arch $runtime ${URL} ${SHA}" >>$FILE 22 | done 23 | done 24 | -------------------------------------------------------------------------------- /embedded/k3s/flannel.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cbr0", 3 | "cniVersion": "0.3.1", 4 | "plugins": [ 5 | { 6 | "type": "flannel", 7 | "delegate": { 8 | "hairpinMode": true, 9 | "forceAddress": true, 10 | "isDefaultGateway": true 11 | } 12 | }, 13 | { 14 | "type": "portmap", 15 | "capabilities": { 16 | "portMappings": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /embedded/network/networks.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT! 2 | # This file would be replaced by Colima on startup. 3 | 4 | networks: 5 | user-v2: 6 | mode: user-v2 7 | gateway: 192.168.5.2 8 | netmask: 255.255.255.0 9 | -------------------------------------------------------------------------------- /embedded/network/sudo.txt: -------------------------------------------------------------------------------- 1 | # starting vmnet daemon 2 | %staff ALL=(root:wheel) NOPASSWD:NOSETENV: /opt/colima/bin/socket_vmnet --vmnet-mode shared --socket-group staff --vmnet-gateway 192.168.106.1 --vmnet-dhcp-end 192.168.106.254 * 3 | # terminating vmnet daemon 4 | %staff ALL=(root:wheel) NOPASSWD:NOSETENV: /usr/bin/pkill -F /opt/colima/run/*.pid 5 | # validating vmnet daemon 6 | %staff ALL=(root:wheel) NOPASSWD:NOSETENV: /usr/bin/pkill -0 -F /opt/colima/run/*.pid 7 | -------------------------------------------------------------------------------- /embedded/network/vmnet_arm64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abiosoft/colima/e674521ec4b5a6c1b06c078a5d8b5229e65eec70/embedded/network/vmnet_arm64.tar.gz -------------------------------------------------------------------------------- /embedded/network/vmnet_x86_64.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abiosoft/colima/e674521ec4b5a6c1b06c078a5d8b5229e65eec70/embedded/network/vmnet_x86_64.tar.gz -------------------------------------------------------------------------------- /environment/container.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | ) 8 | 9 | // IsNoneRuntime returns if runtime is none. 10 | func IsNoneRuntime(runtime string) bool { return runtime == "none" } 11 | 12 | // Container is container environment. 13 | type Container interface { 14 | // Name is the name of the container runtime. e.g. docker, containerd 15 | Name() string 16 | // Provision provisions/installs the container runtime. 17 | // Should be idempotent. 18 | Provision(ctx context.Context) error 19 | // Start starts the container runtime. 20 | Start(ctx context.Context) error 21 | // Stop stops the container runtime. 22 | Stop(ctx context.Context) error 23 | // Teardown tears down/uninstall the container runtime. 24 | Teardown(ctx context.Context) error 25 | // Update the container runtime. 26 | Update(ctx context.Context) (bool, error) 27 | // Version returns the container runtime version. 28 | Version(ctx context.Context) string 29 | // Running returns if the container runtime is currently running. 30 | Running(ctx context.Context) bool 31 | 32 | Dependencies 33 | } 34 | 35 | // NewContainer creates a new container environment. 36 | func NewContainer(runtime string, host HostActions, guest GuestActions) (Container, error) { 37 | if _, ok := containerRuntimes[runtime]; !ok { 38 | return nil, fmt.Errorf("unsupported container runtime '%s'", runtime) 39 | } 40 | 41 | return containerRuntimes[runtime].Func(host, guest), nil 42 | } 43 | 44 | // NewContainerFunc is implemented by container runtime implementations to create a new instance. 45 | type NewContainerFunc func(host HostActions, guest GuestActions) Container 46 | 47 | var containerRuntimes = map[string]containerRuntimeFunc{} 48 | 49 | type containerRuntimeFunc struct { 50 | Func NewContainerFunc 51 | Hidden bool 52 | } 53 | 54 | // RegisterContainer registers a new container runtime. 55 | // If hidden is true, the container is not displayed as an available runtime. 56 | func RegisterContainer(name string, f NewContainerFunc, hidden bool) { 57 | if _, ok := containerRuntimes[name]; ok { 58 | log.Fatalf("container runtime '%s' already registered", name) 59 | } 60 | containerRuntimes[name] = containerRuntimeFunc{Func: f, Hidden: hidden} 61 | } 62 | 63 | // ContainerRuntimes return the names of available container runtimes. 64 | func ContainerRuntimes() (names []string) { 65 | for name, cont := range containerRuntimes { 66 | if cont.Hidden { 67 | continue 68 | } 69 | names = append(names, name) 70 | } 71 | return 72 | } 73 | -------------------------------------------------------------------------------- /environment/container/containerd/buildkitd.toml: -------------------------------------------------------------------------------- 1 | [worker.oci] 2 | enabled = false 3 | 4 | [worker.containerd] 5 | enabled = true -------------------------------------------------------------------------------- /environment/container/containerd/containerd.go: -------------------------------------------------------------------------------- 1 | package containerd 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "fmt" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/abiosoft/colima/cli" 11 | "github.com/abiosoft/colima/config" 12 | "github.com/abiosoft/colima/environment" 13 | ) 14 | 15 | // Name is container runtime name 16 | const Name = "containerd" 17 | 18 | var configDir = func() string { return config.CurrentProfile().ConfigDir() } 19 | 20 | // HostSocketFile returns the path to the containerd socket on host. 21 | func HostSocketFile() string { return filepath.Join(configDir(), "containerd.sock") } 22 | 23 | // This is written with assumption that Lima is the VM, 24 | // which provides nerdctl/containerd support out of the box. 25 | // There may be need to make this flexible for non-Lima VMs. 26 | 27 | //go:embed buildkitd.toml 28 | var buildKitConf []byte 29 | 30 | const buildKitConfFile = "/etc/buildkit/buildkitd.toml" 31 | 32 | func newRuntime(host environment.HostActions, guest environment.GuestActions) environment.Container { 33 | return &containerdRuntime{ 34 | host: host, 35 | guest: guest, 36 | CommandChain: cli.New(Name), 37 | } 38 | } 39 | 40 | func init() { 41 | environment.RegisterContainer(Name, newRuntime, false) 42 | } 43 | 44 | var _ environment.Container = (*containerdRuntime)(nil) 45 | 46 | type containerdRuntime struct { 47 | host environment.HostActions 48 | guest environment.GuestActions 49 | cli.CommandChain 50 | } 51 | 52 | func (c containerdRuntime) Name() string { 53 | return Name 54 | } 55 | 56 | func (c containerdRuntime) Provision(context.Context) error { 57 | return c.guest.Write(buildKitConfFile, buildKitConf) 58 | } 59 | 60 | func (c containerdRuntime) Start(ctx context.Context) error { 61 | a := c.Init(ctx) 62 | 63 | a.Add(func() error { 64 | return c.guest.Run("sudo", "service", "containerd", "restart") 65 | }) 66 | 67 | // service startup takes few seconds, retry at most 10 times before giving up. 68 | a.Retry("", time.Second*5, 10, func(int) error { 69 | return c.guest.RunQuiet("sudo", "nerdctl", "info") 70 | }) 71 | 72 | a.Add(func() error { 73 | return c.guest.Run("sudo", "service", "buildkit", "start") 74 | }) 75 | 76 | return a.Exec() 77 | } 78 | 79 | func (c containerdRuntime) Running(ctx context.Context) bool { 80 | return c.guest.RunQuiet("service", "containerd", "status") == nil 81 | } 82 | 83 | func (c containerdRuntime) Stop(ctx context.Context) error { 84 | a := c.Init(ctx) 85 | a.Add(func() error { 86 | return c.guest.Run("sudo", "service", "containerd", "stop") 87 | }) 88 | return a.Exec() 89 | } 90 | 91 | func (c containerdRuntime) Teardown(context.Context) error { 92 | // teardown not needed, will be part of VM teardown 93 | return nil 94 | } 95 | 96 | func (c containerdRuntime) Dependencies() []string { 97 | // no dependencies 98 | return nil 99 | } 100 | 101 | func (c containerdRuntime) Version(ctx context.Context) string { 102 | version, _ := c.guest.RunOutput("sudo", "nerdctl", "version", "--format", `client: {{.Client.Version}}{{printf "\n"}}server: {{(index .Server.Components 0).Version}}`) 103 | return version 104 | } 105 | 106 | func (c *containerdRuntime) Update(ctx context.Context) (bool, error) { 107 | return false, fmt.Errorf("update not supported for the %s runtime", Name) 108 | } 109 | -------------------------------------------------------------------------------- /environment/container/docker/context.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/abiosoft/colima/config" 7 | ) 8 | 9 | var configDir = func() string { return config.CurrentProfile().ConfigDir() } 10 | 11 | // HostSocketFile returns the path to the docker socket on host. 12 | func HostSocketFile() string { return filepath.Join(configDir(), "docker.sock") } 13 | func LegacyDefaultHostSocketFile() string { 14 | return filepath.Join(filepath.Dir(configDir()), "docker.sock") 15 | } 16 | 17 | func (d dockerRuntime) contextCreated() bool { 18 | return d.host.RunQuiet("docker", "context", "inspect", config.CurrentProfile().ID) == nil 19 | } 20 | 21 | func (d dockerRuntime) setupContext() error { 22 | if d.contextCreated() { 23 | return nil 24 | } 25 | 26 | profile := config.CurrentProfile() 27 | 28 | return d.host.Run("docker", "context", "create", profile.ID, 29 | "--description", profile.DisplayName, 30 | "--docker", "host=unix://"+HostSocketFile(), 31 | ) 32 | } 33 | 34 | func (d dockerRuntime) useContext() error { 35 | return d.host.Run("docker", "context", "use", config.CurrentProfile().ID) 36 | } 37 | 38 | func (d dockerRuntime) teardownContext() error { 39 | if !d.contextCreated() { 40 | return nil 41 | } 42 | 43 | return d.host.Run("docker", "context", "rm", "--force", config.CurrentProfile().ID) 44 | } 45 | -------------------------------------------------------------------------------- /environment/container/docker/daemon.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "strings" 8 | ) 9 | 10 | const daemonFile = "/etc/docker/daemon.json" 11 | const hostGatewayIPKey = "host-gateway-ip" 12 | 13 | func getHostGatewayIp(d dockerRuntime, conf map[string]any) (string, error) { 14 | // get host-gateway ip from the guest 15 | ip, err := d.guest.RunOutput("sh", "-c", "grep 'host.lima.internal' /etc/hosts | awk -F' ' '{print $1}'") 16 | if err != nil { 17 | return "", fmt.Errorf("error retrieving host gateway IP address: %w", err) 18 | } 19 | // if set by the user, use the user specified value 20 | if _, ok := conf[hostGatewayIPKey]; ok { 21 | if gip, ok := conf[hostGatewayIPKey].(string); ok { 22 | ip = gip 23 | } 24 | } 25 | if net.ParseIP(ip) == nil { 26 | return "", fmt.Errorf("invalid host gateway IP address: '%s'", ip) 27 | } 28 | 29 | return ip, nil 30 | } 31 | 32 | func (d dockerRuntime) createDaemonFile(conf map[string]any, env map[string]string) error { 33 | if conf == nil { 34 | conf = map[string]any{} 35 | } 36 | 37 | // enable buildkit (if not set by user) 38 | if _, ok := conf["features"]; !ok { 39 | conf["features"] = map[string]any{"buildkit": true} 40 | } 41 | 42 | // enable cgroupfs for k3s (if not set by user) 43 | if _, ok := conf["exec-opts"]; !ok { 44 | conf["exec-opts"] = []string{"native.cgroupdriver=cgroupfs"} 45 | } else if opts, ok := conf["exec-opts"].([]string); ok { 46 | conf["exec-opts"] = append(opts, "native.cgroupdriver=cgroupfs") 47 | } 48 | // remove host-gateway-ip if set by the user 49 | // to avoid clash with systemd configuration 50 | delete(conf, hostGatewayIPKey) 51 | 52 | // add proxy vars if set 53 | // according to https://docs.docker.com/config/daemon/systemd/#httphttps-proxy 54 | if vars := d.proxyEnvVars(env); !vars.empty() { 55 | proxyConf := map[string]any{} 56 | hostGatewayIP, err := getHostGatewayIp(d, conf) 57 | if err != nil { 58 | return err 59 | } 60 | if vars.http != "" { 61 | proxyConf["http-proxy"] = strings.ReplaceAll(vars.http, "127.0.0.1", hostGatewayIP) 62 | } 63 | if vars.https != "" { 64 | proxyConf["https-proxy"] = strings.ReplaceAll(vars.https, "127.0.0.1", hostGatewayIP) 65 | } 66 | if vars.no != "" { 67 | proxyConf["no-proxy"] = strings.ReplaceAll(vars.no, "127.0.0.1", hostGatewayIP) 68 | } 69 | conf["proxies"] = proxyConf 70 | } 71 | 72 | b, err := json.MarshalIndent(conf, "", " ") 73 | if err != nil { 74 | return fmt.Errorf("error marshaling daemon.json: %w", err) 75 | } 76 | return d.guest.Write(daemonFile, b) 77 | } 78 | 79 | func (d dockerRuntime) addHostGateway(conf map[string]any) error { 80 | // get host-gateway ip from the guest 81 | ip, err := getHostGatewayIp(d, conf) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | // set host-gateway ip as systemd service file 87 | content := fmt.Sprintf(systemdUnitFileContent, ip) 88 | if err := d.guest.Write(systemdUnitFilename, []byte(content)); err != nil { 89 | return fmt.Errorf("error creating systemd unit file: %w", err) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func (d dockerRuntime) reloadAndRestartSystemdService() error { 96 | if err := d.guest.Run("sudo", "systemctl", "daemon-reload"); err != nil { 97 | return fmt.Errorf("error reloading systemd daemon: %w", err) 98 | } 99 | if err := d.guest.Run("sudo", "systemctl", "restart", "docker"); err != nil { 100 | return fmt.Errorf("error restarting docker: %w", err) 101 | } 102 | return nil 103 | } 104 | 105 | const systemdUnitFilename = "/etc/systemd/system/docker.service.d/docker.conf" 106 | const systemdUnitFileContent string = ` 107 | [Service] 108 | LimitNOFILE=infinity 109 | ExecStart= 110 | ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --host-gateway-ip=%s 111 | ` 112 | -------------------------------------------------------------------------------- /environment/container/docker/docker.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/abiosoft/colima/cli" 8 | "github.com/abiosoft/colima/config" 9 | "github.com/abiosoft/colima/environment" 10 | "github.com/abiosoft/colima/util/debutil" 11 | ) 12 | 13 | // Name is container runtime name. 14 | const Name = "docker" 15 | 16 | var _ environment.Container = (*dockerRuntime)(nil) 17 | 18 | func init() { 19 | environment.RegisterContainer(Name, newRuntime, false) 20 | } 21 | 22 | type dockerRuntime struct { 23 | host environment.HostActions 24 | guest environment.GuestActions 25 | cli.CommandChain 26 | } 27 | 28 | // newRuntime creates a new docker runtime. 29 | func newRuntime(host environment.HostActions, guest environment.GuestActions) environment.Container { 30 | return &dockerRuntime{ 31 | host: host, 32 | guest: guest, 33 | CommandChain: cli.New(Name), 34 | } 35 | } 36 | 37 | func (d dockerRuntime) Name() string { 38 | return Name 39 | } 40 | 41 | func (d dockerRuntime) Provision(ctx context.Context) error { 42 | a := d.Init(ctx) 43 | log := d.Logger(ctx) 44 | 45 | conf, _ := ctx.Value(config.CtxKey()).(config.Config) 46 | 47 | // daemon.json 48 | a.Add(func() error { 49 | // these are not fatal errors 50 | if err := d.createDaemonFile(conf.Docker, conf.Env); err != nil { 51 | log.Warnln(err) 52 | } 53 | if err := d.addHostGateway(conf.Docker); err != nil { 54 | log.Warnln(err) 55 | } 56 | if err := d.reloadAndRestartSystemdService(); err != nil { 57 | log.Warnln(err) 58 | } 59 | return nil 60 | }) 61 | 62 | // docker context 63 | a.Add(d.setupContext) 64 | if conf.AutoActivate() { 65 | a.Add(d.useContext) 66 | } 67 | 68 | return a.Exec() 69 | } 70 | 71 | func (d dockerRuntime) Start(ctx context.Context) error { 72 | a := d.Init(ctx) 73 | 74 | // TODO: interval is high due to 0.6.3->0.6.4 docker-ce package transition 75 | // to ensure startup is successful 76 | a.Retry("", time.Second, 120, func(int) error { 77 | return d.guest.RunQuiet("sudo", "service", "docker", "start") 78 | }) 79 | 80 | // service startup takes few seconds, retry for a minute before giving up. 81 | a.Retry("", time.Second, 60, func(int) error { 82 | return d.guest.RunQuiet("sudo", "docker", "info") 83 | }) 84 | 85 | // ensure docker is accessible without root 86 | // otherwise, restart to ensure user is added to docker group 87 | a.Add(func() error { 88 | if err := d.guest.RunQuiet("docker", "info"); err == nil { 89 | return nil 90 | } 91 | ctx := context.WithValue(ctx, cli.CtxKeyQuiet, true) 92 | return d.guest.Restart(ctx) 93 | }) 94 | 95 | return a.Exec() 96 | } 97 | 98 | func (d dockerRuntime) Running(ctx context.Context) bool { 99 | return d.guest.RunQuiet("service", "docker", "status") == nil 100 | } 101 | 102 | func (d dockerRuntime) Stop(ctx context.Context) error { 103 | a := d.Init(ctx) 104 | 105 | a.Add(func() error { 106 | if !d.Running(ctx) { 107 | return nil 108 | } 109 | return d.guest.Run("sudo", "service", "docker", "stop") 110 | }) 111 | 112 | // clear docker context settings 113 | // since the container runtime can be changed on startup, 114 | // it is better to not leave unnecessary traces behind 115 | a.Add(d.teardownContext) 116 | 117 | return a.Exec() 118 | } 119 | 120 | func (d dockerRuntime) Teardown(ctx context.Context) error { 121 | a := d.Init(ctx) 122 | 123 | // clear docker context settings 124 | a.Add(d.teardownContext) 125 | 126 | return a.Exec() 127 | } 128 | 129 | func (d dockerRuntime) Dependencies() []string { 130 | return []string{"docker"} 131 | } 132 | 133 | func (d dockerRuntime) Version(ctx context.Context) string { 134 | version, _ := d.host.RunOutput("docker", "--context", config.CurrentProfile().ID, "version", "--format", `client: v{{.Client.Version}}{{printf "\n"}}server: v{{.Server.Version}}`) 135 | return version 136 | } 137 | 138 | func (d *dockerRuntime) Update(ctx context.Context) (bool, error) { 139 | packages := []string{ 140 | "docker-ce", 141 | "docker-ce-cli", 142 | "containerd.io", 143 | } 144 | 145 | return debutil.UpdateRuntime(ctx, d.guest, d, packages...) 146 | } 147 | -------------------------------------------------------------------------------- /environment/container/docker/proxy.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | type proxyVars struct { 9 | http string 10 | https string 11 | no string 12 | } 13 | 14 | func (p proxyVars) empty() bool { 15 | return p.http == "" && p.https == "" 16 | } 17 | 18 | type proxyVarKey string 19 | 20 | var ( 21 | httpProxy proxyVarKey = "http_proxy" 22 | httpsProxy proxyVarKey = "https_proxy" 23 | noProxy proxyVarKey = "no_proxy" 24 | ) 25 | 26 | // keys return both the lower case and upper case env var keys. 27 | // e.g. http_proxy and HTTP_PROXY 28 | func (p proxyVarKey) Keys() []string { 29 | return []string{string(p), strings.ToUpper(string(p))} 30 | } 31 | 32 | func (d dockerRuntime) proxyEnvVars(env map[string]string) proxyVars { 33 | getVal := func(key proxyVarKey) string { 34 | for _, k := range key.Keys() { 35 | // config 36 | if val, ok := env[k]; ok { 37 | return val 38 | } 39 | // os 40 | if val := os.Getenv(k); val != "" { 41 | return val 42 | } 43 | } 44 | return "" 45 | } 46 | 47 | return proxyVars{ 48 | http: getVal(httpProxy), 49 | https: getVal(httpsProxy), 50 | no: getVal(noProxy), 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /environment/container/incus/config.yaml: -------------------------------------------------------------------------------- 1 | networks: 2 | - config: 3 | ipv4.address: 192.100.0.1/24 4 | ipv4.nat: "true" 5 | ipv6.address: auto 6 | description: "" 7 | name: {{.Interface}} 8 | type: "" 9 | project: default 10 | storage_pools: 11 | - config: 12 | size: {{.Disk}}GiB 13 | description: "" 14 | name: default 15 | driver: zfs 16 | profiles: 17 | - config: {} 18 | description: "" 19 | devices: 20 | eth0: 21 | name: eth0 22 | network: {{.Interface}} 23 | type: nic 24 | root: 25 | path: / 26 | pool: default 27 | type: disk 28 | name: default 29 | projects: [] 30 | cluster: null 31 | -------------------------------------------------------------------------------- /environment/container/kubernetes/cni.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/abiosoft/colima/cli" 9 | "github.com/abiosoft/colima/embedded" 10 | "github.com/abiosoft/colima/environment" 11 | ) 12 | 13 | func installCniConfig(guest environment.GuestActions, a *cli.ActiveCommandChain) { 14 | // fix cni config 15 | a.Add(func() error { 16 | flannelFile := "/etc/cni/net.d/10-flannel.conflist" 17 | cniConfDir := filepath.Dir(flannelFile) 18 | if err := guest.Run("sudo", "mkdir", "-p", cniConfDir); err != nil { 19 | return fmt.Errorf("error creating cni config dir: %w", err) 20 | } 21 | 22 | flannel, err := embedded.Read("k3s/flannel.json") 23 | if err != nil { 24 | return fmt.Errorf("error reading embedded flannel config: %w", err) 25 | } 26 | return guest.Write(flannelFile, flannel) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /environment/container/kubernetes/kubeconfig.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | 10 | "github.com/abiosoft/colima/cli" 11 | "github.com/abiosoft/colima/config" 12 | "github.com/abiosoft/colima/environment/vm/lima/limautil" 13 | ) 14 | 15 | const masterAddressKey = "master_address" 16 | 17 | func (c kubernetesRuntime) provisionKubeconfig(ctx context.Context) error { 18 | ip := limautil.IPAddress(config.CurrentProfile().ID) 19 | if ip == c.guest.Get(masterAddressKey) { 20 | return nil 21 | } 22 | 23 | log := c.Logger(ctx) 24 | a := c.Init(ctx) 25 | 26 | a.Stage("updating config") 27 | 28 | // remove existing configs (if any) 29 | // this is safe as the profile name is unique to colima 30 | c.unsetKubeconfig(a) 31 | 32 | // ensure host kube directory exists 33 | hostHome := c.host.Env("HOME") 34 | if hostHome == "" { 35 | return fmt.Errorf("error retrieving home directory on host") 36 | } 37 | 38 | profile := config.CurrentProfile().ID 39 | hostKubeDir := filepath.Join(hostHome, ".kube") 40 | a.Add(func() error { 41 | return c.host.Run("mkdir", "-p", filepath.Join(hostKubeDir, "."+profile)) 42 | }) 43 | 44 | kubeconfFile := filepath.Join(hostKubeDir, "config") 45 | tmpkubeconfFile := filepath.Join(hostKubeDir, "."+profile, "colima-temp") 46 | 47 | // manipulate in VM and save to host 48 | a.Add(func() error { 49 | kubeconfig, err := c.guest.Read("/etc/rancher/k3s/k3s.yaml") 50 | if err != nil { 51 | return fmt.Errorf("error fetching kubeconfig on guest: %w", err) 52 | } 53 | // replace name 54 | kubeconfig = strings.ReplaceAll(kubeconfig, ": default", ": "+profile) 55 | 56 | // replace IP 57 | if ip != "" && ip != "127.0.0.1" { 58 | kubeconfig = strings.ReplaceAll(kubeconfig, "https://127.0.0.1:", "https://"+ip+":") 59 | } 60 | 61 | // save on the host 62 | return c.host.Write(tmpkubeconfFile, []byte(kubeconfig)) 63 | }) 64 | 65 | // merge on host 66 | a.Add(func() (err error) { 67 | // prepare new host with right env var. 68 | envVar := fmt.Sprintf("KUBECONFIG=%s:%s", kubeconfFile, tmpkubeconfFile) 69 | host := c.host.WithEnv(envVar) 70 | 71 | // get merged config 72 | kubeconfig, err := host.RunOutput("kubectl", "config", "view", "--raw") 73 | if err != nil { 74 | return err 75 | } 76 | 77 | // save 78 | return host.Write(tmpkubeconfFile, []byte(kubeconfig)) 79 | }) 80 | 81 | // backup current settings and save new config 82 | a.Add(func() error { 83 | // backup existing file if exists 84 | if stat, err := c.host.Stat(kubeconfFile); err == nil && !stat.IsDir() { 85 | backup := filepath.Join(filepath.Dir(tmpkubeconfFile), fmt.Sprintf("config-bak-%d", time.Now().Unix())) 86 | if err := c.host.Run("cp", kubeconfFile, backup); err != nil { 87 | return fmt.Errorf("error backing up kubeconfig: %w", err) 88 | } 89 | } 90 | // save new config 91 | if err := c.host.Run("cp", tmpkubeconfFile, kubeconfFile); err != nil { 92 | return fmt.Errorf("error updating kubeconfig: %w", err) 93 | } 94 | 95 | return nil 96 | }) 97 | 98 | // set new context 99 | conf, _ := ctx.Value(config.CtxKey()).(config.Config) 100 | if conf.AutoActivate() { 101 | a.Add(func() error { 102 | out, err := c.host.RunOutput("kubectl", "config", "use-context", profile) 103 | if err != nil { 104 | return err 105 | } 106 | log.Println(out) 107 | return nil 108 | }) 109 | } 110 | 111 | // save settings 112 | a.Add(func() error { 113 | return c.guest.Set(masterAddressKey, ip) 114 | }) 115 | 116 | return a.Exec() 117 | } 118 | 119 | func (c kubernetesRuntime) unsetKubeconfig(a *cli.ActiveCommandChain) { 120 | profile := config.CurrentProfile().ID 121 | a.Add(func() error { 122 | return c.host.Run("kubectl", "config", "unset", "users."+profile) 123 | }) 124 | a.Add(func() error { 125 | return c.host.Run("kubectl", "config", "unset", "contexts."+profile) 126 | }) 127 | a.Add(func() error { 128 | return c.host.Run("kubectl", "config", "unset", "clusters."+profile) 129 | }) 130 | // kubectl config unset current-context 131 | a.Add(func() error { 132 | if c, _ := c.host.RunOutput("kubectl", "config", "current-context"); c != config.CurrentProfile().ID { 133 | return nil 134 | } 135 | return c.host.Run("kubectl", "config", "unset", "current-context") 136 | }) 137 | } 138 | 139 | func (c kubernetesRuntime) teardownKubeconfig(a *cli.ActiveCommandChain) { 140 | a.Stage("reverting config") 141 | c.unsetKubeconfig(a) 142 | a.Add(func() error { 143 | return c.guest.Set(masterAddressKey, "") 144 | }) 145 | } 146 | -------------------------------------------------------------------------------- /environment/environment.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | 8 | "github.com/abiosoft/colima/config" 9 | ) 10 | 11 | type runActions interface { 12 | // Run runs command 13 | Run(args ...string) error 14 | // RunQuiet runs command whilst suppressing the output. 15 | // Useful for commands that only the exit code matters. 16 | RunQuiet(args ...string) error 17 | // RunOutput runs command and returns its output. 18 | RunOutput(args ...string) (string, error) 19 | // RunInteractive runs command interactively. 20 | RunInteractive(args ...string) error 21 | // RunWith runs with stdin and stdout. 22 | RunWith(stdin io.Reader, stdout io.Writer, args ...string) error 23 | } 24 | 25 | type fileActions interface { 26 | Read(fileName string) (string, error) 27 | Write(fileName string, body []byte) error 28 | Stat(fileName string) (os.FileInfo, error) 29 | } 30 | 31 | // HostActions are actions performed on the host. 32 | type HostActions interface { 33 | runActions 34 | fileActions 35 | // WithEnv creates a new instance based on the current instance 36 | // with the specified environment variables. 37 | WithEnv(env ...string) HostActions 38 | // WithDir creates a new instance based on the current instance 39 | // with the working directory set to dir. 40 | WithDir(dir string) HostActions 41 | // Env retrieves environment variable on the host. 42 | Env(string) string 43 | } 44 | 45 | // GuestActions are actions performed on the guest i.e. VM. 46 | type GuestActions interface { 47 | runActions 48 | fileActions 49 | // Start starts up the VM 50 | Start(ctx context.Context, conf config.Config) error 51 | // Stop shuts down the VM 52 | Stop(ctx context.Context, force bool) error 53 | // Restart restarts the VM 54 | Restart(ctx context.Context) error 55 | // SSH performs an ssh connection to the VM 56 | SSH(workingDir string, args ...string) error 57 | // Created returns if the VM has been previously created. 58 | Created() bool 59 | // Running returns if the VM is currently running. 60 | Running(ctx context.Context) bool 61 | // Env retrieves environment variable in the VM. 62 | Env(string) (string, error) 63 | // Get retrieves a configuration in the VM. 64 | Get(key string) string 65 | // Set sets configuration in the VM. 66 | Set(key, value string) error 67 | // User returns the username of the user in the VM. 68 | User() (string, error) 69 | // Arch returns the architecture of the VM. 70 | Arch() Arch 71 | } 72 | 73 | // Dependencies are dependencies that must exist on the host. 74 | type Dependencies interface { 75 | // Dependencies are dependencies that must exist on the host. 76 | // TODO this may need to accommodate non-brew installable dependencies 77 | Dependencies() []string 78 | } 79 | -------------------------------------------------------------------------------- /environment/host.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | // Host is the host environment. 4 | type Host interface { 5 | HostActions 6 | } 7 | -------------------------------------------------------------------------------- /environment/host/host.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/abiosoft/colima/util/terminal" 14 | 15 | "github.com/abiosoft/colima/cli" 16 | "github.com/abiosoft/colima/environment" 17 | ) 18 | 19 | // New creates a new host environment. 20 | func New() environment.Host { 21 | return &hostEnv{} 22 | } 23 | 24 | var _ environment.Host = (*hostEnv)(nil) 25 | 26 | type hostEnv struct { 27 | env []string 28 | dir string // working directory 29 | } 30 | 31 | func (h hostEnv) clone() hostEnv { 32 | var newHost hostEnv 33 | newHost.env = append(newHost.env, h.env...) 34 | newHost.dir = h.dir 35 | return newHost 36 | } 37 | 38 | func (h hostEnv) WithEnv(env ...string) environment.HostActions { 39 | newHost := h.clone() 40 | // append new env vars 41 | newHost.env = append(newHost.env, env...) 42 | return newHost 43 | } 44 | 45 | func (h hostEnv) WithDir(dir string) environment.HostActions { 46 | newHost := h.clone() 47 | newHost.dir = dir 48 | return newHost 49 | } 50 | 51 | func (h hostEnv) Run(args ...string) error { 52 | if len(args) == 0 { 53 | return errors.New("args not specified") 54 | } 55 | cmd := cli.Command(args[0], args[1:]...) 56 | cmd.Env = append(os.Environ(), h.env...) 57 | if h.dir != "" { 58 | cmd.Dir = h.dir 59 | } 60 | 61 | lineHeight := 6 62 | if cli.Settings.Verbose { 63 | lineHeight = -1 // disable scrolling 64 | } 65 | 66 | out := terminal.NewVerboseWriter(lineHeight) 67 | cmd.Stdout = out 68 | cmd.Stderr = out 69 | 70 | err := cmd.Run() 71 | if err == nil { 72 | return out.Close() 73 | } 74 | return err 75 | } 76 | 77 | func (h hostEnv) RunQuiet(args ...string) error { 78 | if len(args) == 0 { 79 | return errors.New("args not specified") 80 | } 81 | cmd := cli.Command(args[0], args[1:]...) 82 | cmd.Env = append(os.Environ(), h.env...) 83 | if h.dir != "" { 84 | cmd.Dir = h.dir 85 | } 86 | 87 | var errBuf bytes.Buffer 88 | cmd.Stdout = nil 89 | cmd.Stderr = &errBuf 90 | 91 | err := cmd.Run() 92 | if err != nil { 93 | return errCmd(cmd.Args, errBuf, err) 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (h hostEnv) RunOutput(args ...string) (string, error) { 100 | if len(args) == 0 { 101 | return "", errors.New("args not specified") 102 | } 103 | 104 | cmd := cli.Command(args[0], args[1:]...) 105 | cmd.Env = append(os.Environ(), h.env...) 106 | if h.dir != "" { 107 | cmd.Dir = h.dir 108 | } 109 | 110 | var buf, errBuf bytes.Buffer 111 | cmd.Stdout = &buf 112 | cmd.Stderr = &errBuf 113 | 114 | err := cmd.Run() 115 | if err != nil { 116 | return "", errCmd(cmd.Args, errBuf, err) 117 | } 118 | 119 | return strings.TrimSpace(buf.String()), nil 120 | } 121 | 122 | func errCmd(args []string, stderr bytes.Buffer, err error) error { 123 | // this is going to be part of a log output, 124 | // reading the first line of the error should suffice 125 | output, _ := stderr.ReadString('\n') 126 | if len(output) > 0 { 127 | output = output[:len(output)-1] 128 | } 129 | return fmt.Errorf("error running %v, output: %s, err: %s", args, strconv.Quote(output), strconv.Quote(err.Error())) 130 | } 131 | 132 | func (h hostEnv) RunInteractive(args ...string) error { 133 | if len(args) == 0 { 134 | return errors.New("args not specified") 135 | } 136 | cmd := cli.CommandInteractive(args[0], args[1:]...) 137 | cmd.Env = append(os.Environ(), h.env...) 138 | if h.dir != "" { 139 | cmd.Dir = h.dir 140 | } 141 | return cmd.Run() 142 | } 143 | 144 | func (h hostEnv) RunWith(stdin io.Reader, stdout io.Writer, args ...string) error { 145 | if len(args) == 0 { 146 | return errors.New("args not specified") 147 | } 148 | cmd := cli.CommandInteractive(args[0], args[1:]...) 149 | cmd.Env = append(os.Environ(), h.env...) 150 | if h.dir != "" { 151 | cmd.Dir = h.dir 152 | } 153 | 154 | cmd.Stdin = stdin 155 | cmd.Stdout = stdout 156 | 157 | var buf bytes.Buffer 158 | cmd.Stderr = &buf 159 | 160 | if err := cmd.Run(); err != nil { 161 | return errCmd(cmd.Args, buf, err) 162 | } 163 | 164 | return nil 165 | } 166 | 167 | func (h hostEnv) Env(s string) string { 168 | return os.Getenv(s) 169 | } 170 | 171 | func (h hostEnv) Read(fileName string) (string, error) { 172 | b, err := os.ReadFile(fileName) 173 | return string(b), err 174 | } 175 | 176 | func (h hostEnv) Write(fileName string, body []byte) error { 177 | return os.WriteFile(fileName, body, 0644) 178 | } 179 | 180 | func (h hostEnv) Stat(fileName string) (os.FileInfo, error) { 181 | return os.Stat(fileName) 182 | } 183 | 184 | // IsInstalled checks if dependencies are installed. 185 | func IsInstalled(dependencies environment.Dependencies) error { 186 | var missing []string 187 | check := func(p string) error { 188 | _, err := exec.LookPath(p) 189 | return err 190 | } 191 | for _, p := range dependencies.Dependencies() { 192 | if check(p) != nil { 193 | missing = append(missing, p) 194 | } 195 | } 196 | 197 | if len(missing) > 0 { 198 | return fmt.Errorf("%s not found, run 'brew install %s' to install", strings.Join(missing, ", "), strings.Join(missing, " ")) 199 | } 200 | 201 | return nil 202 | } 203 | -------------------------------------------------------------------------------- /environment/vm.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | 7 | "github.com/abiosoft/colima/util" 8 | ) 9 | 10 | // VM is virtual machine. 11 | type VM interface { 12 | GuestActions 13 | Dependencies 14 | Host() HostActions 15 | Teardown(ctx context.Context) error 16 | } 17 | 18 | // VM configurations 19 | const ( 20 | // ContainerRuntimeKey is the settings key for container runtime. 21 | ContainerRuntimeKey = "runtime" 22 | ) 23 | 24 | // Arch is the CPU architecture of the VM. 25 | type Arch string 26 | 27 | const ( 28 | X8664 Arch = "x86_64" 29 | AARCH64 Arch = "aarch64" 30 | ) 31 | 32 | // HostArch returns the host CPU architecture. 33 | func HostArch() Arch { 34 | return Arch(runtime.GOARCH).Value() 35 | } 36 | 37 | // GoArch returns the GOARCH equivalent value for the architecture. 38 | func (a Arch) GoArch() string { 39 | switch a { 40 | case X8664: 41 | return "amd64" 42 | case AARCH64: 43 | return "arm64" 44 | } 45 | 46 | return runtime.GOARCH 47 | } 48 | 49 | // Value converts the underlying architecture alias value to one of X8664 or AARCH64. 50 | func (a Arch) Value() Arch { 51 | switch a { 52 | case X8664, AARCH64: 53 | return a 54 | // accept amd, amd64, x86, x64, arm, arm64 and m1 values 55 | case "amd", "amd64", "x86", "x64": 56 | return X8664 57 | case "arm", "arm64", "m1": 58 | return AARCH64 59 | } 60 | 61 | return Arch(runtime.GOARCH).Value() 62 | } 63 | 64 | // DefaultVMType returns the default virtual machine type based on the operation 65 | // system and availability of Qemu. 66 | func DefaultVMType() string { 67 | if util.MacOS13OrNewer() { 68 | return "vz" 69 | } 70 | 71 | return "qemu" 72 | } 73 | -------------------------------------------------------------------------------- /environment/vm/lima/certs.go: -------------------------------------------------------------------------------- 1 | package lima 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/abiosoft/colima/config" 9 | "github.com/abiosoft/colima/util" 10 | ) 11 | 12 | func (l limaVM) copyCerts() error { 13 | log := l.Logger(context.Background()) 14 | err := func() error { 15 | dockerCertsDirHost := filepath.Join(util.HomeDir(), ".docker", "certs.d") 16 | dockerCertsDirsGuest := []string{"/etc/docker/certs.d", "/etc/ssl/certs"} 17 | if _, err := l.host.Stat(dockerCertsDirHost); err != nil { 18 | // no certs found 19 | return nil 20 | } 21 | 22 | // we are utilising the host cache path as it is the only guaranteed mounted path. 23 | 24 | // copy to cache dir 25 | dockerCertsCacheDir := filepath.Join(config.CacheDir(), "docker-certs") 26 | if err := l.host.RunQuiet("rm", "-rf", dockerCertsCacheDir); err != nil { 27 | return err 28 | } 29 | if err := l.host.RunQuiet("mkdir", "-p", dockerCertsCacheDir); err != nil { 30 | return err 31 | } 32 | if err := l.host.RunQuiet("cp", "-R", dockerCertsDirHost+"/.", dockerCertsCacheDir); err != nil { 33 | return err 34 | } 35 | 36 | // copy from cache to vm 37 | for _, dir := range dockerCertsDirsGuest { 38 | // copy from cache to vm 39 | if err := l.RunQuiet("sudo", "mkdir", "-p", dir); err != nil { 40 | return err 41 | } 42 | if err := l.RunQuiet("sudo", "cp", "-R", dockerCertsCacheDir+"/.", dir); err != nil { 43 | return err 44 | } 45 | } 46 | 47 | return nil 48 | }() 49 | 50 | // not a fatal error, a warning suffices. 51 | if err != nil { 52 | log.Warnln(fmt.Errorf("cannot copy registry certs to vm: %w", err)) 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /environment/vm/lima/config.go: -------------------------------------------------------------------------------- 1 | package lima 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "path/filepath" 8 | ) 9 | 10 | const configFile = "/etc/colima/colima.json" 11 | 12 | func (l limaVM) getConf() map[string]string { 13 | log := l.Logger(context.Background()) 14 | 15 | obj := map[string]string{} 16 | b, err := l.Read(configFile) 17 | if err != nil { 18 | log.Trace(fmt.Errorf("error reading config file: %w", err)) 19 | 20 | return obj 21 | } 22 | 23 | // we do not care if it fails 24 | _ = json.Unmarshal([]byte(b), &obj) 25 | 26 | return obj 27 | } 28 | func (l limaVM) Get(key string) string { 29 | if val, ok := l.getConf()[key]; ok { 30 | return val 31 | } 32 | 33 | return "" 34 | } 35 | 36 | func (l limaVM) Set(key, value string) error { 37 | obj := l.getConf() 38 | obj[key] = value 39 | 40 | b, err := json.Marshal(obj) 41 | if err != nil { 42 | return fmt.Errorf("error marshalling settings to json: %w", err) 43 | } 44 | 45 | if err := l.Run("sudo", "mkdir", "-p", filepath.Dir(configFile)); err != nil { 46 | return fmt.Errorf("error saving settings: %w", err) 47 | } 48 | 49 | if err := l.Write(configFile, b); err != nil { 50 | return fmt.Errorf("error saving settings: %w", err) 51 | } 52 | 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /environment/vm/lima/daemon.go: -------------------------------------------------------------------------------- 1 | package lima 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/abiosoft/colima/config" 9 | "github.com/abiosoft/colima/daemon" 10 | "github.com/abiosoft/colima/daemon/process/inotify" 11 | "github.com/abiosoft/colima/daemon/process/vmnet" 12 | "github.com/abiosoft/colima/environment/container/incus" 13 | "github.com/abiosoft/colima/environment/vm/lima/limaconfig" 14 | "github.com/abiosoft/colima/util" 15 | ) 16 | 17 | func (l *limaVM) startDaemon(ctx context.Context, conf config.Config) (context.Context, error) { 18 | // vmnet is used by QEMU and always used by incus (even with VZ) 19 | useVmnet := conf.VMType == limaconfig.QEMU || conf.Runtime == incus.Name 20 | 21 | // network daemon is only needed for vmnet 22 | conf.Network.Address = conf.Network.Address && useVmnet 23 | 24 | // limited to macOS (with vmnet required) 25 | // or with inotify enabled 26 | if !util.MacOS() || (!conf.MountINotify && !conf.Network.Address) { 27 | return ctx, nil 28 | } 29 | 30 | ctxKeyVmnet := daemon.CtxKey(vmnet.Name) 31 | ctxKeyInotify := daemon.CtxKey(inotify.Name) 32 | 33 | // use a nested chain for convenience 34 | a := l.Init(ctx) 35 | log := a.Logger() 36 | 37 | networkInstalledKey := struct{ key string }{key: "network_installed"} 38 | 39 | // add inotify to daemon 40 | if conf.MountINotify { 41 | a.Add(func() error { 42 | ctx = context.WithValue(ctx, ctxKeyInotify, true) 43 | deps, _ := l.daemon.Dependencies(ctx, conf) 44 | if err := deps.Install(l.host); err != nil { 45 | return fmt.Errorf("error setting up inotify dependencies: %w", err) 46 | } 47 | return nil 48 | }) 49 | } 50 | 51 | // add network processes to daemon 52 | if useVmnet { 53 | a.Add(func() error { 54 | if conf.Network.Address { 55 | a.Stage("preparing network") 56 | ctx = context.WithValue(ctx, ctxKeyVmnet, true) 57 | } 58 | deps, root := l.daemon.Dependencies(ctx, conf) 59 | if deps.Installed() { 60 | ctx = context.WithValue(ctx, networkInstalledKey, true) 61 | return nil 62 | } 63 | 64 | // if user interaction is not required (i.e. root), 65 | // no need for another verbose info. 66 | if root { 67 | log.Println("dependencies missing for setting up reachable IP address") 68 | log.Println("sudo password may be required") 69 | } 70 | 71 | // install deps 72 | err := deps.Install(l.host) 73 | if err != nil { 74 | ctx = context.WithValue(ctx, networkInstalledKey, false) 75 | } 76 | return err 77 | }) 78 | } 79 | 80 | // start daemon 81 | a.Add(func() error { 82 | return l.daemon.Start(ctx, conf) 83 | }) 84 | 85 | statusKey := struct{ key string }{key: "daemonStatus"} 86 | // delay to ensure that the processes have started 87 | if conf.Network.Address || conf.MountINotify { 88 | a.Retry("", time.Second*1, 15, func(i int) error { 89 | s, err := l.daemon.Running(ctx, conf) 90 | ctx = context.WithValue(ctx, statusKey, s) 91 | if err != nil { 92 | return err 93 | } 94 | if !s.Running { 95 | return fmt.Errorf("daemon is not running") 96 | } 97 | for _, p := range s.Processes { 98 | if !p.Running { 99 | return p.Error 100 | } 101 | } 102 | return nil 103 | }) 104 | } 105 | 106 | // network failure is not fatal 107 | if err := a.Exec(); err != nil { 108 | if useVmnet { 109 | func() { 110 | installed, _ := ctx.Value(networkInstalledKey).(bool) 111 | if !installed { 112 | log.Warnln(fmt.Errorf("error setting up network dependencies: %w", err)) 113 | return 114 | } 115 | 116 | status, ok := ctx.Value(statusKey).(daemon.Status) 117 | if !ok { 118 | return 119 | } 120 | if !status.Running { 121 | log.Warnln(fmt.Errorf("error starting network: %w", err)) 122 | return 123 | } 124 | 125 | for _, p := range status.Processes { 126 | // TODO: handle inotify separate from network 127 | if p.Name == inotify.Name { 128 | continue 129 | } 130 | if !p.Running { 131 | ctx = context.WithValue(ctx, daemon.CtxKey(p.Name), false) 132 | log.Warnln(fmt.Errorf("error starting %s: %w", p.Name, err)) 133 | } 134 | } 135 | }() 136 | } 137 | } 138 | 139 | // check if inotify is running 140 | if conf.MountINotify { 141 | if inotifyEnabled, _ := ctx.Value(ctxKeyInotify).(bool); !inotifyEnabled { 142 | log.Warnln("error occurred enabling inotify daemon") 143 | } 144 | } 145 | 146 | // preserve vmnet context 147 | if vmnetEnabled, _ := ctx.Value(ctxKeyVmnet).(bool); vmnetEnabled { 148 | // env var for subprocess to detect vmnet 149 | l.host = l.host.WithEnv(vmnet.SubProcessEnvVar + "=1") 150 | } 151 | 152 | return ctx, nil 153 | } 154 | -------------------------------------------------------------------------------- /environment/vm/lima/file.go: -------------------------------------------------------------------------------- 1 | package lima 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/fs" 7 | "os" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/abiosoft/colima/environment" 14 | ) 15 | 16 | func (l limaVM) Read(fileName string) (string, error) { 17 | s, err := l.RunOutput("sudo", "cat", fileName) 18 | if err != nil { 19 | return "", fmt.Errorf("cannot read file '%s': %w", fileName, err) 20 | } 21 | return s, err 22 | } 23 | 24 | func (l *limaVM) Write(fileName string, body []byte) error { 25 | var stdin = bytes.NewReader(body) 26 | dir := filepath.Dir(fileName) 27 | if err := l.RunQuiet("sudo", "mkdir", "-p", dir); err != nil { 28 | return fmt.Errorf("error creating directory '%s': %w", dir, err) 29 | } 30 | return l.RunWith(stdin, nil, "sudo", "sh", "-c", "cat > "+fileName) 31 | } 32 | 33 | func (l *limaVM) Stat(fileName string) (os.FileInfo, error) { 34 | return newFileInfo(l, fileName) 35 | } 36 | 37 | var _ os.FileInfo = (*fileInfo)(nil) 38 | 39 | type fileInfo struct { 40 | isDir bool 41 | modTime time.Time 42 | mode fs.FileMode 43 | name string 44 | size int64 45 | } 46 | 47 | func newFileInfo(guest environment.GuestActions, filename string) (fileInfo, error) { 48 | info := fileInfo{} 49 | // "%s,%a,%Y,%F" -> size, permission, modified time, type 50 | stat, err := guest.RunOutput("sudo", "stat", "-c", "%s,%a,%Y,%F", filename) 51 | if err != nil { 52 | return info, statError(filename, err) 53 | } 54 | stats := strings.Split(stat, ",") 55 | if len(stats) < 4 { 56 | return info, statError(filename, err) 57 | } 58 | info.name = filename 59 | info.size, _ = strconv.ParseInt(stats[0], 10, 64) 60 | info.mode = func() fs.FileMode { 61 | mode, _ := strconv.Atoi(stats[1]) 62 | return fs.FileMode(mode) 63 | }() 64 | info.modTime = func() time.Time { 65 | unix, _ := strconv.ParseInt(stats[2], 10, 64) 66 | return time.Unix(unix, 0) 67 | }() 68 | info.isDir = stats[3] == "directory" 69 | 70 | return info, nil 71 | } 72 | 73 | func statError(filename string, err error) error { 74 | return fmt.Errorf("cannot stat file '%s': %w", filename, err) 75 | } 76 | 77 | // IsDir implements fs.FileInfo 78 | func (f fileInfo) IsDir() bool { return f.isDir } 79 | 80 | // ModTime implements fs.FileInfo 81 | func (f fileInfo) ModTime() time.Time { return f.modTime } 82 | 83 | // Mode implements fs.FileInfo 84 | func (f fileInfo) Mode() fs.FileMode { return f.mode } 85 | 86 | // Name implements fs.FileInfo 87 | func (f fileInfo) Name() string { return f.name } 88 | 89 | // Size implements fs.FileInfo 90 | func (f fileInfo) Size() int64 { return f.size } 91 | 92 | // Sys implements fs.FileInfo 93 | func (fileInfo) Sys() any { return nil } 94 | -------------------------------------------------------------------------------- /environment/vm/lima/limautil/disk.go: -------------------------------------------------------------------------------- 1 | package limautil 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/abiosoft/colima/config" 9 | ) 10 | 11 | // HasDisk checks if a lima disk exists for the current instance. 12 | func HasDisk() bool { 13 | name := config.CurrentProfile().ID 14 | 15 | var resp struct { 16 | Name string `json:"name"` 17 | } 18 | 19 | cmd := Limactl("disk", "list", "--json", name) 20 | var buf bytes.Buffer 21 | cmd.Stdout = &buf 22 | 23 | if err := cmd.Run(); err != nil { 24 | return false 25 | } 26 | 27 | if err := json.NewDecoder(&buf).Decode(&resp); err != nil { 28 | return false 29 | } 30 | 31 | return resp.Name == name 32 | } 33 | 34 | // CreateDisk creates a lima disk with size in GiB. 35 | func CreateDisk(size int) error { 36 | name := config.CurrentProfile().ID 37 | cmd := Limactl("disk", "create", name, "--size", fmt.Sprintf("%dGiB", size)) 38 | 39 | if err := cmd.Run(); err != nil { 40 | return fmt.Errorf("error creating lima disk: %w", err) 41 | } 42 | 43 | return nil 44 | } 45 | 46 | // DeleteDisk deletes lima disk for the current instance. 47 | func DeleteDisk() error { 48 | name := config.CurrentProfile().ID 49 | cmd := Limactl("disk", "delete", name) 50 | 51 | if err := cmd.Run(); err != nil { 52 | return fmt.Errorf("error deleting lima disk: %w", err) 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /environment/vm/lima/limautil/files.go: -------------------------------------------------------------------------------- 1 | package limautil 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/abiosoft/colima/config" 7 | ) 8 | 9 | const colimaDiffDiskFile = "diffdisk" 10 | 11 | // ColimaDiffDisk returns path to the diffdisk for the colima VM. 12 | func ColimaDiffDisk(profileID string) string { 13 | return filepath.Join(config.ProfileFromName(profileID).LimaInstanceDir(), colimaDiffDiskFile) 14 | } 15 | 16 | const networkFile = "networks.yaml" 17 | 18 | // NetworkFile returns path to the network file. 19 | func NetworkFile() string { 20 | return filepath.Join(config.LimaDir(), "_config", networkFile) 21 | } 22 | 23 | // NetworkAssetsDirecotry returns the directory for the generated network assets. 24 | func NetworkAssetsDirectory() string { 25 | return filepath.Join(config.LimaDir(), "_networks") 26 | } 27 | -------------------------------------------------------------------------------- /environment/vm/lima/limautil/instance.go: -------------------------------------------------------------------------------- 1 | package limautil 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/abiosoft/colima/config" 11 | "github.com/abiosoft/colima/config/configmanager" 12 | ) 13 | 14 | // Instance returns current instance. 15 | func Instance() (InstanceInfo, error) { 16 | return getInstance(config.CurrentProfile().ID) 17 | } 18 | 19 | // InstanceInfo is the information about a Lima instance 20 | type InstanceInfo struct { 21 | Name string `json:"name,omitempty"` 22 | Status string `json:"status,omitempty"` 23 | Arch string `json:"arch,omitempty"` 24 | CPU int `json:"cpus,omitempty"` 25 | Memory int64 `json:"memory,omitempty"` 26 | Disk int64 `json:"disk,omitempty"` 27 | Dir string `json:"dir,omitempty"` 28 | Network []struct { 29 | VNL string `json:"vnl,omitempty"` 30 | Interface string `json:"interface,omitempty"` 31 | } `json:"network,omitempty"` 32 | IPAddress string `json:"address,omitempty"` 33 | Runtime string `json:"runtime,omitempty"` 34 | } 35 | 36 | // Running checks if the instance is running. 37 | func (i InstanceInfo) Running() bool { return i.Status == limaStatusRunning } 38 | 39 | // Config returns the current Colima config 40 | func (i InstanceInfo) Config() (config.Config, error) { 41 | return configmanager.LoadFrom(config.ProfileFromName(i.Name).StateFile()) 42 | } 43 | 44 | // Lima statuses 45 | const ( 46 | limaStatusRunning = "Running" 47 | ) 48 | 49 | func getInstance(profileID string) (InstanceInfo, error) { 50 | var i InstanceInfo 51 | var buf bytes.Buffer 52 | cmd := Limactl("list", profileID, "--json") 53 | cmd.Stderr = nil 54 | cmd.Stdout = &buf 55 | 56 | if err := cmd.Run(); err != nil { 57 | return i, fmt.Errorf("error retrieving instance: %w", err) 58 | } 59 | 60 | if buf.Len() == 0 { 61 | return i, fmt.Errorf("instance '%s' does not exist", config.ProfileFromName(profileID).DisplayName) 62 | } 63 | 64 | if err := json.Unmarshal(buf.Bytes(), &i); err != nil { 65 | return i, fmt.Errorf("error retrieving instance: %w", err) 66 | } 67 | return i, nil 68 | } 69 | 70 | // Instances returns Lima instances created by colima. 71 | func Instances(ids ...string) ([]InstanceInfo, error) { 72 | limaIDs := make([]string, len(ids)) 73 | for i := range ids { 74 | limaIDs[i] = config.ProfileFromName(ids[i]).ID 75 | } 76 | args := append([]string{"list", "--json"}, limaIDs...) 77 | 78 | var buf bytes.Buffer 79 | cmd := Limactl(args...) 80 | cmd.Stderr = nil 81 | cmd.Stdout = &buf 82 | 83 | if err := cmd.Run(); err != nil { 84 | return nil, fmt.Errorf("error retrieving instances: %w", err) 85 | } 86 | 87 | var instances []InstanceInfo 88 | scanner := bufio.NewScanner(&buf) 89 | for scanner.Scan() { 90 | var i InstanceInfo 91 | line := scanner.Bytes() 92 | if err := json.Unmarshal(line, &i); err != nil { 93 | return nil, fmt.Errorf("error retrieving instances: %w", err) 94 | } 95 | 96 | // limit to colima instances 97 | if !strings.HasPrefix(i.Name, "colima") { 98 | continue 99 | } 100 | 101 | if i.Running() { 102 | for _, n := range i.Network { 103 | if n.Interface == NetInterface { 104 | i.IPAddress = getIPAddress(i.Name, NetInterface) 105 | } 106 | } 107 | conf, _ := i.Config() 108 | i.Runtime = getRuntime(conf) 109 | } 110 | 111 | // rename to local friendly names 112 | i.Name = config.ProfileFromName(i.Name).ShortName 113 | 114 | // network is low level, remove 115 | i.Network = nil 116 | 117 | instances = append(instances, i) 118 | } 119 | 120 | return instances, nil 121 | } 122 | 123 | // RunningInstances return Lima instances that are has a running status. 124 | func RunningInstances() ([]InstanceInfo, error) { 125 | allInstances, err := Instances() 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | var runningInstances []InstanceInfo 131 | for _, instance := range allInstances { 132 | if instance.Running() { 133 | runningInstances = append(runningInstances, instance) 134 | } 135 | } 136 | 137 | return runningInstances, nil 138 | } 139 | 140 | func getRuntime(conf config.Config) string { 141 | var runtime string 142 | 143 | switch conf.Runtime { 144 | case "docker", "containerd", "incus": 145 | runtime = conf.Runtime 146 | case "none": 147 | return "none" 148 | default: 149 | return "" 150 | } 151 | 152 | if conf.Kubernetes.Enabled { 153 | runtime += "+k3s" 154 | } 155 | return runtime 156 | } 157 | -------------------------------------------------------------------------------- /environment/vm/lima/limautil/limautil.go: -------------------------------------------------------------------------------- 1 | package limautil 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "github.com/abiosoft/colima/cli" 8 | "github.com/abiosoft/colima/config" 9 | ) 10 | 11 | // EnvLimaHome is the environment variable for the Lima directory. 12 | const EnvLimaHome = "LIMA_HOME" 13 | 14 | // LimactlCommand is the limactl command. 15 | const LimactlCommand = "limactl" 16 | 17 | // Limactl prepares a limactl command. 18 | func Limactl(args ...string) *exec.Cmd { 19 | cmd := cli.Command(LimactlCommand, args...) 20 | cmd.Env = append(cmd.Env, os.Environ()...) 21 | cmd.Env = append(cmd.Env, EnvLimaHome+"="+config.LimaDir()) 22 | return cmd 23 | } 24 | -------------------------------------------------------------------------------- /environment/vm/lima/limautil/network.go: -------------------------------------------------------------------------------- 1 | package limautil 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | // network interface for shared network in the virtual machine. 9 | const NetInterface = "col0" 10 | 11 | // network metric for the route 12 | const NetMetric = 300 13 | 14 | // IPAddress returns the ip address for profile. 15 | // It returns the PTP address if networking is enabled or falls back to 127.0.0.1. 16 | // It is guaranteed to return a value. 17 | // 18 | // TODO: unnecessary round-trip is done to get instance details from Lima. 19 | func IPAddress(profileID string) string { 20 | const fallback = "127.0.0.1" 21 | instance, err := getInstance(profileID) 22 | if err != nil { 23 | return fallback 24 | } 25 | 26 | if len(instance.Network) > 0 { 27 | for _, n := range instance.Network { 28 | if n.Interface == NetInterface { 29 | return getIPAddress(profileID, n.Interface) 30 | } 31 | } 32 | } 33 | 34 | return fallback 35 | } 36 | 37 | func getIPAddress(profileID, interfaceName string) string { 38 | var buf bytes.Buffer 39 | // TODO: this should be less hacky 40 | cmd := Limactl("shell", profileID, "sh", "-c", 41 | `ip -4 addr show `+interfaceName+` | grep inet | awk -F' ' '{print $2 }' | cut -d/ -f1`) 42 | cmd.Stderr = nil 43 | cmd.Stdout = &buf 44 | 45 | _ = cmd.Run() 46 | return strings.TrimSpace(buf.String()) 47 | } 48 | -------------------------------------------------------------------------------- /environment/vm/lima/limautil/ssh.go: -------------------------------------------------------------------------------- 1 | package limautil 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/abiosoft/colima/config" 12 | ) 13 | 14 | // ShowSSH runs the show-ssh command in Lima. 15 | // returns the ssh output, if in layer, and an error if any 16 | func ShowSSH(profileID string) (resp struct { 17 | Output string 18 | File struct { 19 | Lima string 20 | Colima string 21 | } 22 | }, err error) { 23 | ssh := sshConfig(profileID) 24 | sshConf, err := ssh.Contents() 25 | if err != nil { 26 | return resp, fmt.Errorf("error retrieving ssh config: %w", err) 27 | } 28 | 29 | resp.Output = replaceSSHConfig(sshConf, profileID) 30 | resp.File.Lima = ssh.File() 31 | resp.File.Colima = config.SSHConfigFile() 32 | return resp, nil 33 | } 34 | 35 | func replaceSSHConfig(conf string, profileID string) string { 36 | profileID = config.ProfileFromName(profileID).ID 37 | 38 | var out bytes.Buffer 39 | scanner := bufio.NewScanner(strings.NewReader(conf)) 40 | 41 | for scanner.Scan() { 42 | line := scanner.Text() 43 | 44 | if strings.HasPrefix(line, "Host ") { 45 | line = "Host " + profileID 46 | } 47 | 48 | _, _ = fmt.Fprintln(&out, line) 49 | } 50 | return out.String() 51 | } 52 | 53 | const sshConfigFile = "ssh.config" 54 | 55 | // sshConfig is the ssh configuration file for a Colima profile. 56 | type sshConfig string 57 | 58 | // Contents returns the content of the SSH config file. 59 | func (s sshConfig) Contents() (string, error) { 60 | profile := config.ProfileFromName(string(s)) 61 | b, err := os.ReadFile(s.File()) 62 | if err != nil { 63 | return "", fmt.Errorf("error retrieving Lima SSH config file for profile '%s': %w", strings.TrimPrefix(profile.DisplayName, "lima"), err) 64 | } 65 | return string(b), nil 66 | } 67 | 68 | // File returns the path to the SSH config file. 69 | func (s sshConfig) File() string { 70 | profile := config.ProfileFromName(string(s)) 71 | return filepath.Join(profile.LimaInstanceDir(), sshConfigFile) 72 | } 73 | -------------------------------------------------------------------------------- /environment/vm/lima/network.go: -------------------------------------------------------------------------------- 1 | package lima 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/abiosoft/colima/config" 9 | "github.com/abiosoft/colima/config/configmanager" 10 | "github.com/abiosoft/colima/embedded" 11 | "github.com/abiosoft/colima/environment/vm/lima/limautil" 12 | "github.com/abiosoft/colima/util" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func (l *limaVM) writeNetworkFile() error { 17 | networkFile := limautil.NetworkFile() 18 | embeddedFile, err := embedded.Read("network/networks.yaml") 19 | if err != nil { 20 | return fmt.Errorf("error reading embedded network config file: %w", err) 21 | } 22 | 23 | // if there are no running instances, clear network directory 24 | if instances, err := limautil.RunningInstances(); err == nil && len(instances) == 0 { 25 | if err := os.RemoveAll(limautil.NetworkAssetsDirectory()); err != nil { 26 | logrus.Warnln(fmt.Errorf("could not clear network assets directory: %w", err)) 27 | } 28 | } 29 | 30 | if err := os.MkdirAll(filepath.Dir(networkFile), 0755); err != nil { 31 | return fmt.Errorf("error creating Lima config directory: %w", err) 32 | } 33 | if err := os.WriteFile(networkFile, embeddedFile, 0755); err != nil { 34 | return fmt.Errorf("error writing Lima network config file: %w", err) 35 | } 36 | return nil 37 | } 38 | 39 | func (l *limaVM) replicateHostAddresses(conf config.Config) error { 40 | if !conf.Network.Address && conf.Network.HostAddresses { 41 | for _, ip := range util.HostIPAddresses() { 42 | if err := l.RunQuiet("sudo", "ip", "address", "add", ip.String()+"/24", "dev", "lo"); err != nil { 43 | return err 44 | } 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | func (l *limaVM) removeHostAddresses() { 51 | conf, _ := configmanager.LoadInstance() 52 | if !conf.Network.Address && conf.Network.HostAddresses { 53 | for _, ip := range util.HostIPAddresses() { 54 | _ = l.RunQuiet("sudo", "ip", "address", "del", ip.String()+"/24", "dev", "lo") 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /environment/vm/lima/shell.go: -------------------------------------------------------------------------------- 1 | package lima 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/abiosoft/colima/config" 8 | ) 9 | 10 | func (l limaVM) Run(args ...string) error { 11 | args = append([]string{lima}, args...) 12 | 13 | a := l.Init(context.Background()) 14 | 15 | a.Add(func() error { 16 | return l.host.Run(args...) 17 | }) 18 | 19 | return a.Exec() 20 | } 21 | 22 | func (l limaVM) SSH(workingDir string, args ...string) error { 23 | args = append([]string{limactl, "shell", "--workdir", workingDir, config.CurrentProfile().ID}, args...) 24 | 25 | a := l.Init(context.Background()) 26 | 27 | a.Add(func() error { 28 | return l.host.RunInteractive(args...) 29 | }) 30 | 31 | return a.Exec() 32 | } 33 | 34 | func (l limaVM) RunInteractive(args ...string) error { 35 | args = append([]string{lima}, args...) 36 | 37 | a := l.Init(context.Background()) 38 | 39 | a.Add(func() error { 40 | return l.host.RunInteractive(args...) 41 | }) 42 | 43 | return a.Exec() 44 | } 45 | 46 | func (l limaVM) RunWith(stdin io.Reader, stdout io.Writer, args ...string) error { 47 | args = append([]string{lima}, args...) 48 | 49 | a := l.Init(context.Background()) 50 | 51 | a.Add(func() error { 52 | return l.host.RunWith(stdin, stdout, args...) 53 | }) 54 | 55 | return a.Exec() 56 | } 57 | 58 | func (l limaVM) RunOutput(args ...string) (out string, err error) { 59 | args = append([]string{lima}, args...) 60 | 61 | a := l.Init(context.Background()) 62 | 63 | a.Add(func() (err error) { 64 | out, err = l.host.RunOutput(args...) 65 | return 66 | }) 67 | 68 | err = a.Exec() 69 | return 70 | } 71 | 72 | func (l limaVM) RunQuiet(args ...string) (err error) { 73 | args = append([]string{lima}, args...) 74 | 75 | a := l.Init(context.Background()) 76 | 77 | a.Add(func() (err error) { 78 | return l.host.RunQuiet(args...) 79 | }) 80 | 81 | err = a.Exec() 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /environment/vm/lima/yaml_test.go: -------------------------------------------------------------------------------- 1 | package lima 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/abiosoft/colima/config" 11 | "github.com/abiosoft/colima/environment/vm/lima/limaconfig" 12 | "github.com/abiosoft/colima/util" 13 | "github.com/abiosoft/colima/util/fsutil" 14 | ) 15 | 16 | func Test_checkOverlappingMounts(t *testing.T) { 17 | type args struct { 18 | mounts []string 19 | } 20 | tests := []struct { 21 | args args 22 | wantErr bool 23 | }{ 24 | {args: args{mounts: []string{"/User", "/User/something"}}, wantErr: true}, 25 | {args: args{mounts: []string{"/User/one", "/User/two"}}, wantErr: false}, 26 | {args: args{mounts: []string{"/User/one", "/User/one_other"}}, wantErr: false}, 27 | {args: args{mounts: []string{"/User/one_other", "/User/one"}}, wantErr: false}, 28 | {args: args{mounts: []string{"/User/one", "/User/one/other"}}, wantErr: true}, 29 | {args: args{mounts: []string{"/User/one/", "/User/one"}}, wantErr: true}, 30 | {args: args{mounts: []string{"/User/one/", "/User/two", "User/one"}}, wantErr: true}, 31 | {args: args{mounts: []string{"/home/a/b/c", "/home/b/c/a", "/home/c/a/b"}}, wantErr: false}, 32 | } 33 | for i, tt := range tests { 34 | t.Run(fmt.Sprint(i), func(t *testing.T) { 35 | mounts := func(mounts []string) (mnts []config.Mount) { 36 | for _, m := range mounts { 37 | mnts = append(mnts, config.Mount{Location: m}) 38 | } 39 | return 40 | }(tt.args.mounts) 41 | if err := checkOverlappingMounts(mounts); (err != nil) != tt.wantErr { 42 | t.Errorf("checkOverlappingMounts() error = %v, wantErr %v", err, tt.wantErr) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func Test_config_Mounts(t *testing.T) { 49 | fsutil.FS = fsutil.FakeFS 50 | tests := []struct { 51 | mounts []string 52 | isDefault bool 53 | includesCache bool 54 | }{ 55 | {mounts: []string{"/User/user", "/tmp/another"}}, 56 | {mounts: []string{"/User/another", "/User/something", "/User/else"}}, 57 | {isDefault: true}, 58 | {mounts: []string{util.HomeDir()}, includesCache: true}, 59 | } 60 | for i, tt := range tests { 61 | t.Run(fmt.Sprint(i), func(t *testing.T) { 62 | mounts := func(mounts []string) (mnts []config.Mount) { 63 | for _, m := range mounts { 64 | mnts = append(mnts, config.Mount{Location: m}) 65 | } 66 | return 67 | }(tt.mounts) 68 | conf, err := newConf(context.Background(), config.Config{Mounts: mounts}) 69 | if err != nil { 70 | t.Error(err) 71 | return 72 | } 73 | 74 | expectedLocations := tt.mounts 75 | if tt.isDefault { 76 | expectedLocations = []string{"~", "/tmp/colima"} 77 | } else if !tt.includesCache { 78 | expectedLocations = append([]string{config.CacheDir()}, tt.mounts...) 79 | } 80 | 81 | sameMounts := func(expectedLocations []string, mounts []limaconfig.Mount) bool { 82 | sanitize := func(s string) string { return strings.TrimSuffix(s, "/") + "/" } 83 | for i, m := range mounts { 84 | if sanitize(m.Location) != sanitize(expectedLocations[i]) { 85 | return false 86 | } 87 | } 88 | return true 89 | }(expectedLocations, conf.Mounts) 90 | if !sameMounts { 91 | foundLocations := func() (locations []string) { 92 | for _, m := range conf.Mounts { 93 | locations = append(locations, m.Location) 94 | } 95 | return 96 | }() 97 | t.Errorf("got: %+v, want: %v", foundLocations, expectedLocations) 98 | } 99 | }) 100 | } 101 | } 102 | 103 | func Test_ingressDisabled(t *testing.T) { 104 | tests := []struct { 105 | args []string 106 | want bool 107 | }{ 108 | {args: []string{"--flag=f", "--another", "flag"}, want: false}, 109 | {args: []string{"--disable=traefik", "--version=3"}, want: true}, 110 | {args: []string{}, want: false}, 111 | {args: []string{"--disable", "traefik", "--one=two"}, want: true}, 112 | } 113 | for i, tt := range tests { 114 | t.Run(strconv.Itoa(i+1), func(t *testing.T) { 115 | if got := ingressDisabled(tt.args); got != tt.want { 116 | t.Errorf("ingressDisabled() = %v, want %v", got, tt.want) 117 | } 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1656065134, 6 | "narHash": "sha256-oc6E6ByIw3oJaIyc67maaFcnjYOz1mMcOtHxbEf9NwQ=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "bee6a7250dd1b01844a2de7e02e4df7d8a0a206c", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "id": "flake-utils", 14 | "type": "indirect" 15 | } 16 | }, 17 | "nixpkgs": { 18 | "locked": { 19 | "lastModified": 1678654296, 20 | "narHash": "sha256-aVfw3ThpY7vkUeF1rFy10NAkpKDS2imj3IakrzT0Occ=", 21 | "owner": "NixOS", 22 | "repo": "nixpkgs", 23 | "rev": "5a1dc8acd977ff3dccd1328b7c4a6995429a656b", 24 | "type": "github" 25 | }, 26 | "original": { 27 | "owner": "NixOS", 28 | "ref": "nixos-unstable", 29 | "repo": "nixpkgs", 30 | "type": "github" 31 | } 32 | }, 33 | "root": { 34 | "inputs": { 35 | "flake-utils": "flake-utils", 36 | "nixpkgs": "nixpkgs" 37 | } 38 | } 39 | }, 40 | "root": "root", 41 | "version": 7 42 | } 43 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Container runtimes on macOS (and Linux) with minimal setup"; 3 | 4 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 5 | 6 | outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem 7 | (system: 8 | let 9 | pkgs = nixpkgs.legacyPackages.${system}; 10 | in 11 | { 12 | packages.default = import ./colima.nix { inherit pkgs; }; 13 | devShell = import ./shell.nix { inherit pkgs; }; 14 | apps.default = { 15 | type = "app"; 16 | program = "${self.packages.${system}.default}/bin/colima"; 17 | }; 18 | } 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/abiosoft/colima 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/coreos/go-semver v0.3.1 7 | github.com/docker/go-units v0.5.0 8 | github.com/fatih/color v1.18.0 9 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 10 | github.com/rjeczalik/notify v0.9.3 11 | github.com/sevlyar/go-daemon v0.1.6 12 | github.com/sirupsen/logrus v1.9.3 13 | github.com/spf13/cobra v1.9.1 14 | golang.org/x/term v0.29.0 15 | gopkg.in/yaml.v3 v3.0.1 16 | ) 17 | 18 | require ( 19 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 20 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect 21 | github.com/mattn/go-colorable v0.1.13 // indirect 22 | github.com/mattn/go-isatty v0.0.20 // indirect 23 | github.com/spf13/pflag v1.0.6 // indirect 24 | github.com/stretchr/testify v1.8.4 // indirect 25 | golang.org/x/sys v0.30.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= 2 | github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 8 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 9 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 10 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 11 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 12 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 13 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 14 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 15 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= 16 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= 17 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 18 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 19 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 20 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 21 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= 25 | github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= 26 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 27 | github.com/sevlyar/go-daemon v0.1.6 h1:EUh1MDjEM4BI109Jign0EaknA2izkOyi0LV3ro3QQGs= 28 | github.com/sevlyar/go-daemon v0.1.6/go.mod h1:6dJpPatBT9eUwM5VCw9Bt6CdX9Tk6UWvhW3MebLDRKE= 29 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 30 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 31 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 32 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 33 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 34 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 35 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 36 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 37 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 38 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 39 | golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 40 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 44 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 45 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 46 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 47 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 48 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 49 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 51 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 52 | -------------------------------------------------------------------------------- /integration/Dockerfile: -------------------------------------------------------------------------------- 1 | # sample dockerfile to test image building 2 | # without pulling from docker hub 3 | FROM scratch 4 | 5 | COPY . /files -------------------------------------------------------------------------------- /scripts/build_vmnet.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -ex 4 | 5 | export DIR_BUILD=$PWD/_build/network 6 | export DIR_VMNET=$DIR_BUILD/socket_vmnet 7 | export EMBED_DIR=$PWD/embedded/network 8 | 9 | clone() ( 10 | if [ ! -d "$2" ]; then 11 | git clone "$1" "$2" 12 | fi 13 | ) 14 | 15 | mkdir -p "$DIR_BUILD" 16 | clone https://github.com/lima-vm/socket_vmnet.git "$DIR_VMNET" 17 | 18 | move_to_embed_dir() ( 19 | mkdir -p "$EMBED_DIR"/vmnet/bin 20 | cp "$DIR_VMNET"/socket_vmnet "$DIR_VMNET"/socket_vmnet_client "$EMBED_DIR"/vmnet/bin 21 | cd "$EMBED_DIR"/vmnet && tar cvfz "$EMBED_DIR"/vmnet_"${1}".tar.gz bin/socket_vmnet bin/socket_vmnet_client 22 | rm -rf "$EMBED_DIR"/vmnet 23 | ) 24 | 25 | build_x86_64() ( 26 | cd "$DIR_VMNET" 27 | 28 | # pinning to a commit for consistency 29 | git checkout v1.1.5 30 | make ARCH=x86_64 31 | 32 | move_to_embed_dir x86_64 33 | 34 | # cleanup 35 | make clean 36 | ) 37 | 38 | build_arm64() ( 39 | cd "$DIR_VMNET" 40 | 41 | # pinning to a commit for consistency 42 | git checkout v1.1.5 43 | make ARCH=arm64 44 | move_to_embed_dir arm64 45 | 46 | # cleanup 47 | make clean 48 | ) 49 | 50 | test_archives() ( 51 | TEMP_DIR=/tmp/colima-test-archives 52 | rm -rf $TEMP_DIR 53 | mkdir -p $TEMP_DIR/x86 $TEMP_DIR/arm 54 | ( 55 | cp "$EMBED_DIR"/vmnet_x86_64.tar.gz $TEMP_DIR/x86 56 | cd $TEMP_DIR/x86 && tar xvfz vmnet_x86_64.tar.gz 57 | ) 58 | ( 59 | cp "$EMBED_DIR"/vmnet_arm64.tar.gz $TEMP_DIR/arm 60 | cd $TEMP_DIR/arm && tar xvfz vmnet_arm64.tar.gz 61 | ) 62 | 63 | assert_not_equal() ( 64 | if diff $TEMP_DIR/x86/"$1" $TEMP_DIR/arm/"$1"; then 65 | echo "$1" is same for both arch 66 | exit 1 67 | fi 68 | ) 69 | 70 | assert_not_equal bin/socket_vmnet 71 | assert_not_equal bin/socket_vmnet_client 72 | ) 73 | 74 | build_x86_64 75 | build_arm64 76 | test_archives 77 | -------------------------------------------------------------------------------- /scripts/integration.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | alias colima="$COLIMA_BINARY" 6 | DOCKER_CONTEXT="$(docker info -f '{{json .}}' | jq -r '.ClientInfo.Context')" 7 | 8 | OTHER_ARCH="amd64" 9 | if [ "$GOARCH" == "amd64" ]; then 10 | OTHER_ARCH="arm64" 11 | fi 12 | 13 | stage() ( 14 | set +x 15 | echo 16 | echo "######################################" 17 | echo "$@" 18 | echo "######################################" 19 | echo 20 | set -x 21 | ) 22 | 23 | test_runtime() ( 24 | stage "runtime: $2, arch: $1" 25 | 26 | NAME="itest-$2" 27 | COLIMA="$COLIMA_BINARY -p $NAME" 28 | 29 | COMMAND="docker" 30 | if [ "$2" == "containerd" ]; then 31 | COMMAND="$COLIMA nerdctl --" 32 | fi 33 | 34 | # reset 35 | $COLIMA delete -f 36 | 37 | # start 38 | $COLIMA start --arch "$1" --runtime "$2" 39 | 40 | # validate 41 | $COMMAND ps && $COMMAND info 42 | 43 | # validate DNS 44 | $COLIMA ssh -- nslookup host.docker.internal 45 | 46 | # valid building image 47 | $COMMAND build integration 48 | 49 | # teardown 50 | $COLIMA delete -f 51 | ) 52 | 53 | test_kubernetes() ( 54 | stage "k8s runtime: $2, arch: $1" 55 | 56 | NAME="itest-$2-k8s" 57 | COLIMA="$COLIMA_BINARY -p $NAME" 58 | 59 | # reset 60 | $COLIMA delete -f 61 | 62 | # start 63 | $COLIMA start --arch "$1" --runtime "$2" --kubernetes 64 | 65 | # short delay 66 | sleep 5 67 | 68 | # validate 69 | kubectl cluster-info && kubectl version && kubectl get nodes -o wide 70 | 71 | # teardown 72 | $COLIMA delete -f 73 | ) 74 | 75 | test_runtime $GOARCH docker 76 | test_runtime $GOARCH containerd 77 | test_kubernetes $GOARCH docker 78 | test_kubernetes $GOARCH containerd 79 | test_runtime $OTHER_ARCH docker 80 | test_runtime $OTHER_ARCH containerd 81 | 82 | if [ -n "$DOCKER_CONTEXT" ]; then 83 | docker context use "$DOCKER_CONTEXT" || echo # prevent error 84 | fi 85 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | 3 | pkgs.mkShell { 4 | # nativeBuildInputs is usually what you want -- tools you need to run 5 | nativeBuildInputs = with pkgs.buildPackages; [ 6 | go_1_20 7 | git 8 | lima 9 | qemu 10 | ]; 11 | shellHook = '' 12 | echo Nix Shell with $(go version) 13 | echo 14 | 15 | COLIMA_BIN="$PWD/$(make print-binary-name)" 16 | if [ ! -f "$COLIMA_BIN" ]; then 17 | echo "Run 'make' to build Colima." 18 | echo 19 | fi 20 | 21 | set -x 22 | set -x 23 | alias colima="$COLIMA_BIN" 24 | set +x 25 | ''; 26 | } 27 | -------------------------------------------------------------------------------- /util/debutil/debutil.go: -------------------------------------------------------------------------------- 1 | package debutil 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/abiosoft/colima/cli" 9 | "github.com/abiosoft/colima/environment" 10 | ) 11 | 12 | // packages is list of deb package names. 13 | type packages []string 14 | 15 | // Upgradable returns the shell command to check if the packages are upgradable with apt. 16 | // The returned command should be passed to 'sh -c' or equivalent. 17 | func (p packages) Upgradable() string { 18 | cmd := "sudo apt list --upgradable | grep" 19 | for _, v := range p { 20 | cmd += fmt.Sprintf(" -e '^%s/'", v) 21 | } 22 | return cmd 23 | } 24 | 25 | // Install returns the shell command to install the packages with apt. 26 | // The returned command should be passed to 'sh -c' or equivalent. 27 | func (p packages) Install() string { 28 | return "sudo apt-get install -y --allow-change-held-packages " + strings.Join(p, " ") 29 | } 30 | 31 | func UpdateRuntime( 32 | ctx context.Context, 33 | guest environment.GuestActions, 34 | chain cli.CommandChain, 35 | packageNames ...string, 36 | ) (bool, error) { 37 | a := chain.Init(ctx) 38 | log := a.Logger() 39 | 40 | packages := packages(packageNames) 41 | 42 | hasUpdates := false 43 | updated := false 44 | 45 | a.Stage("refreshing package manager") 46 | a.Add(func() error { 47 | return guest.RunQuiet( 48 | "sh", 49 | "-c", 50 | "sudo apt-get update -y", 51 | ) 52 | }) 53 | 54 | a.Stage("checking for updates") 55 | a.Add(func() error { 56 | err := guest.RunQuiet( 57 | "sh", 58 | "-c", 59 | packages.Upgradable(), 60 | ) 61 | hasUpdates = err == nil 62 | return nil 63 | }) 64 | 65 | a.Add(func() (err error) { 66 | if !hasUpdates { 67 | log.Warnln("no updates available") 68 | return 69 | } 70 | 71 | log.Println("updating packages ...") 72 | err = guest.RunQuiet( 73 | "sh", 74 | "-c", 75 | packages.Install(), 76 | ) 77 | if err == nil { 78 | updated = true 79 | log.Println("done") 80 | } 81 | return 82 | }) 83 | 84 | // it is necessary to execute the chain here to get the correct value for `updated`. 85 | err := a.Exec() 86 | return updated, err 87 | } 88 | -------------------------------------------------------------------------------- /util/downloader/download.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/abiosoft/colima/config" 11 | "github.com/abiosoft/colima/environment" 12 | "github.com/abiosoft/colima/util/shautil" 13 | "github.com/abiosoft/colima/util/terminal" 14 | ) 15 | 16 | type ( 17 | hostActions = environment.HostActions 18 | guestActions = environment.GuestActions 19 | ) 20 | 21 | // Request is download request 22 | type Request struct { 23 | URL string // request URL 24 | SHA *SHA // shasum url 25 | } 26 | 27 | // DownloadToGuest downloads file at url and saves it in the destination. 28 | // 29 | // In the implementation, the file is downloaded (and cached) on the host, but copied to the desired 30 | // destination for the guest. 31 | // filename must be an absolute path and a directory on the guest that does not require root access. 32 | func DownloadToGuest(host hostActions, guest guestActions, r Request, filename string) error { 33 | // if file is on the filesystem, no need for download. A copy suffices 34 | if strings.HasPrefix(r.URL, "/") { 35 | return guest.RunQuiet("cp", r.URL, filename) 36 | } 37 | 38 | cacheFile, err := Download(host, r) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return guest.RunQuiet("cp", cacheFile, filename) 44 | } 45 | 46 | // Download downloads file at url and returns the location of the downloaded file. 47 | func Download(host hostActions, r Request) (string, error) { 48 | d := downloader{ 49 | host: host, 50 | } 51 | 52 | if !d.hasCache(r.URL) { 53 | if err := d.downloadFile(r); err != nil { 54 | return "", fmt.Errorf("error downloading '%s': %w", r.URL, err) 55 | } 56 | } 57 | 58 | return CacheFilename(r.URL), nil 59 | } 60 | 61 | type downloader struct { 62 | host hostActions 63 | } 64 | 65 | // CacheFilename returns the computed filename for the url. 66 | func CacheFilename(url string) string { 67 | return filepath.Join(config.CacheDir(), "caches", shautil.SHA256(url).String()) 68 | } 69 | 70 | func (d downloader) cacheDownloadingFileName(url string) string { 71 | return CacheFilename(url) + ".downloading" 72 | } 73 | 74 | func (d downloader) downloadFile(r Request) (err error) { 75 | // save to a temporary file initially before renaming to the desired file after successful download 76 | // this prevents having a corrupt file 77 | cacheDownloadingFilename := d.cacheDownloadingFileName(r.URL) 78 | if err := d.host.RunQuiet("mkdir", "-p", filepath.Dir(cacheDownloadingFilename)); err != nil { 79 | return fmt.Errorf("error preparing cache dir: %w", err) 80 | } 81 | 82 | // get rid of curl's initial progress bar by getting the redirect url directly. 83 | downloadURL, err := d.host.RunOutput("curl", "-ILs", "-o", "/dev/null", "-w", "%{url_effective}", r.URL) 84 | if err != nil { 85 | return fmt.Errorf("error retrieving redirect url: %w", err) 86 | } 87 | 88 | // ask curl to resume previous download if possible "-C -" 89 | if err := d.host.RunInteractive("curl", "-L", "-#", "-C", "-", "-o", cacheDownloadingFilename, downloadURL); err != nil { 90 | return err 91 | } 92 | // clear curl progress line 93 | terminal.ClearLine() 94 | 95 | // validate download if sha is present 96 | if r.SHA != nil { 97 | if err := r.SHA.validateDownload(d.host, r.URL, cacheDownloadingFilename); err != nil { 98 | 99 | // move file to allow subsequent re-download 100 | // error discarded, would not be actioned anyways 101 | _ = d.host.RunQuiet("mv", cacheDownloadingFilename, cacheDownloadingFilename+".invalid") 102 | 103 | return fmt.Errorf("error validating SHA sum for '%s': %w", path.Base(r.URL), err) 104 | } 105 | } 106 | 107 | return d.host.RunQuiet("mv", cacheDownloadingFilename, CacheFilename(r.URL)) 108 | } 109 | 110 | func (d downloader) hasCache(url string) bool { 111 | _, err := os.Stat(CacheFilename(url)) 112 | return err == nil 113 | } 114 | -------------------------------------------------------------------------------- /util/downloader/sha.go: -------------------------------------------------------------------------------- 1 | package downloader 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/abiosoft/colima/util" 10 | ) 11 | 12 | // SHA is the shasum of a file. 13 | type SHA struct { 14 | Digest string // shasum 15 | URL string // url to download the shasum file (if Digest is empty) 16 | Size int // one of 256 or 512 17 | } 18 | 19 | // ValidateFile validates the SHA of the file. 20 | func (s SHA) ValidateFile(host hostActions, file string) error { 21 | dir, filename := filepath.Split(file) 22 | digest := strings.TrimPrefix(s.Digest, fmt.Sprintf("sha%d:", s.Size)) 23 | shasumBinary := "shasum" 24 | if util.MacOS() { 25 | shasumBinary = "/usr/bin/shasum" 26 | } 27 | 28 | script := strings.NewReplacer( 29 | "{dir}", dir, 30 | "{digest}", digest, 31 | "{size}", strconv.Itoa(s.Size), 32 | "{filename}", filename, 33 | "{shasum_bin}", shasumBinary, 34 | ).Replace( 35 | `cd {dir} && echo "{digest} {filename}" | {shasum_bin} -a {size} --check --status`, 36 | ) 37 | 38 | return host.Run("sh", "-c", script) 39 | } 40 | 41 | func (s SHA) validateDownload(host hostActions, url string, filename string) error { 42 | if s.URL == "" && s.Digest == "" { 43 | return fmt.Errorf("error validating SHA: one of Digest or URL must be set") 44 | } 45 | 46 | // fetch digest from URL if empty 47 | if s.Digest == "" { 48 | // retrieve the filename from the download url. 49 | filename := func() string { 50 | if url == "" { 51 | return "" 52 | } 53 | split := strings.Split(url, "/") 54 | return split[len(split)-1] 55 | }() 56 | 57 | digest, err := fetchSHAFromURL(host, s.URL, filename) 58 | if err != nil { 59 | return err 60 | } 61 | s.Digest = digest 62 | } 63 | 64 | return s.ValidateFile(host, filename) 65 | } 66 | 67 | func fetchSHAFromURL(host hostActions, url, filename string) (string, error) { 68 | script := strings.NewReplacer( 69 | "{url}", url, 70 | "{filename}", filename, 71 | ).Replace( 72 | "curl -sL {url} | grep ' {filename}$' | awk -F' ' '{print $1}'", 73 | ) 74 | sha, err := host.RunOutput("sh", "-c", script) 75 | if err != nil { 76 | return "", fmt.Errorf("error retrieving sha from url '%s': %w", url, err) 77 | } 78 | return strings.TrimSpace(sha), nil 79 | } 80 | -------------------------------------------------------------------------------- /util/fsutil/fs.go: -------------------------------------------------------------------------------- 1 | package fsutil 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "testing/fstest" 7 | ) 8 | 9 | // FS is the host filesystem implementation. 10 | var FS FileSystem = DefaultFS{} 11 | 12 | // MkdirAll calls FS.MakedirAll 13 | func MkdirAll(path string, perm os.FileMode) error { return FS.MkdirAll(path, perm) } 14 | 15 | // Open calls FS.Open 16 | func Open(name string) (fs.File, error) { return FS.Open(name) } 17 | 18 | // FS is abstraction for filesystem. 19 | type FileSystem interface { 20 | MkdirAll(path string, perm os.FileMode) error 21 | fs.FS 22 | } 23 | 24 | var _ FileSystem = DefaultFS{} 25 | var _ FileSystem = fakeFS{} 26 | 27 | // DefaultFS is the default OS implementation of FileSystem. 28 | type DefaultFS struct{} 29 | 30 | // Open implements FS 31 | func (DefaultFS) Open(name string) (fs.File, error) { return os.Open(name) } 32 | 33 | // MkdirAll implements FS 34 | func (DefaultFS) MkdirAll(path string, perm fs.FileMode) error { return os.MkdirAll(path, perm) } 35 | 36 | // FakeFS is a mock FS. The following can be done in a test before usage. 37 | // osutil.FS = osutil.FakeFS 38 | var FakeFS FileSystem = fakeFS{} 39 | 40 | type fakeFS struct{} 41 | 42 | // Open implements FileSystem 43 | func (fakeFS) Open(name string) (fs.File, error) { 44 | return fstest.MapFS{name: &fstest.MapFile{ 45 | Data: []byte("fake file - " + name), 46 | }}.Open(name) 47 | } 48 | 49 | // MkdirAll implements FileSystem 50 | func (fakeFS) MkdirAll(path string, perm fs.FileMode) error { return nil } 51 | -------------------------------------------------------------------------------- /util/macos.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "os/exec" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/abiosoft/colima/cli" 12 | "github.com/coreos/go-semver/semver" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // MacOS returns if the current OS is macOS. 17 | func MacOS() bool { 18 | return runtime.GOOS == "darwin" 19 | } 20 | 21 | // MacOS13OrNewer returns if the current OS is macOS 13 or newer. 22 | func MacOS13OrNewerOnArm() bool { 23 | return runtime.GOARCH == "arm64" && MacOS13OrNewer() 24 | } 25 | 26 | // MacOS13OrNewer returns if the current OS is macOS 13 or newer. 27 | func MacOS13OrNewer() bool { return minMacOSVersion("13.0.0") } 28 | 29 | // MacOS15OrNewer returns if the current OS is macOS 15 or newer. 30 | func MacOS15OrNewer() bool { return minMacOSVersion("15.0.0") } 31 | 32 | // MacOSNestedVirtualizationSupported returns if the current device supports nested virtualization. 33 | func MacOSNestedVirtualizationSupported() bool { 34 | return (IsMx(3) || IsMx(4)) && MacOS15OrNewer() 35 | } 36 | 37 | func minMacOSVersion(version string) bool { 38 | if !MacOS() { 39 | return false 40 | } 41 | ver, err := macOSProductVersion() 42 | if err != nil { 43 | logrus.Warnln(fmt.Errorf("error retrieving macOS version: %w", err)) 44 | return false 45 | } 46 | 47 | cver, err := semver.NewVersion(version) 48 | if err != nil { 49 | logrus.Warnln(fmt.Errorf("error parsing version: %w", err)) 50 | return false 51 | } 52 | 53 | return cver.Compare(*ver) <= 0 54 | } 55 | 56 | // IsMx returns if the current device is an Apple Silicon Mx device 57 | // where x is the number e.g. x = 1 --> m1, x = 3 --> m3 e.t.c. 58 | func IsMx(x int) bool { 59 | var resp struct { 60 | SPHardwareDataType []struct { 61 | ChipType string `json:"chip_type"` 62 | } `json:"SPHardwareDataType"` 63 | } 64 | 65 | var buf bytes.Buffer 66 | cmd := cli.Command("system_profiler", "-json", "SPHardwareDataType") 67 | cmd.Stdout = &buf 68 | 69 | if err := cmd.Run(); err != nil { 70 | logrus.Trace(fmt.Errorf("error retriving chip version: %w", err)) 71 | return false 72 | } 73 | 74 | if err := json.NewDecoder(&buf).Decode(&resp); err != nil { 75 | logrus.Trace(fmt.Errorf("error decoding system_profiler response: %w", err)) 76 | return false 77 | } 78 | 79 | if len(resp.SPHardwareDataType) == 0 { 80 | return false 81 | } 82 | 83 | chipType := strings.ToUpper(resp.SPHardwareDataType[0].ChipType) 84 | return strings.Contains(chipType, fmt.Sprintf("M%d", x)) 85 | } 86 | 87 | // RosettaRunning checks if Rosetta process is running. 88 | func RosettaRunning() bool { 89 | if !MacOS() { 90 | return false 91 | } 92 | cmd := cli.Command("pgrep", "oahd") 93 | cmd.Stderr = nil 94 | cmd.Stdout = nil 95 | return cmd.Run() == nil 96 | } 97 | 98 | // macOSProductVersion returns the host's macOS version. 99 | func macOSProductVersion() (*semver.Version, error) { 100 | cmd := exec.Command("sw_vers", "-productVersion") 101 | // output is like "12.3.1\n" 102 | b, err := cmd.Output() 103 | if err != nil { 104 | return nil, fmt.Errorf("failed to execute %v: %w", cmd.Args, err) 105 | } 106 | verTrimmed := strings.TrimSpace(string(b)) 107 | // macOS 12.4 returns just "12.4\n" 108 | for strings.Count(verTrimmed, ".") < 2 { 109 | verTrimmed += ".0" 110 | } 111 | verSem, err := semver.NewVersion(verTrimmed) 112 | if err != nil { 113 | return nil, fmt.Errorf("failed to parse macOS version %q: %w", verTrimmed, err) 114 | } 115 | return verSem, nil 116 | } 117 | -------------------------------------------------------------------------------- /util/osutil/os.go: -------------------------------------------------------------------------------- 1 | package osutil 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // EnvVar is environment variable 15 | type EnvVar string 16 | 17 | // Exists checks if the environment variable has been set. 18 | func (e EnvVar) Exists() bool { 19 | _, ok := os.LookupEnv(string(e)) 20 | return ok 21 | } 22 | 23 | // Bool returns the environment variable value as boolean. 24 | func (e EnvVar) Bool() bool { 25 | ok, _ := strconv.ParseBool(e.Val()) 26 | return ok 27 | } 28 | 29 | // Bool returns the environment variable value. 30 | func (e EnvVar) Val() string { 31 | return os.Getenv(string(e)) 32 | } 33 | 34 | // Or returns the environment variable value if set, otherwise returns val. 35 | func (e EnvVar) ValOr(val string) string { 36 | if v := os.Getenv(string(e)); v != "" { 37 | return v 38 | } 39 | return val 40 | } 41 | 42 | const EnvColimaBinary = "COLIMA_BINARY" 43 | 44 | // Executable returns the path name for the executable that started 45 | // the current process. 46 | func Executable() string { 47 | e, err := func(s string) (string, error) { 48 | // prioritize env var in case this is a nested process 49 | if e := os.Getenv(EnvColimaBinary); e != "" { 50 | return e, nil 51 | } 52 | 53 | if filepath.IsAbs(s) { 54 | return s, nil 55 | } 56 | 57 | e, err := exec.LookPath(s) 58 | if err != nil { 59 | return "", fmt.Errorf("error looking up '%s' in PATH: %w", s, err) 60 | } 61 | 62 | abs, err := filepath.Abs(e) 63 | if err != nil { 64 | return "", fmt.Errorf("error computing absolute path of '%s': %w", e, err) 65 | } 66 | 67 | return abs, nil 68 | }(os.Args[0]) 69 | 70 | if err != nil { 71 | // this should never happen, thereby it is safe to do 72 | logrus.Traceln(fmt.Errorf("cannot detect current running executable: %w", err)) 73 | logrus.Traceln("falling back to first CLI argument") 74 | return os.Args[0] 75 | } 76 | 77 | return e 78 | } 79 | 80 | // Socket is a unix socket 81 | type Socket string 82 | 83 | // Unix returns the unix address for the socket. 84 | func (s Socket) Unix() string { return "unix://" + s.File() } 85 | 86 | // File returns the file path for the socket. 87 | func (s Socket) File() string { return strings.TrimPrefix(string(s), "unix://") } 88 | -------------------------------------------------------------------------------- /util/qemu.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | ) 7 | 8 | // AssertQemuImg checks if qemu-img is available. 9 | func AssertQemuImg() error { 10 | cmd := "qemu-img" 11 | if _, err := exec.LookPath(cmd); err != nil { 12 | return fmt.Errorf("%s not found, run 'brew install %s' to install", cmd, "qemu") 13 | } 14 | 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /util/shautil/sha.go: -------------------------------------------------------------------------------- 1 | package shautil 2 | 3 | import ( 4 | "crypto/sha1" 5 | "crypto/sha256" 6 | "fmt" 7 | ) 8 | 9 | // SHA is a sha computation 10 | type SHA interface { 11 | String() string 12 | Bytes() []byte 13 | } 14 | 15 | type s1 [20]byte 16 | 17 | func (s s1) String() string { return fmt.Sprintf("%x", s[:]) } 18 | func (s s1) Bytes() []byte { return s[:] } 19 | 20 | type s256 [32]byte 21 | 22 | func (s s256) String() string { return fmt.Sprintf("%x", s[:]) } 23 | func (s s256) Bytes() []byte { return s[:] } 24 | 25 | // SHA256Hash computes a sha256sum of a string. 26 | func SHA256(s string) SHA { 27 | return s256(sha256.Sum256([]byte(s))) 28 | } 29 | 30 | // SHA256Hash computes a sha256sum of a string. 31 | func SHA1(s string) SHA { 32 | return s1(sha1.Sum([]byte(s))) 33 | } 34 | -------------------------------------------------------------------------------- /util/template.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "text/template" 8 | ) 9 | 10 | // WriteTemplate writes template with body to file after applying values. 11 | func WriteTemplate(body string, file string, values interface{}) error { 12 | b, err := ParseTemplate(body, values) 13 | if err != nil { 14 | return err 15 | } 16 | return os.WriteFile(file, b, 0644) 17 | } 18 | 19 | // ParseTemplate parses template with body and values and returns the resulting bytes. 20 | func ParseTemplate(body string, values interface{}) ([]byte, error) { 21 | t, err := template.New("").Parse(body) 22 | if err != nil { 23 | return nil, fmt.Errorf("error parsing template: %w", err) 24 | } 25 | 26 | var b bytes.Buffer 27 | if err := t.Execute(&b, values); err != nil { 28 | return nil, fmt.Errorf("error executing template: %w", err) 29 | } 30 | 31 | return b.Bytes(), err 32 | } 33 | -------------------------------------------------------------------------------- /util/terminal/output.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/fatih/color" 13 | "golang.org/x/term" 14 | ) 15 | 16 | var _ io.WriteCloser = (*verboseWriter)(nil) 17 | 18 | type verboseWriter struct { 19 | buf bytes.Buffer 20 | lines []string 21 | 22 | lineHeight int 23 | termWidth int 24 | overflow int 25 | 26 | lastUpdate time.Time 27 | } 28 | 29 | // NewVerboseWriter creates a new verbose writer. 30 | // A verbose writer pipes the input received to the stdout while tailing the specified lines. 31 | // Calling `Close` when done is recommended to clear the last uncleared output. 32 | func NewVerboseWriter(lineHeight int) io.WriteCloser { 33 | return &verboseWriter{lineHeight: lineHeight} 34 | } 35 | 36 | func (v *verboseWriter) Write(p []byte) (n int, err error) { 37 | // if it's not a terminal, simply write to stdout 38 | if !isTerminal { 39 | return os.Stdout.Write(p) 40 | } 41 | 42 | for i, c := range p { 43 | if c != '\n' { 44 | v.buf.WriteByte(c) 45 | continue 46 | } 47 | 48 | if err := v.refresh(); err != nil { 49 | return i + 1, err 50 | } 51 | 52 | } 53 | return len(p), nil 54 | } 55 | 56 | func (v *verboseWriter) printLineVerbose() { 57 | line := v.sanitizeLine(v.buf.String()) 58 | line = color.HiBlackString(line) 59 | _, _ = fmt.Fprintln(os.Stderr, line) 60 | } 61 | 62 | func (v *verboseWriter) refresh() error { 63 | v.clearScreen() 64 | v.addLine() 65 | return v.printScreen() 66 | } 67 | 68 | func (v *verboseWriter) addLine() { 69 | defer v.buf.Reset() 70 | 71 | // if height <=0, do not scroll 72 | if v.lineHeight <= 0 { 73 | v.printLineVerbose() 74 | return 75 | } 76 | 77 | if len(v.lines) >= v.lineHeight { 78 | v.lines = v.lines[1:] 79 | } 80 | v.lines = append(v.lines, v.buf.String()) 81 | } 82 | 83 | func (v *verboseWriter) Close() error { 84 | if v.buf.Len() > 0 { 85 | if err := v.refresh(); err != nil { 86 | return err 87 | } 88 | } 89 | 90 | v.clearScreen() 91 | return nil 92 | } 93 | 94 | func (v verboseWriter) sanitizeLine(line string) string { 95 | // remove logrus noises 96 | if strings.HasPrefix(line, "time=") && strings.Contains(line, "msg=") { 97 | line = line[strings.Index(line, "msg=")+4:] 98 | if l, err := strconv.Unquote(line); err == nil { 99 | line = l 100 | } 101 | } 102 | 103 | return "> " + line 104 | } 105 | 106 | func (v *verboseWriter) printScreen() error { 107 | if err := v.updateTerm(); err != nil { 108 | return err 109 | } 110 | 111 | v.overflow = 0 112 | for _, line := range v.lines { 113 | line = v.sanitizeLine(line) 114 | if len(line) > v.termWidth { 115 | v.overflow += len(line) / v.termWidth 116 | if len(line)%v.termWidth == 0 { 117 | v.overflow -= 1 118 | } 119 | } 120 | line = color.HiBlackString(line) 121 | fmt.Println(line) 122 | } 123 | return nil 124 | } 125 | 126 | func (v *verboseWriter) clearScreen() { 127 | for i := 0; i < len(v.lines)+v.overflow; i++ { 128 | ClearLine() 129 | } 130 | } 131 | 132 | func (v *verboseWriter) updateTerm() error { 133 | // no need to refresh so quickly 134 | if time.Since(v.lastUpdate) < time.Second*2 { 135 | return nil 136 | } 137 | v.lastUpdate = time.Now().UTC() 138 | 139 | w, _, err := term.GetSize(int(os.Stdout.Fd())) 140 | if err != nil { 141 | return fmt.Errorf("error getting terminal size: %w", err) 142 | } 143 | // A width of zero would result in a division by zero panic when computing overflow 144 | // in printScreen. Therefore, set it to a safe - even though probably wrong - value. 145 | // We use <= 0 here because negative values are guaranteed to lead to unexpected 146 | // results, even if they don't cause panics. 147 | if w <= 0 { 148 | w = 80 149 | } 150 | v.termWidth = w 151 | 152 | return nil 153 | } 154 | -------------------------------------------------------------------------------- /util/terminal/terminal.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "golang.org/x/term" 8 | ) 9 | 10 | var isTerminal = term.IsTerminal(int(os.Stdout.Fd())) 11 | 12 | // ClearLine clears the previous line of the terminal 13 | func ClearLine() { 14 | if !isTerminal { 15 | return 16 | } 17 | 18 | fmt.Print("\033[1A \033[2K \r") 19 | } 20 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/google/shlex" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // HomeDir returns the user home directory. 15 | func HomeDir() string { 16 | home, err := os.UserHomeDir() 17 | if err != nil { 18 | // this should never happen 19 | logrus.Fatal(fmt.Errorf("error retrieving home directory: %w", err)) 20 | } 21 | return home 22 | } 23 | 24 | // RandomAvailablePort returns an available port on the host machine. 25 | func RandomAvailablePort() int { 26 | listener, err := net.Listen("tcp", ":0") 27 | if err != nil { 28 | logrus.Fatal(fmt.Errorf("error picking an available port: %w", err)) 29 | } 30 | 31 | if err := listener.Close(); err != nil { 32 | logrus.Fatal(fmt.Errorf("error closing temporary port listener: %w", err)) 33 | } 34 | 35 | return listener.Addr().(*net.TCPAddr).Port 36 | } 37 | 38 | // HostIPAddresses returns all IPv4 addresses on the host. 39 | func HostIPAddresses() []net.IP { 40 | var addresses []net.IP 41 | ints, err := net.InterfaceAddrs() 42 | if err != nil { 43 | return nil 44 | } 45 | for i := range ints { 46 | split := strings.Split(ints[i].String(), "/") 47 | addr := net.ParseIP(split[0]).To4() 48 | // ignore default loopback 49 | if addr != nil && addr.String() != "127.0.0.1" { 50 | addresses = append(addresses, addr) 51 | } 52 | } 53 | 54 | return addresses 55 | } 56 | 57 | // ShellSplit splits cmd into arguments using. 58 | func ShellSplit(cmd string) []string { 59 | split, err := shlex.Split(cmd) 60 | if err != nil { 61 | logrus.Warnln("error splitting into args: %w", err) 62 | logrus.Warnln("falling back to whitespace split", err) 63 | split = strings.Fields(cmd) 64 | } 65 | 66 | return split 67 | } 68 | 69 | // CleanPath returns the absolute path to the mount location. 70 | // If location is an empty string, nothing is done. 71 | func CleanPath(location string) (string, error) { 72 | if location == "" { 73 | return "", nil 74 | } 75 | 76 | str := os.ExpandEnv(location) 77 | 78 | if strings.HasPrefix(str, "~") { 79 | str = strings.Replace(str, "~", HomeDir(), 1) 80 | } 81 | 82 | str = filepath.Clean(str) 83 | if !filepath.IsAbs(str) { 84 | return "", fmt.Errorf("relative paths not supported for mount '%s'", location) 85 | } 86 | 87 | return strings.TrimSuffix(str, "/") + "/", nil 88 | } 89 | -------------------------------------------------------------------------------- /util/yamlutil/yaml_test.go: -------------------------------------------------------------------------------- 1 | package yamlutil 2 | 3 | import ( 4 | "net" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/abiosoft/colima/config" 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | func Test_encode_Docker(t *testing.T) { 13 | conf := config.Config{ 14 | Docker: map[string]any{"insecure-registries": []any{"127.0.0.1"}}, 15 | Network: config.Network{DNSResolvers: []net.IP{net.ParseIP("1.1.1.1")}}, 16 | Kubernetes: config.Kubernetes{K3sArgs: []string{"--disable=traefik"}}, 17 | } 18 | 19 | tests := []struct { 20 | name string 21 | args config.Config 22 | want config.Config 23 | wantErr bool 24 | }{ 25 | {name: "nested", args: conf, want: conf}, 26 | } 27 | for _, tt := range tests { 28 | t.Run(tt.name, func(t *testing.T) { 29 | b, err := encodeYAML(tt.args) 30 | var got config.Config 31 | if err := yaml.Unmarshal(b, &got); err != nil { 32 | t.Errorf("resulting byte is not a valid yaml: %v", err) 33 | return 34 | } 35 | 36 | if (err != nil) != tt.wantErr { 37 | t.Errorf("save() error = %v, wantErr %v", err, tt.wantErr) 38 | return 39 | } 40 | if !reflect.DeepEqual(got.Docker, tt.want.Docker) { 41 | t.Errorf("save() = %+v\nwant %+v", got.Docker, tt.want.Docker) 42 | } 43 | }) 44 | } 45 | } 46 | --------------------------------------------------------------------------------