Repository: abiosoft/colima Branch: main Commit: 8cb5d3528716 Files: 143 Total size: 429.4 KB Directory structure: gitextract_xof0cru8/ ├── .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 │ ├── model.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.nix ├── 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/ │ ├── CONTRIBUTE.md │ ├── FAQ.md │ └── INSTALL.md ├── embedded/ │ ├── defaults/ │ │ ├── abort.yaml │ │ ├── colima.yaml │ │ └── template.yaml │ ├── embed.go │ ├── images/ │ │ ├── images.txt │ │ └── images_sha.sh │ ├── k3s/ │ │ └── flannel.json │ ├── network/ │ │ └── sudo.txt │ └── sudoers.go ├── environment/ │ ├── container/ │ │ ├── containerd/ │ │ │ ├── buildkitd.toml │ │ │ ├── config.toml │ │ │ └── containerd.go │ │ ├── docker/ │ │ │ ├── config.toml │ │ │ ├── containerd.go │ │ │ ├── context.go │ │ │ ├── daemon.go │ │ │ ├── docker.go │ │ │ └── proxy.go │ │ ├── incus/ │ │ │ ├── config.yaml │ │ │ ├── incus.go │ │ │ └── route.go │ │ └── kubernetes/ │ │ ├── cni.go │ │ ├── k3s.go │ │ ├── kubeconfig.go │ │ └── kubernetes.go │ ├── container.go │ ├── environment.go │ ├── guest/ │ │ └── systemctl/ │ │ ├── systemctl.go │ │ └── systemctl_test.go │ ├── host/ │ │ └── host.go │ ├── host.go │ ├── vm/ │ │ └── lima/ │ │ ├── certs.go │ │ ├── config.go │ │ ├── daemon.go │ │ ├── disk.go │ │ ├── disk.sh │ │ ├── dns.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 │ └── vm.go ├── flake.nix ├── go.mod ├── go.sum ├── integration/ │ └── Dockerfile ├── model/ │ ├── docker.go │ ├── ramalama.go │ ├── runner.go │ └── runner_test.go ├── scripts/ │ ├── build_vmnet.sh │ └── integration.sh ├── shell.nix ├── store/ │ └── store.go └── util/ ├── debutil/ │ └── debutil.go ├── downloader/ │ ├── curl.go │ ├── download.go │ ├── errors.go │ ├── http.go │ ├── native.go │ └── sha.go ├── fsutil/ │ └── fs.go ├── macos.go ├── macos_test.go ├── osutil/ │ └── os.go ├── qemu.go ├── shautil/ │ └── sha.go ├── template.go ├── terminal/ │ ├── output.go │ └── terminal.go ├── util.go └── yamlutil/ ├── yaml.go └── yaml_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ [*.go] indent_style = tab ================================================ FILE: .github/FUNDING.yml ================================================ github: abiosoft custom: - "https://buymeacoffee.com/abiosoft" patreon: colima ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ name: Bug report description: Report a bug or issue body: - type: textarea attributes: label: Description description: A clear and concise description of what the issue is. - type: textarea attributes: label: Version description: Please show the output of `colima version && limactl --version && qemu-img --version`. - type: checkboxes attributes: label: Operating System description: Which Operating System/Architecture does this issue happen on? Check all that apply. options: - label: macOS Intel <= 13 (Ventura) required: false - label: macOS Intel >= 14 (Sonoma) required: false - label: Apple Silicon <= 13 (Ventura) required: false - label: Apple Silicon >= 14 (Sonoma) required: false - label: Linux required: false - type: textarea attributes: label: Output of `colima status` description: The output of `colima status` or `colima status -p ` tells us what vm-type and mount type, etc. value: - type: textarea attributes: label: Reproduction Steps description: Kindly walk us through the steps to reproduce this behaviour. value: | 1. 2. 3. - type: textarea attributes: label: Expected behaviour description: A clear and concise description of what you expected to happen. - type: textarea attributes: label: Additional context description: Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yaml ================================================ name: Feature request description: Request a missing feature body: - type: textarea attributes: label: Description ================================================ FILE: .github/dependabot.yaml ================================================ version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" open-pull-requests-limit: 10 - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/go.yml ================================================ name: Go on: push: tags: ["v*"] paths-ignore: - "**/*.md" - "**/*.nix" - "**/*.lock" pull_request: branches: [main] paths-ignore: - "**/*.md" - "**/*.nix" - "**/*.lock" permissions: write-all jobs: build-linux: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.26.1" - name: Build run: go build -v ./... - name: Test run: go test -v ./... build-macos: runs-on: macos-15-intel steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.26.1" - name: Build run: go build -v ./... - name: Test run: go test -v ./... binaries-linux: needs: "build-linux" runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.26.1" - name: install gcc-aarch64-linux-gnu run: | sudo apt-get update sudo apt-get install -y gcc-aarch64-linux-gnu - name: generate binaries run: | OS=Linux ARCH=x86_64 make OS=Linux ARCH=aarch64 make - name: upload artifacts uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: artifacts-linux path: _output/binaries/ binaries-macos: needs: "build-macos" runs-on: macos-15-intel steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.26.1" - name: generate binaries run: | CGO_ENABLED=1 OS=Darwin ARCH=x86_64 make CGO_ENABLED=1 OS=Darwin ARCH=arm64 make - name: upload artifacts uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: artifacts-macos path: _output/binaries/ release: needs: ["binaries-linux", "binaries-macos"] runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: artifacts-linux path: _output/binaries/ - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: artifacts-macos path: _output/binaries/ - name: create release if: github.event_name != 'pull_request' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: > tag="${GITHUB_REF##*/}" gh release create "${tag}" --draft --title "${tag}" _output/binaries/colima-Darwin-x86_64 _output/binaries/colima-Darwin-x86_64.sha256sum _output/binaries/colima-Darwin-arm64 _output/binaries/colima-Darwin-arm64.sha256sum _output/binaries/colima-Linux-x86_64 _output/binaries/colima-Linux-x86_64.sha256sum _output/binaries/colima-Linux-aarch64 _output/binaries/colima-Linux-aarch64.sha256sum ================================================ FILE: .github/workflows/golang-ci.yml ================================================ name: golangci-lint on: push: tags: [v*] branches: [main] paths-ignore: - "**/*.md" - "**/*.nix" - "**/*.lock" pull_request: paths-ignore: - "**/*.md" - "**/*.nix" - "**/*.lock" jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.26.1" - name: golangci-lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: version: v2.11.3 args: --timeout 3m0s ================================================ FILE: .github/workflows/integration.yml ================================================ name: Integration on: push: tags: ["v*"] branches: [main] paths-ignore: - "**/*.md" - "**/*.nix" - "**/*.lock" pull_request: branches: [main] paths-ignore: - "**/*.md" - "**/*.nix" - "**/*.lock" workflow_dispatch: inputs: debug_enabled: description: 'Debug with tmate set "debug_enabled"' required: false default: "false" jobs: kubernetes-docker: runs-on: macos-15-intel steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.26.1" - name: Install CLI deps run: brew install kubectl docker coreutils lima - name: Build and Install run: make && sudo make install - name: tmate debugging session uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23 with: limit-access-to-actor: true github-token: ${{ secrets.GITHUB_TOKEN }} if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} - name: Start Colima run: colima start --runtime docker --kubernetes - name: Delay run: sleep 20 - name: Validate Kubernetes run: kubectl cluster-info && kubectl version && kubectl get nodes -o wide - name: Teardown run: colima delete -f kubernetes-containerd: runs-on: macos-15-intel steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.26.1" - name: Install CLI deps run: brew install kubectl docker coreutils lima - name: Build and Install run: make && sudo make install - name: tmate debugging session uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23 with: limit-access-to-actor: true github-token: ${{ secrets.GITHUB_TOKEN }} if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} - name: Start run: colima start --runtime containerd --kubernetes - name: Delay run: sleep 20 - name: Validate Kubernetes run: kubectl cluster-info && kubectl version && kubectl get nodes -o wide - name: Teardown run: colima delete -f docker: runs-on: macos-15-intel steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.26.1" - name: Install CLI deps run: brew install kubectl docker coreutils lima - name: Build and Install run: make && sudo make install - name: tmate debugging session uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23 with: limit-access-to-actor: true github-token: ${{ secrets.GITHUB_TOKEN }} if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} - name: Start Colima run: colima start --runtime docker - name: Delay run: sleep 10 - name: Validate Docker run: docker ps && docker info - name: Validate DNS run: colima ssh -- sh -c "sudo apt-get update -y -qq && sudo apt-get install -qq dnsutils && nslookup host.docker.internal" - name: Build Image run: docker build integration - name: Run Image arm64 run: docker run --rm --platform=linux/arm64 ghcr.io/linuxcontainers/alpine:latest uname -a - name: Run Image amd64 run: docker run --rm --platform=linux/amd64 ghcr.io/linuxcontainers/alpine:latest uname -a - name: Stop run: colima stop - name: Temp Delete run: colima delete -f - name: Restart run: colima start --runtime docker - name: Assert runtime disk arm64 run: docker run --pull=never --rm --platform=linux/arm64 ghcr.io/linuxcontainers/alpine:latest uname -a - name: Assert runtime disk amd64 run: docker run --pull=never --rm --platform=linux/amd64 ghcr.io/linuxcontainers/alpine:latest uname -a - name: Teardown run: colima delete --data -f containerd: runs-on: macos-15-intel steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.26.1" - name: Install CLI deps run: brew install kubectl docker coreutils lima - name: Build and Install run: make && sudo make install - name: tmate debugging session uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23 with: limit-access-to-actor: true github-token: ${{ secrets.GITHUB_TOKEN }} if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} - name: Start Colima run: colima start --runtime containerd - name: Delay run: sleep 10 - name: Validate Containerd run: colima nerdctl ps && colima nerdctl info - name: Validate DNS run: colima ssh -- sh -c "sudo apt-get update -y -qq && sudo apt-get install -qq dnsutils && nslookup host.docker.internal" - name: Build Image run: colima nerdctl -- build integration - name: Run Image arm64 run: colima nerdctl -- run --rm --platform=linux/arm64 ghcr.io/linuxcontainers/alpine:latest uname -a - name: Run Image amd64 run: colima nerdctl -- run --rm --platform=linux/amd64 ghcr.io/linuxcontainers/alpine:latest uname -a - name: Stop run: colima stop - name: Temp Delete run: colima delete -f - name: Restart run: colima start --runtime containerd - name: Assert runtime disk arm64 run: colima nerdctl -- run --pull=never --rm --platform=linux/arm64 ghcr.io/linuxcontainers/alpine:latest uname -a - name: Assert runtime disk amd64 run: colima nerdctl -- run --pull=never --rm --platform=linux/amd64 ghcr.io/linuxcontainers/alpine:latest uname -a - name: Teardown run: colima delete --data -f incus: runs-on: macos-15-intel steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: "1.26.1" - name: Install CLI deps run: brew install kubectl docker coreutils lima incus - name: Build and Install run: make && sudo make install - name: tmate debugging session uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23 with: limit-access-to-actor: true github-token: ${{ secrets.GITHUB_TOKEN }} if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} - name: Start Colima run: colima start --runtime incus - name: Delay run: sleep 10 - name: Validate Incus run: incus version && incus list - name: Launch Instance run: incus launch images:alpine/edge test-instance - name: Delay for instance run: sleep 5 - name: Validate Instance run: incus exec test-instance -- cat /etc/os-release - name: Validate DNS run: colima ssh -- sh -c "sudo apt-get update -y -qq && sudo apt-get install -qq dnsutils && nslookup host.docker.internal" - name: Stop run: colima stop - name: Temp Delete run: colima delete -f - name: Restart run: colima start --runtime incus - name: Delay for restart run: sleep 10 - name: Assert instance restored run: incus exec test-instance -- cat /etc/os-release - name: Teardown run: colima delete --data -f ================================================ FILE: .gitignore ================================================ .idea/ .fleet/ .vscode/ _output/ _build/ bin/ result ================================================ FILE: .golangci.yml ================================================ version: "2" linters: enable: - gocritic ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Abiola Ibrahim Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ OS ?= $(shell uname) ARCH ?= $(shell uname -m) GOOS ?= $(shell echo "$(OS)" | tr '[:upper:]' '[:lower:]') GOARCH_x86_64 = amd64 GOARCH_aarch64 = arm64 GOARCH_arm64 = arm64 GOARCH ?= $(shell echo "$(GOARCH_$(ARCH))") VERSION := $(shell git describe --tags --always) REVISION := $(shell git rev-parse HEAD) PACKAGE := github.com/abiosoft/colima/config VERSION_VARIABLES := -X $(PACKAGE).appVersion=$(VERSION) -X $(PACKAGE).revision=$(REVISION) OUTPUT_DIR := _output/binaries OUTPUT_BIN := colima-$(OS)-$(ARCH) INSTALL_DIR := /usr/local/bin BIN_NAME := colima LDFLAGS := $(VERSION_VARIABLES) .PHONY: all all: build .PHONY: clean clean: rm -rf _output _build .PHONY: gopath gopath: go get -v ./cmd/colima .PHONY: fmt fmt: go fmt ./... goimports -w . .PHONY: build build: GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags="$(LDFLAGS)" -o $(OUTPUT_DIR)/$(OUTPUT_BIN) ./cmd/colima ifeq ($(GOOS),darwin) codesign -s - $(OUTPUT_DIR)/$(OUTPUT_BIN) endif cd $(OUTPUT_DIR) && openssl sha256 -r -out $(OUTPUT_BIN).sha256sum $(OUTPUT_BIN) .PHONY: test test: go test -v -ldflags="$(LD_FLAGS)" ./... .PHONY: vmnet vmnet: sh scripts/build_vmnet.sh .PHONY: install install: mkdir -p $(INSTALL_DIR) rm -f $(INSTALL_DIR)/$(BIN_NAME) cp $(OUTPUT_DIR)/colima-$(OS)-$(ARCH) $(INSTALL_DIR)/$(BIN_NAME) chmod +x $(INSTALL_DIR)/$(BIN_NAME) .PHONY: lint lint: ## Assumes that golangci-lint is installed and in the path. To install: https://golangci-lint.run/usage/install/ golangci-lint --timeout 3m run .PHONY: print-binary-name print-binary-name: @echo $(OUTPUT_DIR)/$(OUTPUT_BIN) .PHONY: nix-derivation-shell nix-derivation-shell: $(eval DERIVATION=$(shell nix-build)) echo $(DERIVATION) | grep ^/nix nix-shell -p $(DERIVATION) .PHONY: integration integration: build GOARCH=$(GOARCH) COLIMA_BINARY=$(OUTPUT_DIR)/$(OUTPUT_BIN) scripts/integration.sh .PHONY: images-sha images-sha: bash embedded/images/images_sha.sh ================================================ FILE: README.md ================================================ ![colima-logo](colima.png) ## Colima - container runtimes on macOS (and Linux) with minimal setup. [![Go](https://github.com/abiosoft/colima/actions/workflows/go.yml/badge.svg)](https://github.com/abiosoft/colima/actions/workflows/go.yml) [![Integration](https://github.com/abiosoft/colima/actions/workflows/integration.yml/badge.svg)](https://github.com/abiosoft/colima/actions/workflows/integration.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/abiosoft/colima)](https://goreportcard.com/report/github.com/abiosoft/colima) ![Demonstration](colima.gif) **Website & Documentation:** [colima.run](https://colima.run) | [colima.run/docs](https://colima.run/docs/) ## Features Support for Intel and Apple Silicon macOS, and Linux - Simple CLI interface with sensible defaults - Automatic Port Forwarding - Volume mounts - Multiple instances - Support for multiple container runtimes - [Docker](https://docker.com) (with optional Kubernetes) - [Containerd](https://containerd.io) (with optional Kubernetes) - [Incus](https://linuxcontainers.org/incus) (containers and virtual machines) - GPU accelerated containers for AI workloads ## Getting Started ### Installation Colima is available on Homebrew, MacPorts, Nix and [Mise](http://github.com/jdx/mise). Check [here](docs/INSTALL.md) for other installation options. ```sh # Homebrew brew install colima # MacPorts sudo port install colima # Nix nix-env -iA nixpkgs.colima # Mise mise use -g colima@latest ``` Or stay on the bleeding edge (only Homebrew) ``` brew install --HEAD colima ``` ## Usage Start Colima with defaults ``` colima start ``` For more usage options ``` colima --help colima start --help ``` Or use a config file ``` colima start --edit ``` ## Runtimes On initial startup, Colima initiates with a user specified runtime that defaults to Docker. ### Docker Docker client is required for Docker runtime. Installable with `brew install docker`. ``` colima start docker run hello-world docker ps ``` You can use the `docker` client on macOS after `colima start` with no additional setup. ### Containerd `colima start --runtime containerd` starts and setup Containerd. You can use `colima nerdctl` to interact with Containerd using [nerdctl](https://github.com/containerd/nerdctl). ``` colima start --runtime containerd nerdctl run hello-world nerdctl ps ``` It is recommended to run `colima nerdctl install` to install `nerdctl` alias script in $PATH. ### Kubernetes kubectl is required for Kubernetes. Installable with `brew install kubectl`. To enable Kubernetes, start Colima with `--kubernetes` flag. ``` colima start --kubernetes kubectl run caddy --image=caddy kubectl get pods ``` #### Interacting with Image Registry For Docker runtime, images built or pulled with Docker are accessible to Kubernetes. For Containerd runtime, images built or pulled in the `k8s.io` namespace are accessible to Kubernetes. ### Incus **Requires v0.7.0** Incus client is required for Incus runtime. Installable with brew `brew install incus`. `colima start --runtime incus` starts and setup Incus. ``` colima start --runtime incus incus launch images:alpine/edge incus list ``` You can use the `incus` client on macOS after `colima start` with no additional setup. **Note:** Running virtual machines on Incus is only supported on m3 or newer Apple Silicon devices. ### AI Models (GPU Accelerated) **Requires v0.10.0, Apple Silicon and macOS 13+** Colima supports GPU accelerated containers for AI workloads using the `krunkit` VM type. **Note:** To use krunkit with colima, ensure it is installed. ``` brew tap slp/krunkit brew install krunkit ``` Setup and use a model. ``` colima start --runtime docker --vm-type krunkit colima model run gemma3 ``` Colima supports two model runner backends: - **Docker Model Runner** (default) — supports [Docker AI Registry](https://hub.docker.com/u/ai) and [HuggingFace](https://huggingface.co). - **Ramalama** — supports [HuggingFace](https://huggingface.co) and [Ollama](https://ollama.com) registries. The default registry is the Docker AI Registry. Models can be run by name without a prefix: ```sh colima model run gemma3 colima model run llama3.2 # HuggingFace (Docker Model Runner) colima model run hf.co/microsoft/Phi-3-mini-4k-instruct-gguf # Ollama (requires ramalama runner) colima model run ollama://gemma3 --runner ramalama ``` See the [AI Workloads documentation](https://colima.run/docs/ai/) for more details. ### Customizing the VM The default VM created by Colima has 2 CPUs, 2GiB memory and 100GiB storage. The VM can be customized either by passing additional flags to `colima start`. e.g. `--cpu`, `--memory`, `--disk`, `--runtime`. Or by editing the config file with `colima start --edit`. **NOTE**: Disk size can be increased after the VM is created. #### Customization Examples - create VM with 1CPU, 2GiB memory and 10GiB storage. ``` colima start --cpu 1 --memory 2 --disk 10 ``` - modify an existing VM to 4CPUs and 8GiB memory. ``` colima stop colima start --cpu 4 --memory 8 ``` - create VM with Rosetta 2 emulation. Requires v0.5.3 and macOS >= 13 (Ventura) on Apple Silicon. ``` colima start --vm-type=vz --vz-rosetta ``` ## Project Goal To provide container runtimes on macOS with minimal setup. ## What is with the name? Colima means Containers on [Lima](https://github.com/lima-vm/lima). Since Lima is aka Linux Machines. By transitivity, Colima can also mean Containers on Linux Machines. ## And the Logo? The logo was contributed by [Daniel Hodvogner](https://github.com/dhodvogner). Check [this issue](https://github.com/abiosoft/colima/issues/781) for more. ## Troubleshooting and FAQs Check [here](docs/FAQ.md) for Frequently Asked Questions, or visit the [online FAQ](https://colima.run/docs/faq/) for a searchable version. ## How to Contribute? Check [here](docs/CONTRIBUTE.md) for the instructions on contributing to the project. ## Community - [GitHub Discussions](https://github.com/abiosoft/colima/discussions) - [GitHub Issues](https://github.com/abiosoft/colima/issues) - [Announcements](https://colima.run/announcements/) - `#colima` channel in the CNCF Slack - New account: - Login: ## License MIT ## Sponsoring the Project If you (or your company) are benefiting from the project and would like to support the contributors, kindly sponsor. - [Github Sponsors](https://github.com/sponsors/abiosoft) - [Buy me a coffee](https://www.buymeacoffee.com/abiosoft) - [Patreon](https://patreon.com/colima) --- [](https://macstadium.com) ================================================ FILE: SECURITY.md ================================================ Thanks for helping make Colima safe for everyone. ## Security We take the security of Colima seriously. We will ensure that your finding gets passed along to the appropriate maintainers for remediation. ## Reporting Security Issues If you believe you have found a security vulnerability in this repository, please report it to us through coordinated disclosure. **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** Instead, please send an email to git[@]abiosoft.com. Please include as much of the information listed below as you can to help us better understand and resolve the issue: * The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) * Full paths of source file(s) related to the manifestation of the issue * The location of the affected source code (tag/branch/commit or direct URL) * Any special configuration required to reproduce the issue * Step-by-step instructions to reproduce the issue * Proof-of-concept or exploit code (if possible) * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. ================================================ FILE: app/app.go ================================================ package app import ( "bufio" "bytes" "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/config/configmanager" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/container/containerd" "github.com/abiosoft/colima/environment/container/docker" "github.com/abiosoft/colima/environment/container/incus" "github.com/abiosoft/colima/environment/container/kubernetes" "github.com/abiosoft/colima/environment/host" "github.com/abiosoft/colima/environment/vm/lima" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/abiosoft/colima/store" "github.com/abiosoft/colima/util" "github.com/docker/go-units" log "github.com/sirupsen/logrus" ) type App interface { Active() bool Start(config.Config) error Stop(force bool) error Delete(data, force bool) error SSH(args ...string) error Status(extended bool, json bool) error Version() error Runtime() (string, error) Update() error Kubernetes() (environment.Container, error) } var _ App = (*colimaApp)(nil) // New creates a new app. func New() (App, error) { guest := lima.New(host.New()) if err := host.IsInstalled(guest); err != nil { return nil, fmt.Errorf("dependency check failed for VM: %w", err) } return &colimaApp{ guest: guest, }, nil } type colimaApp struct { guest environment.VM } func (c colimaApp) startWithRuntime(conf config.Config) ([]environment.Container, error) { kubernetesEnabled := conf.Kubernetes.Enabled // Kubernetes can only be enabled for docker and containerd switch conf.Runtime { case docker.Name, containerd.Name: default: kubernetesEnabled = false } var containers []environment.Container { runtime := conf.Runtime if kubernetesEnabled { runtime += "+k3s" } log.Println("runtime:", runtime) } // runtime { env, err := c.containerEnvironment(conf.Runtime) if err != nil { return nil, err } containers = append(containers, env) } // kubernetes should come after required runtime if kubernetesEnabled { env, err := c.containerEnvironment(kubernetes.Name) if err != nil { return nil, err } containers = append(containers, env) } return containers, nil } func (c colimaApp) Start(conf config.Config) error { ctx := context.WithValue(context.Background(), config.CtxKey(), conf) log.Println("starting", config.CurrentProfile().DisplayName) // print the full path of current profile being used log.Tracef("starting with config file: %s\n", config.CurrentProfile().File()) var containers []environment.Container if !environment.IsNoneRuntime(conf.Runtime) { cs, err := c.startWithRuntime(conf) if err != nil { return err } containers = cs } // the order for start is: // vm start -> container runtime provision -> container runtime start // start vm if err := c.guest.Start(ctx, conf); err != nil { return fmt.Errorf("error starting vm: %w", err) } // run after-boot provision scripts c.runProvisionScripts(conf, config.ProvisionModeAfterBoot) // provision and start container runtimes for _, cont := range containers { log := log.WithField("context", cont.Name()) log.Println("provisioning ...") if err := cont.Provision(ctx); err != nil { return fmt.Errorf("error provisioning %s: %w", cont.Name(), err) } log.Println("starting ...") if err := cont.Start(ctx); err != nil { return fmt.Errorf("error starting %s: %w", cont.Name(), err) } } // run ready provision scripts c.runProvisionScripts(conf, config.ProvisionModeReady) // persist the current runtime if err := c.setRuntime(conf.Runtime); err != nil { log.Error(fmt.Errorf("error persisting runtime settings: %w", err)) } // persist the kubernetes config if err := c.setKubernetes(conf.Kubernetes); err != nil { log.Error(fmt.Errorf("error persisting kubernetes settings: %w", err)) } log.Println("done") if err := generateSSHConfig(conf.SSHConfig); err != nil { log.Trace("error generating ssh_config: %w", err) } return nil } func (c colimaApp) runProvisionScripts(conf config.Config, mode string) { var failed bool for _, s := range conf.Provision { if s.Mode != mode { continue } if err := c.guest.Run("sh", "-c", s.Script); err != nil { failed = true } } if failed { log.Warnln(fmt.Errorf("error running %s provision script(s)", mode)) } } func (c colimaApp) Stop(force bool) error { ctx := context.Background() log.Println("stopping", config.CurrentProfile().DisplayName) // the order for stop is: // container stop -> vm stop // stop container runtimes if c.guest.Running(ctx) { containers, err := c.currentContainerEnvironments(ctx) if err != nil { log.Warnln(fmt.Errorf("error retrieving runtimes: %w", err)) } // stop happens in reverse of start for i := len(containers) - 1; i >= 0; i-- { cont := containers[i] log := log.WithField("context", cont.Name()) log.Println("stopping ...") if err := cont.Stop(ctx, force); err != nil { // failure to stop a container runtime is not fatal // it is only meant for graceful shutdown. // the VM will shut down anyways. log.Warnln(fmt.Errorf("error stopping %s: %w", cont.Name(), err)) } } } // stop vm // no need to check running status, it may be in a state that requires stopping. if err := c.guest.Stop(ctx, force); err != nil { return fmt.Errorf("error stopping vm: %w", err) } log.Println("done") if err := generateSSHConfig(false); err != nil { log.Trace("error generating ssh_config: %w", err) } return nil } func (c colimaApp) Delete(data, force bool) error { confirmContainerDestruction := func() bool { return cli.Prompt("\033[31m\033[1mthis will delete ALL container data. Are you sure you want to continue") } s, _ := store.Load() diskInUse := s.DiskFormatted if !force { y := cli.Prompt("are you sure you want to delete " + config.CurrentProfile().DisplayName + " and all settings") if !y { return nil } // runtime disk not in use or data deletion is requested, // deletion deletes all data, warn accordingly. if !diskInUse || data { if y := confirmContainerDestruction(); !y { return nil } } } ctx := context.Background() log.Println("deleting", config.CurrentProfile().DisplayName) // the order for teardown is: // container teardown -> vm teardown // vm teardown would've sufficed but container provision // may have created configurations on the host. // it is thereby necessary to teardown containers as well. // teardown container runtimes if c.guest.Running(ctx) { containers, err := c.currentContainerEnvironments(ctx) if err != nil { log.Warnln(fmt.Errorf("error retrieving runtimes: %w", err)) } for _, cont := range containers { log := log.WithField("context", cont.Name()) log.Println("deleting ...") if err := cont.Teardown(ctx); err != nil { // failure here is not fatal log.Warnln(fmt.Errorf("error during teardown of %s: %w", cont.Name(), err)) } } } // teardown vm if err := c.guest.Teardown(ctx); err != nil { return fmt.Errorf("error during teardown of vm: %w", err) } // delete configs if err := configmanager.Teardown(); err != nil { return fmt.Errorf("error deleting configs: %w", err) } // delete runtime disk if disk in use and data deletion is requested if diskInUse && data { log.Println("deleting container data") if err := limautil.DeleteDisk(); err != nil { return fmt.Errorf("error deleting container data: %w", err) } if err := store.Reset(); err != nil { log.Trace("error resetting store: %w", err) } } log.Println("done") if err := generateSSHConfig(false); err != nil { log.Trace("error generating ssh_config: %w", err) } return nil } func (c colimaApp) SSH(args ...string) error { ctx := context.Background() if !c.guest.Running(ctx) { return fmt.Errorf("%s not running", config.CurrentProfile().DisplayName) } workDir, err := os.Getwd() if err != nil { return fmt.Errorf("error retrieving current working directory: %w", err) } // peek the current directory to see if it is mounted to prevent `cd` errors // with limactl ssh if err := func() error { conf, err := configmanager.LoadInstance() if err != nil { return err } pwd, err := util.CleanPath(workDir) if err != nil { return err } for _, m := range conf.MountsOrDefault() { location := m.MountPoint if location == "" { location = m.Location } location, err := util.CleanPath(location) if err != nil { log.Trace(err) continue } if strings.HasPrefix(pwd, location) { return nil } } return fmt.Errorf("not a mounted directory: %s", workDir) }(); err != nil { // the errors returned here is not critical and thereby silenced. // the goal is to prevent unnecessary warning message from Lima. log.Trace(fmt.Errorf("error checking if PWD is mounted: %w", err)) workDir = "" } guest := lima.New(host.New()) return guest.SSH(workDir, args...) } type statusInfo struct { DisplayName string `json:"display_name"` Driver string `json:"driver"` Arch string `json:"arch"` Runtime string `json:"runtime"` MountType string `json:"mount_type"` IPAddress string `json:"ip_address,omitempty"` DockerSocket string `json:"docker_socket,omitempty"` ContainerdSocket string `json:"containerd_socket,omitempty"` BuildkitdSocket string `json:"buildkitd_socket,omitempty"` IncusSocket string `json:"incus_socket,omitempty"` Kubernetes bool `json:"kubernetes"` CPU int `json:"cpu"` Memory int64 `json:"memory"` Disk int64 `json:"disk"` } func (c colimaApp) getStatus() (status statusInfo, err error) { ctx := context.Background() if !c.guest.Running(ctx) { return status, fmt.Errorf("%s is not running", config.CurrentProfile().DisplayName) } currentRuntime, err := c.currentRuntime(ctx) if err != nil { return status, err } status.DisplayName = config.CurrentProfile().DisplayName status.Driver = "QEMU" conf, _ := configmanager.LoadInstance() if !conf.Empty() { status.Driver = conf.DriverLabel() } status.Arch = string(c.guest.Arch()) status.Runtime = currentRuntime status.MountType = conf.MountType ipAddress := limautil.IPAddress(config.CurrentProfile().ID) if ipAddress != "127.0.0.1" { status.IPAddress = ipAddress } if currentRuntime == docker.Name { status.DockerSocket = "unix://" + docker.HostSocketFile() status.ContainerdSocket = "unix://" + containerd.HostSocketFiles().Containerd } if currentRuntime == containerd.Name { status.ContainerdSocket = "unix://" + containerd.HostSocketFiles().Containerd status.BuildkitdSocket = "unix://" + containerd.HostSocketFiles().Buildkitd } if currentRuntime == incus.Name { status.IncusSocket = "unix://" + incus.HostSocketFile() } if k, err := c.Kubernetes(); err == nil && k.Running(ctx) { status.Kubernetes = true } if inst, err := limautil.Instance(); err == nil { status.CPU = inst.CPU status.Memory = inst.Memory status.Disk = inst.Disk } return status, nil } func (c colimaApp) Status(extended bool, jsonOutput bool) error { status, err := c.getStatus() if err != nil { return err } if jsonOutput { if err := json.NewEncoder(os.Stdout).Encode(status); err != nil { return fmt.Errorf("error encoding status as json: %w", err) } } else { log.Println(config.CurrentProfile().DisplayName, "is running using", status.Driver) log.Println("arch:", status.Arch) log.Println("runtime:", status.Runtime) if status.MountType != "" { log.Println("mountType:", status.MountType) } // ip address if status.IPAddress != "" { log.Println("address:", status.IPAddress) } // docker socket if status.DockerSocket != "" { log.Println("docker socket:", status.DockerSocket) } if status.ContainerdSocket != "" { log.Println("containerd socket:", status.ContainerdSocket) } if status.BuildkitdSocket != "" { log.Println("buildkitd socket:", status.BuildkitdSocket) } if status.IncusSocket != "" { log.Println("incus socket:", status.IncusSocket) } // kubernetes if status.Kubernetes { log.Println("kubernetes: enabled") } // additional details if extended { if status.CPU > 0 { log.Println("cpu:", status.CPU) } if status.Memory > 0 { log.Println("mem:", units.BytesSize(float64(status.Memory))) } if status.Disk > 0 { log.Println("disk:", units.BytesSize(float64(status.Disk))) } } } return nil } func (c colimaApp) Version() error { ctx := context.Background() if !c.guest.Running(ctx) { return nil } containerRuntimes, err := c.currentContainerEnvironments(ctx) if err != nil { return err } var kube environment.Container for _, cont := range containerRuntimes { if cont.Name() == kubernetes.Name { kube = cont continue } fmt.Println() fmt.Println("runtime:", cont.Name()) fmt.Println("arch:", c.guest.Arch()) fmt.Println(cont.Version(ctx)) } if kube != nil && kube.Version(ctx) != "" { fmt.Println() fmt.Println(kubernetes.Name) fmt.Println(kube.Version(ctx)) } return nil } func (c colimaApp) currentRuntime(ctx context.Context) (string, error) { if !c.guest.Running(ctx) { return "", fmt.Errorf("%s is not running", config.CurrentProfile().DisplayName) } r := c.guest.Get(environment.ContainerRuntimeKey) if r == "" { return "", fmt.Errorf("error retrieving current runtime: empty value") } return r, nil } func (c colimaApp) setRuntime(runtime string) error { err := store.Set(func(s *store.Store) { // update runtime if runtime disk is in use if s.DiskFormatted { s.DiskRuntime = runtime } }) if err != nil { log.Traceln(fmt.Errorf("error persisting store: %w", err)) } return c.guest.Set(environment.ContainerRuntimeKey, runtime) } func (c colimaApp) setKubernetes(conf config.Kubernetes) error { b, err := json.Marshal(conf) if err != nil { return err } return c.guest.Set(kubernetes.ConfigKey, string(b)) } func (c colimaApp) currentContainerEnvironments(ctx context.Context) ([]environment.Container, error) { var containers []environment.Container // runtime { runtime, err := c.currentRuntime(ctx) if err != nil { return nil, err } if environment.IsNoneRuntime(runtime) { return nil, nil } env, err := c.containerEnvironment(runtime) if err != nil { return nil, err } containers = append(containers, env) } // detect and add kubernetes if k, err := c.containerEnvironment(kubernetes.Name); err == nil && k.Running(ctx) { containers = append(containers, k) } return containers, nil } func (c colimaApp) containerEnvironment(runtime string) (environment.Container, error) { env, err := environment.NewContainer(runtime, c.guest.Host(), c.guest) if err != nil { return nil, fmt.Errorf("error initiating container runtime: %w", err) } if err := host.IsInstalled(env); err != nil { return nil, fmt.Errorf("dependency check failed for %s: %w", runtime, err) } return env, nil } func (c colimaApp) Runtime() (string, error) { return c.currentRuntime(context.Background()) } func (c colimaApp) Kubernetes() (environment.Container, error) { return c.containerEnvironment(kubernetes.Name) } func (c colimaApp) Active() bool { return c.guest.Running(context.Background()) } func (c *colimaApp) Update() error { ctx := context.Background() if !c.guest.Running(ctx) { return fmt.Errorf("runtime cannot be updated, %s is not running", config.CurrentProfile().DisplayName) } runtime, err := c.currentRuntime(ctx) if err != nil { return err } container, err := c.containerEnvironment(runtime) if err != nil { return err } oldVersion := container.Version(ctx) updated, err := container.Update(ctx) if err != nil { return err } if updated { fmt.Println() fmt.Println("Previous") fmt.Println(oldVersion) fmt.Println() fmt.Println("Current") fmt.Println(container.Version(ctx)) } return nil } func generateSSHConfig(modifySSHConfig bool) error { instances, err := limautil.Instances() if err != nil { return fmt.Errorf("error retrieving instances: %w", err) } var buf bytes.Buffer for _, i := range instances { if !i.Running() { continue } profile := config.ProfileFromName(i.Name) resp, err := limautil.ShowSSH(profile.ID) if err != nil { log.Trace(fmt.Errorf("error retrieving SSH config for '%s': %w", i.Name, err)) continue } fmt.Fprintln(&buf, resp.Output) } sshFileColima := config.SSHConfigFile() if err := os.WriteFile(sshFileColima, buf.Bytes(), 0644); err != nil { return fmt.Errorf("error writing ssh_config file: %w", err) } if !modifySSHConfig { // ~/.ssh/config modification disabled return nil } includeLine := "Include " + sshFileColima sshFileSystem := filepath.Join(util.HomeDir(), ".ssh", "config") // include the SSH config file if not included // if ssh file missing, the only content will be the include if _, err := os.Stat(sshFileSystem); err != nil { if err := os.MkdirAll(filepath.Dir(sshFileSystem), 0700); err != nil { return fmt.Errorf("error creating ssh directory: %w", err) } if err := os.WriteFile(sshFileSystem, []byte(includeLine), 0644); err != nil { return fmt.Errorf("error modifying %s: %w", sshFileSystem, err) } return nil } sshContent, err := os.ReadFile(sshFileSystem) if err != nil { return fmt.Errorf("error reading ssh config: %w", err) } scanner := bufio.NewScanner(bytes.NewReader(sshContent)) for scanner.Scan() { words := strings.Fields(scanner.Text()) // empty line if len(words) == 0 { continue } // comment if strings.HasPrefix(words[0], "#") { continue } // not an include line if len(words) < 2 { continue } if words[0] == "Include" { sshConfig := words[1] sshConfig = strings.Replace(sshConfig, "~/", "$HOME/", 1) sshConfig = os.ExpandEnv(sshConfig) if sshConfig == sshFileColima { // already present return nil } } } // not found, prepend file if err := os.WriteFile(sshFileSystem, []byte(includeLine+"\n\n"+string(sshContent)), 0644); err != nil { return fmt.Errorf("error modifying %s: %w", sshFileSystem, err) } return nil } ================================================ FILE: cli/chain.go ================================================ package cli import ( "context" "fmt" "io" "time" log "github.com/sirupsen/logrus" ) // CtxKeyQuiet is the context key to mute the chain. var CtxKeyQuiet = struct{ key string }{key: "quiet"} // errNonFatal is a non fatal error type errNonFatal struct { err error } // Error implements error func (e errNonFatal) Error() string { return e.err.Error() } // ErrNonFatal creates a non-fatal error for a command chain. // A warning would be printed instead of terminating the chain. func ErrNonFatal(err error) error { return errNonFatal{err} } // New creates a new runner instance. func New(name string) CommandChain { return &namedCommandChain{ name: name, } } type cFunc struct { f func() error s string } // CommandChain is a chain of commands. // commands are executed in order. type CommandChain interface { // Init initiates a new runner using the current instance. Init(ctx context.Context) *ActiveCommandChain // Logger returns the instance logger. Logger(ctx context.Context) *log.Entry } var _ CommandChain = (*namedCommandChain)(nil) type namedCommandChain struct { name string log *log.Entry } func (n *namedCommandChain) Logger(ctx context.Context) *log.Entry { if quiet, _ := ctx.Value(CtxKeyQuiet).(bool); quiet { l := log.New() l.SetOutput(io.Discard) return l.WithContext(ctx) } if n.log == nil { n.log = log.WithField("context", n.name).WithContext(ctx) } return n.log } func (n *namedCommandChain) Init(ctx context.Context) *ActiveCommandChain { return &ActiveCommandChain{ log: n.Logger(ctx), } } // ActiveCommandChain is an active command chain. type ActiveCommandChain struct { funcs []cFunc lastStage string log *log.Entry executing bool } // Logger returns the logger for the command chain. func (a *ActiveCommandChain) Logger() *log.Entry { return a.log } // Add adds a new function to the runner. func (a *ActiveCommandChain) Add(f func() error) { a.funcs = append(a.funcs, cFunc{f: f}) } // Stage sets the current stage of the runner. func (a *ActiveCommandChain) Stage(s string) { if a.executing { a.log.Println(s, "...") return } a.funcs = append(a.funcs, cFunc{s: s}) } // Stagef is like stage with string format. func (a *ActiveCommandChain) Stagef(format string, s ...any) { f := fmt.Sprintf(format, s...) a.Stage(f) } // Exec executes the command chain. // The first errored function terminates the chain and the // error is returned. Otherwise, returns nil. func (a *ActiveCommandChain) Exec() error { a.executing = true defer func() { a.executing = false }() for _, f := range a.funcs { if f.f == nil { if f.s != "" { a.log.Println(f.s, "...") a.lastStage = f.s } continue } // success err := f.f() if err == nil { continue } // warning if _, ok := err.(errNonFatal); ok { if a.lastStage == "" { a.log.Warnln(err) } else { a.log.Warnln(fmt.Errorf("error at '%s': %w", a.lastStage, err)) } continue } // error if a.lastStage == "" { return err } return fmt.Errorf("error at '%s': %w", a.lastStage, err) } return nil } // Retry retries `f` up to `count` times at interval. // If after `count` attempts there is an error, the command chain is terminated with the final error. // retryCount starts from 1. func (a *ActiveCommandChain) Retry(stage string, interval time.Duration, count int, f func(retryCount int) error) { a.Add(func() (err error) { var i int for err = f(i + 1); i < count && err != nil; i, err = i+1, f(i+1) { if stage != "" { a.log.Println(stage, "...") } time.Sleep(interval) } return err }) } ================================================ FILE: cli/command.go ================================================ package cli import ( "fmt" "os" "os/exec" "strconv" log "github.com/sirupsen/logrus" ) var runner commandRunner = &defaultCommandRunner{} // Settings is global cli settings var Settings = struct { // Verbose toggles verbose output for commands. Verbose bool }{} // Command creates a new command. func Command(command string, args ...string) *exec.Cmd { return runner.Command(command, args...) } // CommandInteractive creates a new interactive command. func CommandInteractive(command string, args ...string) *exec.Cmd { return runner.CommandInteractive(command, args...) } type commandRunner interface { Command(command string, args ...string) *exec.Cmd CommandInteractive(command string, args ...string) *exec.Cmd } var _ commandRunner = (*defaultCommandRunner)(nil) type defaultCommandRunner struct{} func (d defaultCommandRunner) Command(command string, args ...string) *exec.Cmd { cmd := exec.Command(command, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr log.Trace("cmd ", quotedArgs(cmd.Args)) return cmd } func (d defaultCommandRunner) CommandInteractive(command string, args ...string) *exec.Cmd { cmd := exec.Command(command, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr log.Trace("cmd int ", quotedArgs(cmd.Args)) return cmd } func quotedArgs(args []string) string { var q []string for _, s := range args { q = append(q, strconv.Quote(s)) } return fmt.Sprintf("%v", q) } // Prompt prompts for input with a question. It returns true only if answer is y or Y. func Prompt(question string) bool { fmt.Print(question) fmt.Print("? [y/N] ") fmt.Print("\033[0m") // reset all formatting modes (if any) used by the question string var answer string _, _ = fmt.Scanln(&answer) if answer == "" { return false } return answer[0] == 'Y' || answer[0] == 'y' } ================================================ FILE: cmd/clone.go ================================================ package cmd import ( "fmt" "os" "path/filepath" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/cmd/root" "github.com/abiosoft/colima/config" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) // stopCmd represents the stop command var cloneCmd = &cobra.Command{ Use: "clone ", Short: "clone Colima profile", Long: `Clone the Colima profile.`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { from := config.ProfileFromName(args[0]) to := config.ProfileFromName(args[1]) logrus.Infof("preparing to clone %s...", from.DisplayName) { // verify source profile exists if stat, err := os.Stat(from.LimaInstanceDir()); err != nil || !stat.IsDir() { return fmt.Errorf("colima profile '%s' does not exist", from.ShortName) } // verify destination profile does not exists if stat, err := os.Stat(to.LimaInstanceDir()); err == nil && stat.IsDir() { return fmt.Errorf("colima profile '%s' already exists, delete with `colima delete %s` and try again", to.ShortName, to.ShortName) } // copy source to destination logrus.Info("cloning virtual machine...") if err := cli.Command("mkdir", "-p", to.LimaInstanceDir()).Run(); err != nil { return fmt.Errorf("error preparing to copy VM: %w", err) } if err := cli.Command("cp", filepath.Join(from.LimaInstanceDir(), "basedisk"), filepath.Join(from.LimaInstanceDir(), "diffdisk"), filepath.Join(from.LimaInstanceDir(), "cidata.iso"), filepath.Join(from.LimaInstanceDir(), "lima.yaml"), to.LimaInstanceDir(), ).Run(); err != nil { return fmt.Errorf("error copying VM: %w", err) } } { logrus.Info("copying config...") // verify source config exists if _, err := os.Stat(from.LimaInstanceDir()); err != nil { return fmt.Errorf("config missing for colima profile '%s': %w", from.ShortName, err) } // ensure destination config directory if err := cli.Command("mkdir", "-p", filepath.Dir(to.LimaInstanceDir())).Run(); err != nil { return fmt.Errorf("cannot copy config to new profile '%s': %w", to.ShortName, err) } if err := cli.Command("cp", from.LimaInstanceDir(), to.LimaInstanceDir()).Run(); err != nil { return fmt.Errorf("error copying VM config: %w", err) } } logrus.Info("clone successful") logrus.Infof("run `colima start %s` to start the newly cloned profile", to.ShortName) return nil }, } func init() { root.Cmd().AddCommand(cloneCmd) cloneCmd.Hidden = true } ================================================ FILE: cmd/colima/main.go ================================================ package main import ( _ "github.com/abiosoft/colima/cmd" // for other commands _ "github.com/abiosoft/colima/cmd/daemon" // for vmnet daemon _ "github.com/abiosoft/colima/embedded" // for embedded assets "github.com/abiosoft/colima/cmd/root" ) func main() { root.Execute() } ================================================ FILE: cmd/completion.go ================================================ package cmd import ( "os" "github.com/abiosoft/colima/cmd/root" "github.com/spf13/cobra" ) // completionCmd represents the completion command func completionCmd() *cobra.Command { cmd := &cobra.Command{ Use: "completion [bash|zsh|fish|powershell]", Short: "Generate completion script", Long: `To load completions: Bash: $ source <(colima completion bash) # To load completions for each session, execute once: # Linux: $ colima completion bash > /etc/bash_completion.d/colima # macOS: $ colima completion bash > /usr/local/etc/bash_completion.d/colima Zsh: # If shell completion is not already enabled in your environment, # you will need to enable it. You can execute the following once: $ echo "autoload -U compinit; compinit" >> ~/.zshrc # To load completions for each session, execute once: $ colima completion zsh > "${fpath[1]}/_colima" # You will need to start a new shell for this setup to take effect. fish: $ colima completion fish | source # To load completions for each session, execute once: $ colima completion fish > ~/.config/fish/completions/colima.fish PowerShell: PS> colima completion powershell | Out-String | Invoke-Expression # To load completions for every new session, run: PS> colima completion powershell > colima.ps1 # and source this file from your PowerShell profile. `, DisableFlagsInUseLine: true, ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), Run: func(cmd *cobra.Command, args []string) { switch args[0] { case "bash": _ = cmd.Root().GenBashCompletion(os.Stdout) case "zsh": _ = cmd.Root().GenZshCompletion(os.Stdout) case "fish": _ = cmd.Root().GenFishCompletion(os.Stdout, true) case "powershell": _ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) } }, } return cmd } func init() { root.Cmd().AddCommand(completionCmd()) } ================================================ FILE: cmd/daemon/cmd.go ================================================ package daemon import ( "context" "time" "github.com/abiosoft/colima/cmd/root" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/daemon/process" "github.com/abiosoft/colima/daemon/process/inotify" "github.com/abiosoft/colima/daemon/process/vmnet" "github.com/abiosoft/colima/environment/host" "github.com/abiosoft/colima/environment/vm/lima" "github.com/spf13/cobra" ) var daemonCmd = &cobra.Command{ Use: "daemon", Short: "daemon", Long: `runner for background daemons.`, Hidden: true, } var startCmd = &cobra.Command{ Use: "start [profile]", Short: "start daemon", Long: `start the daemon`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { config.SetProfile(args[0]) ctx := cmd.Context() var processes []process.Process if daemonArgs.vmnet.enabled { processes = append(processes, vmnet.New(daemonArgs.vmnet.mode, daemonArgs.vmnet.netInterface)) } if daemonArgs.inotify.enabled { processes = append(processes, inotify.New()) guest := lima.New(host.New()) args := inotify.Args{ GuestActions: guest, Runtime: daemonArgs.inotify.runtime, Dirs: daemonArgs.inotify.dirs, } ctx = context.WithValue(ctx, inotify.CtxKeyArgs(), args) } return start(ctx, processes) }, } var stopCmd = &cobra.Command{ Use: "stop [profile]", Short: "stop daemon", Long: `stop the daemon`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { config.SetProfile(args[0]) // wait for 60 seconds timeout := time.Second * 60 ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() return stop(ctx) }, } var statusCmd = &cobra.Command{ Use: "status", Short: "status of the daemon", Long: `status of the daemon`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { config.SetProfile(args[0]) return status() }, } var daemonArgs struct { vmnet struct { enabled bool mode string netInterface string } inotify struct { enabled bool dirs []string runtime string } verbose bool } func init() { root.Cmd().AddCommand(daemonCmd) daemonCmd.AddCommand(startCmd) daemonCmd.AddCommand(stopCmd) daemonCmd.AddCommand(statusCmd) startCmd.Flags().BoolVar(&daemonArgs.vmnet.enabled, "vmnet", false, "start vmnet") startCmd.Flags().StringVar(&daemonArgs.vmnet.mode, "vmnet-mode", "shared", "vmnet mode (shared, bridged)") startCmd.Flags().StringVar(&daemonArgs.vmnet.netInterface, "vmnet-interface", "en0", "vmnet interface for bridged mode") startCmd.Flags().BoolVar(&daemonArgs.inotify.enabled, "inotify", false, "start inotify") startCmd.Flags().StringSliceVar(&daemonArgs.inotify.dirs, "inotify-dir", nil, "set inotify directories") startCmd.Flags().StringVar(&daemonArgs.inotify.runtime, "inotify-runtime", "docker", "set runtime") } ================================================ FILE: cmd/daemon/daemon.go ================================================ package daemon import ( "context" "fmt" "os" "os/signal" "path/filepath" "strconv" "sync" "syscall" "time" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/daemon/process" "github.com/abiosoft/colima/util/fsutil" godaemon "github.com/sevlyar/go-daemon" "github.com/sirupsen/logrus" ) var dir = process.Dir // daemonize creates the daemon and returns if this is a child process func daemonize() (ctx *godaemon.Context, child bool, err error) { dir := dir() if err := fsutil.MkdirAll(dir, 0755); err != nil { return nil, false, fmt.Errorf("cannot make dir: %w", err) } info := Info() ctx = &godaemon.Context{ PidFileName: info.PidFile, PidFilePerm: 0644, LogFileName: info.LogFile, LogFilePerm: 0644, } d, err := ctx.Reborn() if err != nil { return ctx, false, fmt.Errorf("error starting daemon: %w", err) } if d != nil { return ctx, false, nil } logrus.Info("- - - - - - - - - - - - - - -") logrus.Info("daemon started by colima") logrus.Infof("Run `/usr/bin/pkill -F %s` to kill the daemon", info.PidFile) return ctx, true, nil } func start(ctx context.Context, processes []process.Process) error { if status() == nil { logrus.Info("daemon already running, startup ignored") return nil } { ctx, child, err := daemonize() if err != nil { return err } if ctx != nil { defer func() { _ = ctx.Release() }() } if !child { return nil } } ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) defer stop() return RunProcesses(ctx, processes...) } func stop(ctx context.Context) error { if status() != nil { // not running return nil } info := Info() if err := cli.CommandInteractive("/usr/bin/pkill", "-F", info.PidFile).Run(); err != nil { return fmt.Errorf("error sending sigterm to daemon: %w", err) } logrus.Info("waiting for process to terminate") for { alive := status() == nil if !alive { return nil } select { case <-ctx.Done(): return ctx.Err() default: time.Sleep(time.Second * 1) } } } func status() error { info := Info() if _, err := os.Stat(info.PidFile); err != nil { return fmt.Errorf("pid file not found: %w", err) } // check if process is actually running p, err := os.ReadFile(info.PidFile) if err != nil { return fmt.Errorf("error reading pid file: %w", err) } pid, _ := strconv.Atoi(string(p)) if pid == 0 { return fmt.Errorf("invalid pid: %v", string(p)) } process, err := os.FindProcess(pid) if err != nil { return fmt.Errorf("process not found: %v", err) } if err := process.Signal(syscall.Signal(0)); err != nil { return fmt.Errorf("process signal(0) returned error: %w", err) } return nil } const ( pidFileName = "daemon.pid" logFileName = "daemon.log" ) func Info() struct { PidFile string LogFile string } { dir := dir() return struct { PidFile string LogFile string }{ PidFile: filepath.Join(dir, pidFileName), LogFile: filepath.Join(dir, logFileName), } } // Run runs the daemon with background processes. // NOTE: this must be called from the program entrypoint with minimal intermediary logic // due to the creation of the daemon. func RunProcesses(ctx context.Context, processes ...process.Process) error { ctx, stop := context.WithCancel(ctx) defer stop() var wg sync.WaitGroup wg.Add(len(processes)) for _, bg := range processes { go func(bg process.Process) { err := bg.Start(ctx) if err != nil { logrus.Error(fmt.Errorf("error starting %s: %w", bg.Name(), err)) stop() } wg.Done() }(bg) } <-ctx.Done() logrus.Info("terminate signal received") wg.Wait() return ctx.Err() } ================================================ FILE: cmd/daemon/daemon_test.go ================================================ package daemon import ( "context" "os" "os/exec" "testing" "time" "github.com/abiosoft/colima/daemon/process" ) var testDir string func setDir(t *testing.T) { if testDir == "" { testDir = t.TempDir() } dir = func() string { return testDir } } func getProcesses() []process.Process { var addresses = []string{ "localhost", "127.0.0.1", } var processes []process.Process for _, add := range addresses { processes = append(processes, &pinger{address: add}) } return processes } func TestStart(t *testing.T) { setDir(t) info := Info() processes := getProcesses() t.Log("pidfile", info.PidFile) timeout := time.Second * 5 ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // start the processes if err := start(ctx, processes); err != nil { t.Fatal(err) } t.Log("start successful") { loop: for { select { case <-ctx.Done(): t.Skipf("daemon not supported: %v", ctx.Err()) default: if p, err := os.ReadFile(info.PidFile); err == nil && len(p) > 0 { break loop } else if err != nil { t.Logf("encountered err: %v", err) } time.Sleep(1 * time.Second) } } } // verify the processes are running if err := status(); err != nil { t.Error(err) return } // stop the processes if err := stop(ctx); err != nil { t.Error(err) } // verify the processes are no longer running if err := status(); err == nil { t.Errorf("process with pidFile %s is still running", info.PidFile) return } } func TestRunProcesses(t *testing.T) { processes := getProcesses() timeout := time.Second * 5 ctx, cancel := context.WithTimeout(context.Background(), timeout) // start the processes done := make(chan error, 1) go func() { done <- RunProcesses(ctx, processes...) }() cancel() select { case <-ctx.Done(): if err := ctx.Err(); err != context.Canceled { t.Error(err) } case err := <-done: t.Error(err) } } var _ process.Process = (*pinger)(nil) type pinger struct { address string } func (p pinger) Alive(ctx context.Context) error { return nil } // Name implements BgProcess func (pinger) Name() string { return "pinger" } // Start implements BgProcess func (p *pinger) Start(ctx context.Context) error { return p.run(ctx, "ping", "-c10", p.address) } // Start implements BgProcess func (p *pinger) Dependencies() ([]process.Dependency, bool) { return nil, false } func (p *pinger) run(ctx context.Context, command string, args ...string) error { cmd := exec.CommandContext(ctx, command, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } ================================================ FILE: cmd/delete.go ================================================ package cmd import ( "github.com/abiosoft/colima/cmd/root" "github.com/spf13/cobra" ) var deleteCmdArgs struct { force bool data bool } // deleteCmd represents the delete command var deleteCmd = &cobra.Command{ Use: "delete [profile]", Short: "delete and teardown Colima", Long: `Delete and teardown Colima and all settings. Use with caution. This deletes everything and a startup afterwards is like the initial startup of Colima.`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return newApp().Delete(deleteCmdArgs.data, deleteCmdArgs.force) }, } func init() { root.Cmd().AddCommand(deleteCmd) deleteCmd.Flags().BoolVarP(&deleteCmdArgs.force, "force", "f", false, "do not prompt for yes/no") deleteCmd.Flags().BoolVarP(&deleteCmdArgs.data, "data", "d", false, "delete container runtime data") } ================================================ FILE: cmd/kubernetes.go ================================================ package cmd import ( "context" "fmt" "github.com/abiosoft/colima/cmd/root" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment/container/kubernetes" "github.com/spf13/cobra" ) // kubernetesCmd represents the kubernetes command var kubernetesCmd = &cobra.Command{ Use: "kubernetes", Aliases: []string{"kube", "k8s", "k3s", "k"}, Short: "manage Kubernetes cluster", Long: `Manage the Kubernetes cluster`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { // cobra overrides PersistentPreRunE when redeclared. // re-run rootCmd's. if err := root.Cmd().PersistentPreRunE(cmd, args); err != nil { return err } if !newApp().Active() { return fmt.Errorf("%s is not running", config.CurrentProfile().DisplayName) } return nil }, } // kubernetesStartCmd represents the kubernetes start command var kubernetesStartCmd = &cobra.Command{ Use: "start", Short: "start the Kubernetes cluster", Long: `Start the Kubernetes cluster.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { app := newApp() k, err := app.Kubernetes() if err != nil { return err } if err := k.Provision(context.Background()); err != nil { return err } return k.Start(context.Background()) }, } // kubernetesStopCmd represents the kubernetes stop command var kubernetesStopCmd = &cobra.Command{ Use: "stop", Short: "stop the Kubernetes cluster", Long: `Stop the Kubernetes cluster.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() app := newApp() k, err := app.Kubernetes() if err != nil { return err } if !k.Running(ctx) { return fmt.Errorf("%s is not enabled", kubernetes.Name) } return k.Stop(ctx, false) }, } // kubernetesDeleteCmd represents the kubernetes delete command var kubernetesDeleteCmd = &cobra.Command{ Use: "delete", Short: "delete the Kubernetes cluster", Long: `Delete the Kubernetes cluster.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() app := newApp() k, err := app.Kubernetes() if err != nil { return err } if !k.Running(ctx) { return fmt.Errorf("%s is not enabled", kubernetes.Name) } return k.Teardown(ctx) }, } // kubernetesResetCmd represents the kubernetes reset command var kubernetesResetCmd = &cobra.Command{ Use: "reset", Short: "reset the Kubernetes cluster", Long: `Reset the Kubernetes cluster. This resets the Kubernetes cluster and all Kubernetes objects will be deleted. The Kubernetes images are cached making the startup (after reset) much faster.`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { app := newApp() k, err := app.Kubernetes() if err != nil { return err } if err := k.Teardown(context.Background()); err != nil { return fmt.Errorf("error deleting %s: %w", kubernetes.Name, err) } ctx := context.Background() if err := k.Provision(ctx); err != nil { return err } if err := k.Start(ctx); err != nil { return fmt.Errorf("error starting %s: %w", kubernetes.Name, err) } return nil }, } func init() { root.Cmd().AddCommand(kubernetesCmd) kubernetesCmd.AddCommand(kubernetesStartCmd) kubernetesCmd.AddCommand(kubernetesStopCmd) kubernetesCmd.AddCommand(kubernetesDeleteCmd) kubernetesCmd.AddCommand(kubernetesResetCmd) } ================================================ FILE: cmd/list.go ================================================ package cmd import ( "encoding/json" "fmt" "text/tabwriter" "github.com/abiosoft/colima/cmd/root" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/docker/go-units" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) var listCmdArgs struct { json bool } // listCmd represents the version command var listCmd = &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "list instances", Long: `List all created instances. A new instance can be created during 'colima start' by specifying the '--profile' flag.`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { profile := config.CurrentProfile() profileArgs := []string{} if profile.Changed { profileArgs = append(profileArgs, profile.ID) } instances, err := limautil.Instances(profileArgs...) if err != nil { return err } if listCmdArgs.json { encoder := json.NewEncoder(cmd.OutOrStdout()) // print instance per line to conform with Lima's output for _, instance := range instances { // dir should be hidden from the output instance.Dir = "" if err := encoder.Encode(instance); err != nil { return err } } return nil } w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0) _, _ = fmt.Fprintln(w, "PROFILE\tSTATUS\tARCH\tCPUS\tMEMORY\tDISK\tRUNTIME\tADDRESS") if len(instances) == 0 { logrus.Warn("No instance found. Run `colima start` to create an instance.") } for _, inst := range instances { _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s\t%s\t%s\t%s\n", inst.Name, inst.Status, inst.Arch, inst.CPU, units.BytesSize(float64(inst.Memory)), units.BytesSize(float64(inst.Disk)), inst.Runtime, inst.IPAddress, ) } return w.Flush() }, } func init() { root.Cmd().AddCommand(listCmd) listCmd.Flags().BoolVarP(&listCmdArgs.json, "json", "j", false, "print json output") } ================================================ FILE: cmd/model.go ================================================ package cmd import ( "fmt" "github.com/abiosoft/colima/cmd/root" "github.com/abiosoft/colima/config/configmanager" "github.com/abiosoft/colima/model" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/terminal" "github.com/spf13/cobra" ) // modelCmdArgs holds command-line flags for the model command. var modelCmdArgs struct { Runner string ServePort int } // modelCmd represents the model command var modelCmd = &cobra.Command{ Use: "model", Short: "manage AI models (requires docker runtime and krunkit VM type)", Long: `Manage AI models inside the VM. This requires docker runtime and krunkit VM type for GPU access. Use --runner to select the model runner: - docker: Docker Model Runner (default) - ramalama: Ramalama All arguments are passed to the selected AI model runner. Specifying '--' will pass arguments to the underlying tool. Examples: colima model list colima model pull ai/smollm2 colima model run ai/smollm2 colima model serve colima model serve ai/smollm2 --port 8080 Multiple registries are supported. `, PreRunE: func(cmd *cobra.Command, args []string) error { runner, err := getModelRunner() if err != nil { return err } return runner.ValidatePrerequisites(newApp()) }, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { return cmd.Help() } runner, err := getModelRunner() if err != nil { return err } a := newApp() if err := runner.EnsureProvisioned(); err != nil { return err } runnerArgs, err := runner.BuildArgs(args) if err != nil { return err } return a.SSH(runnerArgs...) }, } // modelSetupCmd reinstalls the model runner in the VM. var modelSetupCmd = &cobra.Command{ Use: "setup", Short: "install or update AI model runner in the VM", Long: `Install or update AI model runner and its dependencies in the VM.`, Aliases: []string{"update"}, PreRunE: func(cmd *cobra.Command, args []string) error { runner, err := getModelRunner() if err != nil { return err } return runner.ValidatePrerequisites(newApp()) }, RunE: func(cmd *cobra.Command, args []string) error { runner, err := getModelRunner() if err != nil { return err } // Check if setup is needed (on primary screen) status, err := runner.CheckSetup() if err != nil { return err } // Print version info on primary screen fmt.Println(runner.DisplayName()) if status.CurrentVersion != "" { fmt.Printf("current: %s\n", status.CurrentVersion) } if status.LatestVersion != "" { fmt.Printf("latest: %s\n", status.LatestVersion) } if !status.NeedsSetup { fmt.Println() fmt.Println("Already up to date") return nil } // Build header for alternate screen separator := "────────────────────────────────────────" header := fmt.Sprintf("Colima - %s Setup\n%s", runner.DisplayName(), separator) // Run setup in alternate screen if err := terminal.WithAltScreen(func() error { return runner.Setup() }, header); err != nil { return err } // Print new version on primary screen after update if newVersion := runner.GetCurrentVersion(); newVersion != "" { fmt.Printf("updated: %s\n", newVersion) } return nil }, } // modelServeCmd serves a model API. var modelServeCmd = &cobra.Command{ Use: "serve [model]", Short: "serve a model API", Long: `Serve a model API. This starts a model server providing: - OpenAI-compatible API at http://localhost:/v1 - Web UI for chat at http://localhost: Press Ctrl-C to stop the server. `, Args: cobra.MaximumNArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { runner, err := getModelRunner() if err != nil { return err } return runner.ValidatePrerequisites(newApp()) }, RunE: func(cmd *cobra.Command, args []string) error { runner, err := getModelRunner() if err != nil { return err } // Determine the model to serve var modelName string if len(args) > 0 { modelName = args[0] } else if runner.Name() == model.RunnerDocker { // For docker runner, get the first available model firstModel, err := model.GetFirstModel() if err != nil { return err } if firstModel == "" { return fmt.Errorf("no models available\nPull a model first: colima model pull ai/smollm2") } modelName = firstModel } else { return fmt.Errorf("model name is required for ramalama runner\nUsage: colima model serve ") } if err := runner.EnsureProvisioned(); err != nil { return err } // Ensure the model is available (pull if necessary) - this happens outside alternate screen normalizedModel, err := runner.EnsureModel(modelName) if err != nil { return err } // Determine the port to use port := modelCmdArgs.ServePort portExplicitlySet := cmd.Flags().Changed("port") // If port was not explicitly set, find an available port starting from the default const maxPortAttempts = 20 if !portExplicitlySet { availablePort, found := util.FindAvailablePort(port, maxPortAttempts) if !found { return fmt.Errorf("no available port found in range %d-%d", port, port+maxPortAttempts-1) } if availablePort != port { fmt.Printf("Port %d is in use, using port %d instead\n", port, availablePort) } port = availablePort } else { // User explicitly set the port, check if it's available if _, found := util.FindAvailablePort(port, 1); !found { return fmt.Errorf("port %d is already in use", port) } } // Build header for alternate screen separator := "────────────────────────────────────────" header := fmt.Sprintf("Colima - Model Server (Ctrl-C to stop)\nWeb UI & API at http://localhost:%d\n%s", port, separator) // Run in alternate screen with header return terminal.WithAltScreen(func() error { return runner.Serve(normalizedModel, port) }, header) }, } func init() { root.Cmd().AddCommand(modelCmd) modelCmd.AddCommand(modelSetupCmd) modelCmd.AddCommand(modelServeCmd) // Add --runner flag with default from config or ramalama modelCmd.PersistentFlags().StringVar(&modelCmdArgs.Runner, "runner", "", "AI model runner (docker, ramalama)") // Add --port flag for serve command modelServeCmd.Flags().IntVar(&modelCmdArgs.ServePort, "port", 8080, "port for the web UI") } // getModelRunner returns the appropriate runner based on flag or config. func getModelRunner() (model.Runner, error) { runnerType := modelCmdArgs.Runner // If not specified via flag, check instance config if runnerType == "" { if conf, err := configmanager.LoadInstance(); err == nil && conf.ModelRunner != "" { runnerType = conf.ModelRunner } } // Default to docker if runnerType == "" { runnerType = string(model.RunnerDocker) } return model.GetRunner(model.RunnerType(runnerType)) } ================================================ FILE: cmd/nerdctl.go ================================================ package cmd import ( "bytes" "fmt" "log" "os" "path/filepath" "strings" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/cmd/root" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment/container/containerd" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/fsutil" "github.com/abiosoft/colima/util/osutil" "github.com/spf13/cobra" ) var nerdctlCmdArgs struct { force bool path string usrBinWriteable bool isColimaScript bool } // nerdctlCmd represents the nerdctl command var nerdctlCmd = &cobra.Command{ Use: "nerdctl", Aliases: []string{"nerd", "n"}, Short: "run nerdctl (requires containerd runtime)", Long: `Run nerdctl to interact with containerd. This requires containerd runtime. It is recommended to specify '--' to differentiate from Colima flags. `, RunE: func(cmd *cobra.Command, args []string) error { app := newApp() r, err := app.Runtime() if err != nil { return err } if r != containerd.Name { return fmt.Errorf("nerdctl only supports %s runtime", containerd.Name) } // collect CONTAINERD_* and NERDCTL_* environment variables from the host var envVars []string for _, env := range os.Environ() { if strings.HasPrefix(env, "CONTAINERD_") || strings.HasPrefix(env, "NERDCTL_") { envVars = append(envVars, env) } } var nerdctlArgs []string if len(envVars) > 0 { // use 'sudo env VAR=value ... nerdctl' to pass environment variables nerdctlArgs = append([]string{"sudo", "env"}, envVars...) nerdctlArgs = append(nerdctlArgs, "nerdctl") } else { nerdctlArgs = []string{"sudo", "nerdctl"} } nerdctlArgs = append(nerdctlArgs, args...) return app.SSH(nerdctlArgs...) }, } // nerdctlLinkFunc represents the nerdctl command var nerdctlLinkFunc = func() *cobra.Command { return &cobra.Command{ Use: "install", Short: "install nerdctl alias script on the host", Long: `Install nerdctl alias script on the host. The script will be installed at ` + nerdctlDefaultInstallPath + `.`, Args: cobra.NoArgs, PreRun: func(cmd *cobra.Command, args []string) { // check if /usr/local/bin is writeable and no need for sudo // if the path is user-specified, ignore. if nerdctlCmdArgs.path != nerdctlDefaultInstallPath { return } // attempt writing to the /usr/local/bin tmpFile := filepath.Join(filepath.Dir(nerdctlDefaultInstallPath), "colima.tmp") if err := os.WriteFile(tmpFile, []byte("tmp"), 0777); err == nil { nerdctlCmdArgs.usrBinWriteable = true _ = os.Remove(tmpFile) } // check if the current file (if exists) is generated by colima // in such case no need for confirmation before overwrite // TODO: this is too basic, should be better if b, err := os.ReadFile(nerdctlCmdArgs.path); err == nil { if strings.Contains(string(b), "colima nerdctl ") { nerdctlCmdArgs.isColimaScript = true } } }, RunE: func(cmd *cobra.Command, args []string) error { exists := false if _, err := os.Stat(nerdctlCmdArgs.path); err == nil && !nerdctlCmdArgs.force && !nerdctlCmdArgs.isColimaScript { return fmt.Errorf("%s exists, use --force to replace", nerdctlCmdArgs.path) } else if err == nil { exists = true } var values = struct { ColimaApp string Profile string }{ ColimaApp: osutil.Executable(), Profile: config.CurrentProfile().ShortName, } buf, err := util.ParseTemplate(nerdctlScript, values) if err != nil { return fmt.Errorf("error applying nerdctl script template: %w", err) } // /usr/local/bin writeable i.e. sudo not needed // or user-specified install path, we assume user specified path is writeable if nerdctlCmdArgs.usrBinWriteable || nerdctlCmdArgs.path != nerdctlDefaultInstallPath { if exists { if err := os.Rename(nerdctlCmdArgs.path, nerdctlCmdArgs.path+".moved"); err != nil { return fmt.Errorf("error backing up existing file: %w", err) } } if err := fsutil.MkdirAll("/usr/local/bin", 0755); err != nil { return nil } return os.WriteFile(nerdctlCmdArgs.path, buf, 0755) } // sudo is needed for the default path log.Println("/usr/local/bin not writable, sudo password required to install nerdctl binary") if exists && !nerdctlCmdArgs.isColimaScript { c := cli.CommandInteractive("sudo", "mv", nerdctlCmdArgs.path, nerdctlCmdArgs.path+".moved") if err := c.Run(); err != nil { return fmt.Errorf("error backing up existing file: %w", err) } } // prepare dir { c := cli.CommandInteractive("sudo", "mkdir", "-p", "/usr/local/bin") if err := c.Run(); err != nil { return err } } // install script { c := cli.CommandInteractive("sudo", "sh", "-c", "cat > "+nerdctlCmdArgs.path) c.Stdin = bytes.NewReader(buf) if err := c.Run(); err != nil { return err } } // ensure it is executable if err := cli.Command("sudo", "chmod", "+x", nerdctlCmdArgs.path).Run(); err != nil { return err } return nil }, } } const nerdctlDefaultInstallPath = "/usr/local/bin/nerdctl" const nerdctlScript = `#!/usr/bin/env sh {{.ColimaApp}} nerdctl --profile {{.Profile}} -- "$@" ` func init() { root.Cmd().AddCommand(nerdctlCmd) nerdctlLink := nerdctlLinkFunc() nerdctlCmd.AddCommand(nerdctlLink) nerdctlLink.Flags().BoolVarP(&nerdctlCmdArgs.force, "force", "f", false, "replace "+nerdctlDefaultInstallPath+" (if exists)") nerdctlLink.Flags().StringVar(&nerdctlCmdArgs.path, "path", nerdctlDefaultInstallPath, "path to install nerdctl binary") } ================================================ FILE: cmd/prune.go ================================================ package cmd import ( "fmt" "os" "path/filepath" "strconv" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/cmd/root" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) var pruneCmdArgs struct { force bool all bool } // pruneCmd represents the prune command var pruneCmd = &cobra.Command{ Use: "prune", Short: "prune cached downloaded assets", Long: `Prune cached downloaded assets`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { colimaCacheDir := config.CacheDir() limaCacheDir := filepath.Join(filepath.Dir(colimaCacheDir), "lima") if !pruneCmdArgs.force { msg := "'" + colimaCacheDir + "' will be emptied, are you sure" if pruneCmdArgs.all { msg = "'" + colimaCacheDir + "' and '" + limaCacheDir + "' will be emptied, are you sure" } if y := cli.Prompt(msg); !y { return nil } } logrus.Info("Pruning ", strconv.Quote(config.CacheDir())) if err := os.RemoveAll(config.CacheDir()); err != nil { return fmt.Errorf("error during prune: %w", err) } if pruneCmdArgs.all { cmd := limautil.Limactl("prune") if err := cmd.Run(); err != nil { return fmt.Errorf("error during Lima prune: %w", err) } } return nil }, } func init() { root.Cmd().AddCommand(pruneCmd) pruneCmd.Flags().BoolVarP(&pruneCmdArgs.force, "force", "f", false, "do not prompt for yes/no") pruneCmd.Flags().BoolVarP(&pruneCmdArgs.all, "all", "a", false, "include Lima assets") } ================================================ FILE: cmd/restart.go ================================================ package cmd import ( "time" "github.com/abiosoft/colima/cmd/root" "github.com/abiosoft/colima/config/configmanager" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/spf13/cobra" ) var restartCmdArgs struct { force bool } // restartCmd represents the restart command var restartCmd = &cobra.Command{ Use: "restart [profile]", Short: "restart Colima", Long: `Stop and then starts Colima. The state of the VM is persisted at stop. A start afterwards should return it back to its previous state.`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { // validate if the instance was previously created if _, err := limautil.Instance(); err != nil { return err } app := newApp() if err := app.Stop(restartCmdArgs.force); err != nil { return err } // delay a bit before starting time.Sleep(time.Second * 3) config, err := configmanager.Load() if err != nil { return err } return app.Start(config) }, } func init() { root.Cmd().AddCommand(restartCmd) restartCmd.Flags().BoolVarP(&restartCmdArgs.force, "force", "f", false, "during restart, do stop without graceful shutdown") } ================================================ FILE: cmd/root/root.go ================================================ package root import ( "log" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) var versionInfo = config.AppVersion() // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "colima", Short: "container runtimes on macOS with minimal setup", Long: `Colima provides container runtimes on macOS with minimal setup.`, Version: versionInfo.Version, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { // use profile from environment variable if set profile := config.EnvProfile() switch cmd.Name() { // special case handling for commands directly interacting with the VM // start, stop, restart, delete, status, version, update, ssh-config case "start", "stop", "restart", "delete", "status", "list", "version", "update", "ssh-config": // if an arg is passed, assume it to be the profile (provided --profile is unset) // i.e. colima start docker == colima start --profile=docker // takes precedence over the environment variable if len(args) > 0 && !cmd.Flag("profile").Changed { profile = args[0] } } // if profile is set via flag, use it // takes precedence over the environment variable and arg if cmd.Flag("profile").Changed { profile = rootCmdArgs.Profile } if profile != "" { config.SetProfile(profile) } initLog() cmd.SilenceUsage = true cmd.SilenceErrors = true return nil }, } // Cmd returns the root command. func Cmd() *cobra.Command { return rootCmd } // rootCmdArgs holds all flags configured in root Cmd var rootCmdArgs struct { Profile string Verbose bool VeryVerbose bool } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { if err := rootCmd.Execute(); err != nil { logrus.Fatal(err) } } func init() { rootCmd.PersistentFlags().BoolVarP(&rootCmdArgs.Verbose, "verbose", "v", rootCmdArgs.Verbose, "enable verbose log") rootCmd.PersistentFlags().BoolVar(&rootCmdArgs.VeryVerbose, "very-verbose", rootCmdArgs.VeryVerbose, "enable more verbose log") rootCmd.PersistentFlags().StringVarP(&rootCmdArgs.Profile, "profile", "p", "default", "profile name, for multiple instances") } func initLog() { if rootCmdArgs.Verbose { cli.Settings.Verbose = true logrus.SetLevel(logrus.DebugLevel) } if rootCmdArgs.VeryVerbose { cli.Settings.Verbose = true logrus.SetLevel(logrus.TraceLevel) } // general log output log.SetOutput(logrus.StandardLogger().Writer()) log.SetFlags(0) } ================================================ FILE: cmd/ssh-config.go ================================================ package cmd import ( "fmt" "github.com/abiosoft/colima/cmd/root" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/spf13/cobra" ) // statusCmd represents the status command var sshConfigCmd = &cobra.Command{ Use: "ssh-config [profile]", Short: "show SSH connection config", Long: `Show configuration of the SSH connection to the VM.`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { resp, err := limautil.ShowSSH(config.CurrentProfile().ID) if err == nil { fmt.Println(resp.Output) } return err }, } func init() { root.Cmd().AddCommand(sshConfigCmd) } ================================================ FILE: cmd/ssh.go ================================================ package cmd import ( "github.com/abiosoft/colima/cmd/root" "github.com/spf13/cobra" ) // sshCmd represents the ssh command var sshCmd = &cobra.Command{ Use: "ssh", Aliases: []string{"exec", "x"}, Short: "SSH into the VM", Long: `SSH into the VM. Appending additional command runs the command instead. e.g. 'colima ssh -- htop' will run htop. It is recommended to specify '--' to differentiate from colima flags.`, RunE: func(cmd *cobra.Command, args []string) error { return newApp().SSH(args...) }, } func init() { root.Cmd().AddCommand(sshCmd) } ================================================ FILE: cmd/start.go ================================================ package cmd import ( "fmt" "net" "os" "os/signal" "path/filepath" "strings" "syscall" "time" "github.com/abiosoft/colima/app" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/cmd/root" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/config/configmanager" "github.com/abiosoft/colima/core" "github.com/abiosoft/colima/embedded" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/container/docker" "github.com/abiosoft/colima/environment/container/incus" "github.com/abiosoft/colima/environment/container/kubernetes" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/downloader" "github.com/abiosoft/colima/util/osutil" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) // startCmd represents the start command var startCmd = &cobra.Command{ Use: "start [profile]", Short: "start Colima", Long: `Start Colima with the specified container runtime and optional kubernetes. Colima can also be configured with a YAML file. Run 'colima template' to set the default configurations or 'colima start --edit' to customize before startup. `, Example: " colima start\n" + " colima start --edit\n" + " colima start --foreground\n" + " colima start --runtime containerd\n" + " colima start --kubernetes\n" + " colima start --runtime containerd --kubernetes\n" + " colima start --cpu 4 --memory 8 --disk 100\n" + " colima start --arch aarch64\n" + " colima start --dns 1.1.1.1 --dns 8.8.8.8\n" + " colima start --dns-host example.com=1.2.3.4\n" + " colima start --gateway-address 192.168.6.2\n" + " colima start --kubernetes --k3s-arg='\"--disable=coredns,servicelb,traefik,local-storage,metrics-server\"'", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { app := newApp() conf := startCmdArgs.Config if !startCmdArgs.Flags.Edit { if app.Active() { log.Warnln("already running, ignoring") return nil } return start(app, conf) } // edit flag is specified conf, err := editConfigFile() if err != nil { return err } // validate config if err := configmanager.ValidateConfig(conf); err != nil { return fmt.Errorf("error in config file: %w", err) } if app.Active() { if !cli.Prompt("colima is currently running, restart to apply changes") { return nil } if err := app.Stop(false); err != nil { return fmt.Errorf("error stopping :%w", err) } // pause before startup to prevent race condition time.Sleep(time.Second * 3) } return start(app, conf) }, PreRunE: func(cmd *cobra.Command, args []string) error { // validate Lima version if err := core.LimaVersionSupported(); err != nil { return fmt.Errorf("lima compatibility error: %w", err) } // combine args and current config file(if any) prepareConfig(cmd) // validate config if err := configmanager.ValidateConfig(startCmdArgs.Config); err != nil { return fmt.Errorf("error in config: %w", err) } // persist in preparation for application start if startCmdArgs.Flags.SaveConfig { if err := configmanager.Save(startCmdArgs.Config); err != nil { return fmt.Errorf("error preparing config file: %w", err) } } // validate and set downloader if flag is specified (takes precedence over env var) if cmd.Flag("downloader").Changed { normalized, err := downloader.ValidateDownloader(startCmdArgs.Flags.Downloader) if err != nil { return err } downloader.SetDownloader(normalized) } return nil }, } const ( defaultCPU = 2 defaultMemory = 2 defaultDisk = 100 defaultRootDisk = 20 defaultKubernetesVersion = kubernetes.DefaultVersion defaultMountTypeQEMU = "sshfs" defaultMountTypeVZ = "virtiofs" ) var ( defaultVMType = "qemu" defaultK3sArgs = []string{"--disable=traefik"} envSaveConfig = osutil.EnvVar("COLIMA_SAVE_CONFIG") ) var startCmdArgs struct { config.Config Flags struct { Mounts []string LegacyKubernetes bool // for backward compatibility LegacyKubernetesDisable []string Edit bool Editor string ActivateRuntime bool Binfmt bool DNSHosts []string Foreground bool SaveConfig bool LegacyCPU int // for backward compatibility Template bool Downloader string // downloader to use (native, curl) } } func init() { runtimes := strings.Join(environment.ContainerRuntimes(), ", ") defaultArch := string(environment.HostArch()) defaultVMType = environment.DefaultVMType() defaultMountType := defaultMountTypeQEMU if defaultVMType == "vz" { defaultMountType = defaultMountTypeVZ } mounts := strings.Join([]string{defaultMountTypeQEMU, "9p", "virtiofs"}, ", ") vmTypes := []string{"qemu", "vz"} if util.MacOS13OrNewerOnArm() { vmTypes = append(vmTypes, "krunkit") } types := strings.Join(vmTypes, ", ") saveConfigDefault := true if envSaveConfig.Exists() { saveConfigDefault = envSaveConfig.Bool() } root.Cmd().AddCommand(startCmd) startCmd.Flags().StringVarP(&startCmdArgs.Runtime, "runtime", "r", docker.Name, "container runtime ("+runtimes+")") startCmd.Flags().BoolVar(&startCmdArgs.Flags.ActivateRuntime, "activate", true, "set as active Docker/Kubernetes/Incus context on startup") startCmd.Flags().IntVarP(&startCmdArgs.CPU, "cpus", "c", defaultCPU, "number of CPUs") startCmd.Flags().StringVar(&startCmdArgs.CPUType, "cpu-type", "", "the CPU type, options can be checked with 'qemu-system-"+defaultArch+" -cpu help'") startCmd.Flags().Float32VarP(&startCmdArgs.Memory, "memory", "m", defaultMemory, "memory in GiB") startCmd.Flags().IntVarP(&startCmdArgs.Disk, "disk", "d", defaultDisk, "disk size in GiB") startCmd.Flags().IntVar(&startCmdArgs.RootDisk, "root-disk", defaultRootDisk, "disk size in GiB for the root filesystem") startCmd.Flags().StringVarP(&startCmdArgs.Arch, "arch", "a", defaultArch, "architecture (aarch64, x86_64)") startCmd.Flags().BoolVarP(&startCmdArgs.Flags.Foreground, "foreground", "f", false, "Keep colima in the foreground") startCmd.Flags().StringVar(&startCmdArgs.Hostname, "hostname", "", "custom hostname for the virtual machine") startCmd.Flags().StringVarP(&startCmdArgs.DiskImage, "disk-image", "i", "", "file path to a custom disk image") startCmd.Flags().BoolVar(&startCmdArgs.Flags.Template, "template", true, "use the template file for initial configuration") // port forwarder startCmd.Flags().StringVar(&startCmdArgs.PortForwarder, "port-forwarder", "ssh", "port forwarder to use (ssh, grpc, none)") // retain cpu flag for backward compatibility startCmd.Flags().IntVar(&startCmdArgs.Flags.LegacyCPU, "cpu", defaultCPU, "number of CPUs") startCmd.Flag("cpu").Hidden = true // host IP addresses startCmd.Flags().BoolVar(&startCmdArgs.Network.HostAddresses, "network-host-addresses", false, "support port forwarding to specific host IP addresses") binfmtDesc := "use binfmt for foreign architecture emulation" if util.MacOS() { // network address startCmd.Flags().BoolVar(&startCmdArgs.Network.Address, "network-address", false, "assign reachable IP address to the VM") startCmd.Flags().StringVar(&startCmdArgs.Network.Mode, "network-mode", "shared", "network mode (shared, bridged)") startCmd.Flags().StringVar(&startCmdArgs.Network.BridgeInterface, "network-interface", "en0", "host network interface to use for bridged mode") startCmd.Flags().BoolVar(&startCmdArgs.Network.PreferredRoute, "network-preferred-route", false, "use the assigned IP address as the preferred route for the VM (implies --network-address)") // vm type if util.MacOS13OrNewer() { startCmd.Flags().StringVarP(&startCmdArgs.VMType, "vm-type", "t", defaultVMType, "virtual machine type ("+types+")") if util.MacOS13OrNewerOnArm() { startCmd.Flags().BoolVar(&startCmdArgs.VZRosetta, "vz-rosetta", false, "enable Rosetta for amd64 emulation") startCmd.Flags().StringVar(&startCmdArgs.ModelRunner, "model-runner", "docker", "AI model runner (docker, ramalama)") binfmtDesc += " (no-op if Rosetta is enabled)" } } // nested virtualization if util.MacOSNestedVirtualizationSupported() { startCmd.Flags().BoolVarP(&startCmdArgs.NestedVirtualization, "nested-virtualization", "z", false, "enable nested virtualization") } } // Gateway Address startCmd.Flags().IPVar(&startCmdArgs.Network.GatewayAddress, "gateway-address", net.ParseIP("192.168.5.2"), "gateway address") // binfmt startCmd.Flags().BoolVar(&startCmdArgs.Flags.Binfmt, "binfmt", true, binfmtDesc) // config startCmd.Flags().BoolVarP(&startCmdArgs.Flags.Edit, "edit", "e", false, "edit the configuration file before starting") startCmd.Flags().StringVar(&startCmdArgs.Flags.Editor, "editor", "", `editor to use for edit e.g. vim, nano, code (default "$EDITOR" env var)`) startCmd.Flags().BoolVar(&startCmdArgs.Flags.SaveConfig, "save-config", saveConfigDefault, "persist and overwrite config file with (newly) specified flags") // mounts startCmd.Flags().StringSliceVarP(&startCmdArgs.Flags.Mounts, "mount", "V", nil, "directories to mount, suffix ':w' for writable, disable with 'none'") startCmd.Flags().StringVar(&startCmdArgs.MountType, "mount-type", defaultMountType, "volume driver for the mount ("+mounts+")") startCmd.Flags().BoolVar(&startCmdArgs.MountINotify, "mount-inotify", true, "propagate inotify file events to the VM") // ssh startCmd.Flags().BoolVarP(&startCmdArgs.ForwardAgent, "ssh-agent", "s", false, "forward SSH agent to the VM") startCmd.Flags().BoolVar(&startCmdArgs.SSHConfig, "ssh-config", true, "generate SSH config in ~/.ssh/config") startCmd.Flags().IntVar(&startCmdArgs.SSHPort, "ssh-port", 0, "SSH server port") // k8s startCmd.Flags().BoolVarP(&startCmdArgs.Kubernetes.Enabled, "kubernetes", "k", false, "start with Kubernetes") startCmd.Flags().BoolVar(&startCmdArgs.Flags.LegacyKubernetes, "with-kubernetes", false, "start with Kubernetes") startCmd.Flags().StringVar(&startCmdArgs.Kubernetes.Version, "kubernetes-version", defaultKubernetesVersion, "must match a k3s version https://github.com/k3s-io/k3s/releases") startCmd.Flags().StringSliceVar(&startCmdArgs.Flags.LegacyKubernetesDisable, "kubernetes-disable", nil, "components to disable for k3s e.g. traefik,servicelb") startCmd.Flags().StringSliceVar(&startCmdArgs.Kubernetes.K3sArgs, "k3s-arg", defaultK3sArgs, "additional args to pass to k3s") startCmd.Flags().IntVar(&startCmdArgs.Kubernetes.Port, "k3s-listen-port", 0, "k3s server listen port") startCmd.Flag("with-kubernetes").Hidden = true startCmd.Flag("kubernetes-disable").Hidden = true // env startCmd.Flags().StringToStringVar(&startCmdArgs.Env, "env", nil, "environment variables for the VM") // dns startCmd.Flags().IPSliceVarP(&startCmdArgs.Network.DNSResolvers, "dns", "n", nil, "DNS resolvers for the VM") startCmd.Flags().StringSliceVar(&startCmdArgs.Flags.DNSHosts, "dns-host", nil, "custom DNS names to provide to resolver") // download options startCmd.Flags().StringVar(&startCmdArgs.Flags.Downloader, "downloader", downloader.DownloaderNative, "downloader to use (native, curl)") } func dnsHostsFromFlag(hosts []string) map[string]string { mapping := make(map[string]string) for _, h := range hosts { str := strings.SplitN(h, "=", 2) if len(str) != 2 { log.Warnf("unable to parse custom dns host: %v, skipping\n", h) continue } src := str[0] target := str[1] mapping[src] = target } return mapping } // mountsFromFlag converts mounts from cli flag format to config file format func mountsFromFlag(mounts []string) []config.Mount { mnts := make([]config.Mount, len(mounts)) for i, mount := range mounts { // if one of the parameters is none, treat as none. if strings.ToLower(mount) == "none" { return nil } str := strings.SplitN(mount, ":", 3) mnt := config.Mount{Location: str[0]} if len(str) > 1 { if filepath.IsAbs(str[1]) { mnt.MountPoint = str[1] } else if str[1] == "w" { mnt.Writable = true } } if len(str) > 2 && str[2] == "w" { mnt.Writable = true } mnts[i] = mnt } return mnts } func setFlagDefaults(cmd *cobra.Command) { if startCmdArgs.VMType == "" { startCmdArgs.VMType = defaultVMType } if util.MacOS13OrNewer() { // changing to vz implies changing mount type to virtiofs if cmd.Flag("vm-type").Changed && startCmdArgs.VMType == "vz" && !cmd.Flag("mount-type").Changed { startCmdArgs.MountType = "virtiofs" cmd.Flag("mount-type").Changed = true } } // mount type { // convert mount type for qemu if startCmdArgs.VMType != "vz" && startCmdArgs.VMType != "krunkit" && startCmdArgs.MountType == defaultMountTypeVZ { startCmdArgs.MountType = defaultMountTypeQEMU if cmd.Flag("mount-type").Changed { log.Warnf("%s is only available for 'vz' vmType, using %s", defaultMountTypeVZ, defaultMountTypeQEMU) } } // convert mount type for vz if startCmdArgs.VMType == "vz" && startCmdArgs.MountType == "9p" { startCmdArgs.MountType = "virtiofs" if cmd.Flag("mount-type").Changed { log.Warnf("9p is only available for 'qemu' vmType, using %s", defaultMountTypeVZ) } } } // always enable nested virtualization for incus, if supported and not explicitly disabled. if util.MacOSNestedVirtualizationSupported() { if !cmd.Flag("nested-virtualization").Changed { if startCmdArgs.Runtime == incus.Name && (startCmdArgs.VMType == "vz" || startCmdArgs.VMType == "krunkit") { startCmdArgs.NestedVirtualization = true } } } // always enable network address for incus, if supported and not explicitly disabled if util.MacOS13OrNewer() { if !cmd.Flag("network-address").Changed { if startCmdArgs.Runtime == incus.Name && startCmdArgs.VMType == "vz" { startCmdArgs.Network.Address = true } } } } func setConfigDefaults(conf *config.Config) { // handle macOS virtualization.framework transition if conf.VMType == "" { conf.VMType = defaultVMType // if on macOS with no qemu, use vz if err := util.AssertQemuImg(); err != nil && util.MacOS13OrNewer() { conf.VMType = "vz" } } if conf.MountType == "" { conf.MountType = defaultMountTypeQEMU if util.MacOS13OrNewer() && conf.VMType == "vz" { conf.MountType = defaultMountTypeVZ } } if conf.Hostname == "" { conf.Hostname = config.CurrentProfile().ID } if conf.PortForwarder == "" { conf.PortForwarder = "ssh" } } func setFixedConfigs(conf *config.Config) { fixedConf, err := configmanager.LoadFrom(config.CurrentProfile().StateFile()) if err != nil { return } warnIfNotEqual := func(name, newVal, fixedVal string) { if newVal != fixedVal { log.Warnln(fmt.Errorf("'%s' cannot be updated after initial setup, discarded", name)) } } // override the fixed configs // arch, vmType, mountType, runtime are fixed and cannot be changed if fixedConf.Arch != "" { warnIfNotEqual("architecture", conf.Arch, fixedConf.Arch) conf.Arch = fixedConf.Arch } if fixedConf.VMType != "" { warnIfNotEqual("virtual machine type", conf.VMType, fixedConf.VMType) conf.VMType = fixedConf.VMType } if fixedConf.Runtime != "" { warnIfNotEqual("runtime", conf.Runtime, fixedConf.Runtime) conf.Runtime = fixedConf.Runtime } if fixedConf.MountType != "" { warnIfNotEqual("volume mount type", conf.MountType, fixedConf.MountType) conf.MountType = fixedConf.MountType } if fixedConf.Network.Address && !conf.Network.Address { log.Warnln("network address cannot be disabled once enabled") conf.Network.Address = true } if fixedConf.Network.Mode != "" { warnIfNotEqual("network mode", conf.Network.Mode, fixedConf.Network.Mode) conf.Network.Mode = fixedConf.Network.Mode } } func prepareConfig(cmd *cobra.Command) { current, err := configmanager.Load() if err != nil { // not fatal, will proceed with defaults log.Warnln(fmt.Errorf("config load failed: %w", err)) log.Warnln("reverting to default settings") } // handle legacy kubernetes flag if cmd.Flag("with-kubernetes").Changed { startCmdArgs.Kubernetes.Enabled = startCmdArgs.Flags.LegacyKubernetes cmd.Flag("kubernetes").Changed = true } // handle legacy cpu flag if cmd.Flag("cpu").Changed && !cmd.Flag("cpus").Changed { startCmdArgs.CPU = startCmdArgs.Flags.LegacyCPU cmd.Flag("cpus").Changed = true } // convert cli to config file format startCmdArgs.Mounts = mountsFromFlag(startCmdArgs.Flags.Mounts) startCmdArgs.Network.DNSHosts = dnsHostsFromFlag(startCmdArgs.Flags.DNSHosts) startCmdArgs.ActivateRuntime = &startCmdArgs.Flags.ActivateRuntime startCmdArgs.Binfmt = &startCmdArgs.Flags.Binfmt // handle legacy kubernetes-disable for _, disable := range startCmdArgs.Flags.LegacyKubernetesDisable { startCmdArgs.Kubernetes.K3sArgs = append(startCmdArgs.Kubernetes.K3sArgs, "--disable="+disable) } // set relevant missing default values setFlagDefaults(cmd) // if there is no existing settings if current.Empty() { templateUsed := false // attempt template if enabled if startCmdArgs.Flags.Template { template, err := configmanager.LoadFrom(templateFile()) if err == nil { current = template templateUsed = true } } if !templateUsed { // use default config if there is no template or template is disabled return } } // set missing defaults in the current config setConfigDefaults(¤t) // docker can only be set in config file startCmdArgs.Docker = current.Docker // provision scripts can only be set in config file startCmdArgs.Provision = current.Provision // use current settings for unchanged configs // otherwise may be reverted to their default values. if !cmd.Flag("arch").Changed { startCmdArgs.Arch = current.Arch } if !cmd.Flag("disk").Changed { startCmdArgs.Disk = current.Disk } if !cmd.Flag("root-disk").Changed { if current.RootDisk > 0 { startCmdArgs.RootDisk = current.RootDisk } } if !cmd.Flag("kubernetes").Changed { startCmdArgs.Kubernetes.Enabled = current.Kubernetes.Enabled } if !cmd.Flag("kubernetes-version").Changed && current.Kubernetes.Version != "" { startCmdArgs.Kubernetes.Version = current.Kubernetes.Version } if !cmd.Flag("k3s-arg").Changed && current.Kubernetes.K3sArgs != nil { startCmdArgs.Kubernetes.K3sArgs = current.Kubernetes.K3sArgs } if !cmd.Flag("k3s-listen-port").Changed && current.Kubernetes.Port > 0 { startCmdArgs.Kubernetes.Port = current.Kubernetes.Port } if !cmd.Flag("runtime").Changed { startCmdArgs.Runtime = current.Runtime } if util.MacOS13OrNewerOnArm() { if !cmd.Flag("model-runner").Changed { startCmdArgs.ModelRunner = current.ModelRunner } } if !cmd.Flag("cpus").Changed { startCmdArgs.CPU = current.CPU } if !cmd.Flag("cpu-type").Changed { startCmdArgs.CPUType = current.CPUType } if !cmd.Flag("memory").Changed { startCmdArgs.Memory = current.Memory } if !cmd.Flag("mount").Changed { startCmdArgs.Mounts = current.Mounts } if !cmd.Flag("mount-type").Changed { startCmdArgs.MountType = current.MountType } if !cmd.Flag("mount-inotify").Changed { startCmdArgs.MountINotify = current.MountINotify } if !cmd.Flag("ssh-agent").Changed { startCmdArgs.ForwardAgent = current.ForwardAgent } if !cmd.Flag("ssh-config").Changed { startCmdArgs.SSHConfig = current.SSHConfig } if !cmd.Flag("ssh-port").Changed { startCmdArgs.SSHPort = current.SSHPort } if !cmd.Flag("port-forwarder").Changed { startCmdArgs.PortForwarder = current.PortForwarder } if !cmd.Flag("dns").Changed { startCmdArgs.Network.DNSResolvers = current.Network.DNSResolvers } if !cmd.Flag("dns-host").Changed { startCmdArgs.Network.DNSHosts = current.Network.DNSHosts } if !cmd.Flag("gateway-address").Changed { startCmdArgs.Network.GatewayAddress = current.Network.GatewayAddress } if !cmd.Flag("env").Changed { startCmdArgs.Env = current.Env } if !cmd.Flag("hostname").Changed { startCmdArgs.Hostname = current.Hostname } if !cmd.Flag("activate").Changed { if current.ActivateRuntime != nil { // backward compatibility for `activate` startCmdArgs.ActivateRuntime = current.ActivateRuntime } } if !cmd.Flag("binfmt").Changed { if current.Binfmt != nil { startCmdArgs.Binfmt = current.Binfmt } } if !cmd.Flag("network-host-addresses").Changed { startCmdArgs.Network.HostAddresses = current.Network.HostAddresses } if util.MacOS() { if !cmd.Flag("network-address").Changed { startCmdArgs.Network.Address = current.Network.Address } if !cmd.Flag("network-mode").Changed { startCmdArgs.Network.Mode = current.Network.Mode } if !cmd.Flag("network-interface").Changed { startCmdArgs.Network.BridgeInterface = current.Network.BridgeInterface } if !cmd.Flag("network-preferred-route").Changed { startCmdArgs.Network.PreferredRoute = current.Network.PreferredRoute } if util.MacOS13OrNewer() { if !cmd.Flag("vm-type").Changed { startCmdArgs.VMType = current.VMType } } if util.MacOS13OrNewerOnArm() { if !cmd.Flag("vz-rosetta").Changed { startCmdArgs.VZRosetta = current.VZRosetta } } if util.MacOSNestedVirtualizationSupported() { if !cmd.Flag("nested-virtualization").Changed { startCmdArgs.NestedVirtualization = current.NestedVirtualization } } } setFixedConfigs(&startCmdArgs.Config) } // editConfigFile launches an editor to edit the config file. func editConfigFile() (config.Config, error) { var c config.Config // preserve the current file in case the user terminates currentFile, err := os.ReadFile(config.CurrentProfile().File()) if err != nil { return c, fmt.Errorf("error reading config file: %w", err) } // prepend the config file with termination instruction abort, err := embedded.ReadString("defaults/abort.yaml") if err != nil { log.Warnln(fmt.Errorf("unable to read embedded file: %w", err)) } tmpFile, err := waitForUserEdit(startCmdArgs.Flags.Editor, []byte(abort+"\n"+string(currentFile))) if err != nil { return c, fmt.Errorf("error editing config file: %w", err) } // if file is empty, abort if tmpFile == "" { return c, fmt.Errorf("empty file, startup aborted") } defer func() { _ = os.Remove(tmpFile) }() if startCmdArgs.Flags.SaveConfig { if err := configmanager.SaveFromFile(tmpFile); err != nil { return c, err } } return configmanager.LoadFrom(tmpFile) } func start(app app.App, conf config.Config) error { if err := app.Start(conf); err != nil { return err } if startCmdArgs.Flags.Foreground { return awaitForInterruption(app) } return nil } func awaitForInterruption(app app.App) error { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) log.Println("keeping Colima in the foreground, press ctrl+c to exit...") sig := <-c log.Infof("interrupted by: %v", sig) if err := app.Stop(false); err != nil { log.Errorf("error stopping: %v", err) return err } return nil } ================================================ FILE: cmd/start_test.go ================================================ package cmd import ( "reflect" "strconv" "testing" "github.com/abiosoft/colima/config" ) func Test_mountsFromFlag(t *testing.T) { tests := []struct { mounts []string want []config.Mount }{ { mounts: []string{ "~:w", }, want: []config.Mount{ {Location: "~", Writable: true}, }, }, { mounts: []string{ "~", }, want: []config.Mount{ {Location: "~"}, }, }, { mounts: []string{ "/home/users", "/home/another:w", "/tmp", }, want: []config.Mount{ {Location: "/home/users"}, {Location: "/home/another", Writable: true}, {Location: "/tmp"}, }, }, { mounts: []string{ "/home/users:/home/users", "/home/another:w", "/tmp:/users/tmp", "/tmp:/users/tmp:w", }, want: []config.Mount{ {Location: "/home/users", MountPoint: "/home/users"}, {Location: "/home/another", Writable: true}, {Location: "/tmp", MountPoint: "/users/tmp"}, {Location: "/tmp", MountPoint: "/users/tmp", Writable: true}, }, }, { mounts: []string{ "none", }, want: nil, }, } for i, tt := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { if got := mountsFromFlag(tt.mounts); !reflect.DeepEqual(got, tt.want) { t.Errorf("mountsFromFlag() = %+v, want %+v", got, tt.want) } }) } } ================================================ FILE: cmd/status.go ================================================ package cmd import ( "github.com/abiosoft/colima/cmd/root" "github.com/spf13/cobra" ) var statusCmdArgs struct { extended bool json bool } // statusCmd represents the status command var statusCmd = &cobra.Command{ Use: "status [profile]", Short: "show the status of Colima", Long: `Show the status of Colima`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return newApp().Status(statusCmdArgs.extended, statusCmdArgs.json) }, } func init() { root.Cmd().AddCommand(statusCmd) statusCmd.Flags().BoolVarP(&statusCmdArgs.extended, "extended", "e", false, "include additional details") statusCmd.Flags().BoolVarP(&statusCmdArgs.json, "json", "j", false, "print json output") } ================================================ FILE: cmd/stop.go ================================================ package cmd import ( "github.com/abiosoft/colima/cmd/root" "github.com/spf13/cobra" ) var stopCmdArgs struct { force bool } // stopCmd represents the stop command var stopCmd = &cobra.Command{ Use: "stop [profile]", Short: "stop Colima", Long: `Stop Colima to free up resources. The state of the VM is persisted at stop. A start afterwards should return it back to its previous state.`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return newApp().Stop(stopCmdArgs.force) }, } func init() { root.Cmd().AddCommand(stopCmd) stopCmd.Flags().BoolVarP(&stopCmdArgs.force, "force", "f", false, "stop without graceful shutdown") } ================================================ FILE: cmd/template.go ================================================ package cmd import ( "fmt" "log" "os" "path/filepath" "github.com/abiosoft/colima/cmd/root" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/config/configmanager" "github.com/abiosoft/colima/embedded" "github.com/spf13/cobra" ) // templateCmd represents the template command var templateCmd = &cobra.Command{ Use: "template", Aliases: []string{"tmpl", "tpl", "t"}, Short: "edit the template for default configurations", Long: `Edit the template for default configurations of new instances. `, RunE: func(cmd *cobra.Command, args []string) error { if templateCmdArgs.Print { fmt.Println(templateFile()) return nil } // there are unwarranted []byte to string overheads. // not a big deal in this case abort, err := embedded.ReadString("defaults/abort.yaml") if err != nil { return fmt.Errorf("error reading embedded file: %w", err) } info, err := embedded.ReadString("defaults/template.yaml") if err != nil { return fmt.Errorf("error reading embedded file: %w", err) } template, err := templateFileOrDefault() if err != nil { return fmt.Errorf("error reading template file: %w", err) } tmpFile, err := waitForUserEdit(templateCmdArgs.Editor, []byte(abort+"\n"+info+"\n"+template)) if err != nil { return fmt.Errorf("error editing template file: %w", err) } if tmpFile == "" { return fmt.Errorf("empty file, template edit aborted") } defer func() { _ = os.Remove(tmpFile) }() // load and resave template to ensure the format is correct cf, err := configmanager.LoadFrom(tmpFile) if err != nil { return fmt.Errorf("error in template: %w", err) } if err := configmanager.SaveToFile(cf, templateFile()); err != nil { return fmt.Errorf("error saving template: %w", err) } log.Println("configurations template saved") return nil }, } func templateFile() string { return filepath.Join(config.TemplatesDir(), "default.yaml") } func templateFileOrDefault() (string, error) { tFile := templateFile() if _, err := os.Stat(tFile); err == nil { b, err := os.ReadFile(tFile) if err == nil { return string(b), nil } } return embedded.ReadString("defaults/colima.yaml") } var templateCmdArgs struct { Editor string Print bool } func init() { root.Cmd().AddCommand(templateCmd) templateCmd.Flags().StringVar(&templateCmdArgs.Editor, "editor", "", `editor to use for edit e.g. vim, nano, code (default "$EDITOR" env var)`) templateCmd.Flags().BoolVar(&templateCmdArgs.Print, "print", false, `print out the configuration file path, without editing`) } ================================================ FILE: cmd/update.go ================================================ package cmd import ( "github.com/abiosoft/colima/cmd/root" "github.com/spf13/cobra" ) // statusCmd represents the status command var updateCmd = &cobra.Command{ Use: "update [profile]", Aliases: []string{"u", "up"}, Short: "update the container runtime", Long: `Update the current container runtime.`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return newApp().Update() }, } func init() { root.Cmd().AddCommand(updateCmd) } ================================================ FILE: cmd/util.go ================================================ package cmd import ( "bytes" "fmt" "log" "os" "os/exec" "strconv" "strings" "github.com/abiosoft/colima/app" "github.com/abiosoft/colima/cli" "github.com/sirupsen/logrus" ) func newApp() app.App { colimaApp, err := app.New() if err != nil { logrus.Fatal("Error: ", err) } return colimaApp } // waitForUserEdit launches a temporary file with content using editor, // and waits for the user to close the editor. // It returns the filename (if saved), empty file name (if aborted), and an error (if any). func waitForUserEdit(editor string, content []byte) (string, error) { tmp, err := os.CreateTemp("", "colima-*.yaml") if err != nil { return "", fmt.Errorf("error creating temporary file: %w", err) } if _, err := tmp.Write(content); err != nil { return "", fmt.Errorf("error writing temporary file: %w", err) } if err := tmp.Close(); err != nil { return "", fmt.Errorf("error closing temporary file: %w", err) } if err := launchEditor(editor, tmp.Name()); err != nil { return "", err } // aborted if f, err := os.ReadFile(tmp.Name()); err == nil && len(bytes.TrimSpace(f)) == 0 { return "", nil } return tmp.Name(), nil } var editors = []string{ "vim", "code --wait --new-window", "nano", } func launchEditor(editor string, file string) error { if editor != "" { log.Println("editing in", editor) } // if not specified, prefer vscode if this a vscode terminal if editor == "" { if os.Getenv("TERM_PROGRAM") == "vscode" { log.Println("vscode detected, editing in vscode") editor = "code --wait" } } // if not found, check the EDITOR env var if editor == "" { if e := os.Getenv("EDITOR"); e != "" { log.Println("editing in", e, "from", "$EDITOR environment variable") editor = e } } // if not found, check the preferred editors if editor == "" { for _, e := range editors { s := strings.Fields(e) if _, err := exec.LookPath(s[0]); err == nil { editor = e log.Println("editing in", e) break } } } // if still not found, abort if editor == "" { return fmt.Errorf("no editor found in $PATH, kindly set $EDITOR environment variable and try again") } // some editors need the wait flag, let us add it if the user has not. switch editor { case "code", "code-insiders", "code-oss", "codium", "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code": editor = strconv.Quote(editor) + " --wait --new-window" case "mate", "/Applications/TextMate 2.app/Contents/MacOS/mate", "/Applications/TextMate 2.app/Contents/MacOS/TextMate": editor = strconv.Quote(editor) + " --wait" } return cli.CommandInteractive("sh", "-c", editor+" "+file).Run() } ================================================ FILE: cmd/version.go ================================================ package cmd import ( "fmt" "github.com/abiosoft/colima/app" "github.com/abiosoft/colima/cmd/root" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/model" "github.com/abiosoft/colima/store" "github.com/spf13/cobra" ) // versionCmd represents the version command var versionCmd = &cobra.Command{ Use: "version [profile]", Short: "print the version of Colima", Long: `Print the version of Colima`, Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { version := config.AppVersion() fmt.Println(config.AppName, "version", version.Version) fmt.Println("git commit:", version.Revision) if colimaApp, err := app.New(); err == nil { _ = colimaApp.Version() // Show AI model runner version if provisioned s, _ := store.Load() if s.RamalamaProvisioned { if modelVersion := model.GetRamalamaVersion(); modelVersion != "" { fmt.Println() fmt.Println("AI model runner") fmt.Println("version:", modelVersion) } } } }, } func init() { root.Cmd().AddCommand(versionCmd) } ================================================ FILE: colima.nix ================================================ { pkgs ? import }: with pkgs; buildGo123Module { name = "colima"; pname = "colima"; src = ./.; nativeBuildInputs = [ installShellFiles makeWrapper git ]; vendorHash = "sha256-ZwgzKCOEhgKK2LNRLjnWP6qHI4f6OGORvt3CREJf55I="; CGO_ENABLED = 1; subPackages = [ "cmd/colima" ]; # `nix-build` has .git folder but `nix build` does not, this caters for both cases preConfigure = '' export VERSION="$(git describe --tags --always || echo nix-build-at-"$(date +%s)")" export REVISION="$(git rev-parse HEAD || echo nix-unknown)" ldflags="-X github.com/abiosoft/colima/config.appVersion=$VERSION -X github.com/abiosoft/colima/config.revision=$REVISION" ''; postInstall = '' wrapProgram $out/bin/colima \ --prefix PATH : ${lib.makeBinPath [ qemu lima ]} installShellCompletion --cmd colima \ --bash <($out/bin/colima completion bash) \ --fish <($out/bin/colima completion fish) \ --zsh <($out/bin/colima completion zsh) ''; } ================================================ FILE: config/config.go ================================================ package config import ( "fmt" "net" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/osutil" ) const ( AppName = "colima" envProfile = "COLIMA_PROFILE" // environment variable for profile name ) // VersionInfo is the application version info. type VersionInfo struct { Version string Revision string } func AppVersion() VersionInfo { return VersionInfo{Version: appVersion, Revision: revision} } func EnvProfile() string { return osutil.EnvVar(envProfile).Val() } var ( appVersion = "development" revision = "unknown" ) // Config is the application config. type Config struct { CPU int `yaml:"cpu,omitempty"` Disk int `yaml:"disk,omitempty"` RootDisk int `yaml:"rootDisk,omitempty"` Memory float32 `yaml:"memory,omitempty"` Arch string `yaml:"arch,omitempty"` CPUType string `yaml:"cpuType,omitempty"` Network Network `yaml:"network,omitempty"` Env map[string]string `yaml:"env,omitempty"` // environment variables Hostname string `yaml:"hostname"` // SSH SSHPort int `yaml:"sshPort,omitempty"` ForwardAgent bool `yaml:"forwardAgent,omitempty"` SSHConfig bool `yaml:"sshConfig,omitempty"` // config generation // VM VMType string `yaml:"vmType,omitempty"` VZRosetta bool `yaml:"rosetta,omitempty"` Binfmt *bool `yaml:"binfmt,omitempty"` NestedVirtualization bool `yaml:"nestedVirtualization,omitempty"` DiskImage string `yaml:"diskImage,omitempty"` PortForwarder string `yaml:"portForwarder,omitempty"` // "ssh", "grpc" // volume mounts Mounts []Mount `yaml:"mounts,omitempty"` MountType string `yaml:"mountType,omitempty"` MountINotify bool `yaml:"mountInotify,omitempty"` // Runtime is one of docker, containerd. Runtime string `yaml:"runtime,omitempty"` ActivateRuntime *bool `yaml:"autoActivate,omitempty"` // ModelRunner is the AI model runner (docker, ramalama). ModelRunner string `yaml:"modelRunner,omitempty"` // Kubernetes configuration Kubernetes Kubernetes `yaml:"kubernetes,omitempty"` // Docker configuration Docker map[string]any `yaml:"docker,omitempty"` // provision scripts Provision []Provision `yaml:"provision,omitempty"` } // Kubernetes is kubernetes configuration type Kubernetes struct { Enabled bool `yaml:"enabled"` Version string `yaml:"version"` K3sArgs []string `yaml:"k3sArgs"` Port int `yaml:"port,omitempty"` } // Network is VM network configuration type Network struct { Address bool `yaml:"address"` DNSResolvers []net.IP `yaml:"dns"` DNSHosts map[string]string `yaml:"dnsHosts"` HostAddresses bool `yaml:"hostAddresses"` Mode string `yaml:"mode"` // shared, bridged BridgeInterface string `yaml:"interface"` PreferredRoute bool `yaml:"preferredRoute"` GatewayAddress net.IP `yaml:"gatewayAddress"` } // Mount is volume mount type Mount struct { Location string `yaml:"location"` MountPoint string `yaml:"mountPoint,omitempty"` Writable bool `yaml:"writable"` } // Provision modes managed by Colima (not passed to Lima). const ( ProvisionModeAfterBoot = "after-boot" ProvisionModeReady = "ready" ) type Provision struct { Mode string `yaml:"mode"` Script string `yaml:"script"` } // IsColimaMode returns true if the provision script is managed by Colima // rather than being passed to Lima. func (p Provision) IsColimaMode() bool { return p.Mode == ProvisionModeAfterBoot || p.Mode == ProvisionModeReady } func (c Config) MountsOrDefault() []Mount { // explicit empty list means mount home directory (matches yaml.go) if c.Mounts != nil && len(c.Mounts) == 0 { return []Mount{ {Location: util.HomeDir(), Writable: true}, } } // nil means no mounts, non-empty means user-specified mounts return c.Mounts } // AutoActivate returns if auto-activation of host client config is enabled. func (c Config) AutoActivate() bool { if c.ActivateRuntime == nil { return true } return *c.ActivateRuntime } // Empty checks if the configuration is empty. func (c Config) Empty() bool { return c.Runtime == "" } // this may be better but not really needed. func (c Config) DriverLabel() string { if util.MacOS13OrNewer() && c.VMType == "vz" { return "macOS Virtualization.Framework" } else if util.MacOS13OrNewerOnArm() && c.VMType == "krunkit" { return "Krunkit" } return "QEMU" } // Disk is an instance disk size type Disk int // GiB returns the string represent of the disk in GiB. func (d Disk) GiB() string { return fmt.Sprintf("%dGiB", d) } // Int returns the disk size in bytes. func (d Disk) Int() int64 { return 1024 * 1024 * 1024 * int64(d) } // CtxKey returns the context key for config. func CtxKey() any { return struct{ name string }{name: "colima_config"} } ================================================ FILE: config/configmanager/configmanager.go ================================================ package configmanager import ( "fmt" "net" "os" "strings" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/yamlutil" "gopkg.in/yaml.v3" ) // Save saves the config. func Save(c config.Config) error { return yamlutil.Save(c, config.CurrentProfile().File()) } // SaveFromFile loads configuration from file and save as config. func SaveFromFile(file string) error { c, err := LoadFrom(file) if err != nil { return err } return Save(c) } // SaveToFile saves configuration to file. func SaveToFile(c config.Config, file string) error { return yamlutil.Save(c, file) } // LoadFrom loads config from file. func LoadFrom(file string) (config.Config, error) { var c config.Config b, err := os.ReadFile(file) if err != nil { return c, fmt.Errorf("could not load config from file: %w", err) } err = yaml.Unmarshal(b, &c) if err != nil { return c, fmt.Errorf("could not load config from file: %w", err) } return c, nil } // ValidateConfig validates config before we use it func ValidateConfig(c config.Config) error { validMountTypes := map[string]bool{"9p": true, "sshfs": true} validPortForwarders := map[string]bool{"grpc": true, "ssh": true, "none": true} if util.MacOS13OrNewer() { validMountTypes["virtiofs"] = true } if _, ok := validMountTypes[c.MountType]; !ok { return fmt.Errorf("invalid mountType: '%s'", c.MountType) } validVMTypes := map[string]bool{"qemu": true} if util.MacOS13OrNewer() { validVMTypes["vz"] = true } if util.MacOS13OrNewerOnArm() { validVMTypes["krunkit"] = true } if c.VMType == "krunkit" && !util.MacOS13OrNewerOnArm() { return fmt.Errorf("vmType 'krunkit' is only available on macOS with Apple Silicon") } if _, ok := validVMTypes[c.VMType]; !ok { return fmt.Errorf("invalid vmType: '%s'", c.VMType) } if c.VMType == "qemu" { if err := util.AssertQemuImg(); err != nil { return fmt.Errorf("cannot use vmType: '%s', error: %w", c.VMType, err) } } if c.VMType == "krunkit" { if err := util.AssertKrunkit(); err != nil { return fmt.Errorf("cannot use vmType: '%s', error: %w", c.VMType, err) } } if c.DiskImage != "" { if strings.HasPrefix(c.DiskImage, "http://") || strings.HasPrefix(c.DiskImage, "https://") { return fmt.Errorf("cannot use diskImage: remote URLs not supported, only local files can be specified") } } if _, ok := validPortForwarders[c.PortForwarder]; !ok { return fmt.Errorf("invalid port forwarder: '%s'", c.PortForwarder) } if c.Network.GatewayAddress != nil { if err := validateGatewayAddress(c.Network.GatewayAddress); err != nil { return err } } return nil } // Load loads the config. // Error is only returned if the config file exists but could not be loaded. // No error is returned if the config file does not exist. func Load() (c config.Config, err error) { f := config.CurrentProfile().File() if _, err := os.Stat(f); err != nil { return c, nil } return LoadFrom(f) } // LoadInstance is like Load but returns the config of the currently running instance. func LoadInstance() (config.Config, error) { return LoadFrom(config.CurrentProfile().StateFile()) } // Teardown deletes the config. func Teardown() error { dir := config.CurrentProfile().ConfigDir() if _, err := os.Stat(dir); err == nil { return os.RemoveAll(dir) } return nil } // Validates that gateway is a valid IPv4 address and that the last octet is “2”. // Lima uses the last octet as 2 for gateways. func validateGatewayAddress(gateway net.IP) error { ip4 := gateway.To4() if ip4 == nil { return fmt.Errorf("gateway %q is not IPv4", gateway) } // Check last octet if ip4[3] != 2 { return fmt.Errorf("the last octet of gateway %q is not 2", gateway) } return nil } ================================================ FILE: config/files.go ================================================ package config import ( "fmt" "os" "path/filepath" "sync" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/fsutil" "github.com/sirupsen/logrus" ) // requiredDir is a directory that must exist on the filesystem type requiredDir struct { once sync.Once // dir is a func to enable deferring the value of the directory // until execution time. // if dir() returns an error, a fatal error is triggered. dir func() (string, error) computedDir *string } // Dir returns the directory path. // It ensures the directory is created on the filesystem by calling // `mkdir` prior to returning the directory path. func (r *requiredDir) Dir() string { if r.computedDir != nil { return *r.computedDir } dir, err := r.dir() if err != nil { logrus.Fatal(fmt.Errorf("cannot fetch required directory: %w", err)) } r.once.Do(func() { if err := fsutil.MkdirAll(dir, 0755); err != nil { logrus.Fatal(fmt.Errorf("cannot make required directory: %w", err)) } }) r.computedDir = &dir return dir } var ( configBaseDir = requiredDir{ dir: func() (string, error) { // colima home explicit config dir := os.Getenv("COLIMA_HOME") if _, err := os.Stat(dir); err == nil { return dir, nil } // user home directory homeDir, err := os.UserHomeDir() if err != nil { return "", err } // colima's config directory based on home directory dir = filepath.Join(homeDir, ".colima") // validate existence of colima's config directory _, err = os.Stat(dir) // extra xdg config directory xdgDir, xdg := os.LookupEnv("XDG_CONFIG_HOME") if err == nil { // ~/.colima is found but xdg dir is set if xdg { logrus.Warnln("found ~/.colima, ignoring $XDG_CONFIG_HOME...") logrus.Warnln("delete ~/.colima to use $XDG_CONFIG_HOME as config directory") logrus.Warnf("or run `mv ~/.colima \"%s\"`", filepath.Join(xdgDir, "colima")) } return dir, nil } else { // ~/.colima is missing and xdg dir is set if xdg { return filepath.Join(xdgDir, "colima"), nil } } // macOS users are accustomed to ~/.colima if util.MacOS() { return dir, nil } // other environments fall back to user config directory dir, err = os.UserConfigDir() if err != nil { return "", err } return filepath.Join(dir, "colima"), nil }, } cacheDir = requiredDir{ dir: func() (string, error) { if dir := os.Getenv("COLIMA_CACHE_HOME"); dir != "" { return dir, nil } if dir := os.Getenv("XDG_CACHE_HOME"); dir != "" { return filepath.Join(dir, "colima"), nil } // else dir, err := os.UserCacheDir() if err != nil { return "", err } return filepath.Join(dir, "colima"), nil }, } templatesDir = requiredDir{ dir: func() (string, error) { dir, err := configBaseDir.dir() if err != nil { return "", err } return filepath.Join(dir, "_templates"), nil }, } limaDir = requiredDir{ dir: func() (string, error) { // if LIMA_HOME env var is set, obey it. if dir := os.Getenv("LIMA_HOME"); dir != "" { return dir, nil } dir, err := configBaseDir.dir() if err != nil { return "", err } return filepath.Join(dir, "_lima"), nil }, } storeDir = requiredDir{ dir: func() (string, error) { dir, err := configBaseDir.dir() if err != nil { return "", err } return filepath.Join(dir, "_store"), nil }, } ) // CacheDir returns the cache directory. func CacheDir() string { return cacheDir.Dir() } // TemplatesDir returns the templates' directory. func TemplatesDir() string { return templatesDir.Dir() } // LimaDir returns Lima directory. func LimaDir() string { return limaDir.Dir() } const configFileName = "colima.yaml" // SSHConfigFile returns the path to generated ssh config. func SSHConfigFile() string { return filepath.Join(configBaseDir.Dir(), "ssh_config") } ================================================ FILE: config/profile.go ================================================ package config import ( "path/filepath" "strings" ) var profile = &Profile{ID: AppName, DisplayName: AppName, ShortName: "default"} // SetProfile sets the profile name for the application. // This is an avenue to test Colima without breaking an existing stable setup. // Not perfect, but good enough for testing. func SetProfile(profileName string) { profile = ProfileFromName(profileName) profile.Changed = true } // ProfileFromName retrieves profile given name. func ProfileFromName(name string) *Profile { var i Profile switch name { case "", AppName, "default": i.ID = AppName i.DisplayName = AppName i.ShortName = "default" return &i } // sanitize name = strings.TrimPrefix(name, "colima-") // if custom profile is specified, // use a prefix to prevent possible name clashes i.ID = "colima-" + name i.DisplayName = "colima [profile=" + name + "]" i.ShortName = name return &i } // CurrentProfile returns the current running profile. func CurrentProfile() *Profile { return profile } // Profile is colima profile. type Profile struct { ID string DisplayName string ShortName string Changed bool // indicates if the profile has been changed configDir *requiredDir } // ConfigDir returns the configuration directory. func (p *Profile) ConfigDir() string { if p.configDir == nil { p.configDir = &requiredDir{ dir: func() (string, error) { return filepath.Join(configBaseDir.Dir(), p.ShortName), nil }, } } return p.configDir.Dir() } // LimaInstanceDir returns the directory for the Lima instance. func (p *Profile) LimaInstanceDir() string { return filepath.Join(limaDir.Dir(), p.ID) } // File returns the path to the config file. func (p *Profile) File() string { return filepath.Join(p.ConfigDir(), configFileName) } // LimaFile returns the path to the lima config file. func (p *Profile) LimaFile() string { return filepath.Join(p.LimaInstanceDir(), "lima.yaml") } // StateFile returns the path to the state file. func (p *Profile) StateFile() string { return filepath.Join(p.LimaInstanceDir(), configFileName) } func (p *Profile) StoreFile() string { return filepath.Join(storeDir.Dir(), p.ID+".json") } var _ ProfileInfo = (*Profile)(nil) // ProfileInfo is the information about a profile. type ProfileInfo interface { // ConfigDir returns the configuration directory. ConfigDir() string // LimaInstanceDir returns the directory for the Lima instance. LimaInstanceDir() string // File returns the path to the config file. File() string // LimaFile returns the path to the lima config file. LimaFile() string // StateFile returns the path to the state file. StateFile() string // StoreFile returns the path to the store file. StoreFile() string } ================================================ FILE: core/core.go ================================================ package core import ( "bytes" "encoding/json" "fmt" "strings" "github.com/sirupsen/logrus" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/environment" "github.com/coreos/go-semver/semver" ) const limaVersion = "v0.18.0" // minimum Lima version supported type ( hostActions = environment.HostActions guestActions = environment.GuestActions ) // SetupBinfmt downloads and install binfmt func SetupBinfmt(host hostActions, guest guestActions, arch environment.Arch) error { qemuArch := environment.AARCH64 if arch.Value().GoArch() == "arm64" { qemuArch = environment.X8664 } install := func() error { if err := guest.Run("sh", "-c", "sudo QEMU_PRESERVE_ARGV0=1 /usr/bin/binfmt --install 386,"+qemuArch.GoArch()); err != nil { return fmt.Errorf("error installing binfmt: %w", err) } return nil } // validate binfmt if err := guest.RunQuiet("command", "-v", "binfmt"); err != nil { return fmt.Errorf("binfmt not found: %w", err) } return install() } // LimaVersionSupported checks if the currently installed Lima version is supported. func LimaVersionSupported() error { var values struct { Version string `json:"version"` } var buf bytes.Buffer cmd := cli.Command("limactl", "info") cmd.Stdout = &buf if err := cmd.Run(); err != nil { return fmt.Errorf("error checking Lima version: %w", err) } if err := json.NewDecoder(&buf).Decode(&values); err != nil { return fmt.Errorf("error decoding 'limactl info' json: %w", err) } // remove pre-release hyphen parts := strings.SplitN(values.Version, "-", 2) if len(parts) > 0 { values.Version = parts[0] } if parts[0] == "HEAD" { logrus.Warnf("to avoid compatibility issues, ensure lima development version (%s) in use is not lower than %s", values.Version, limaVersion) return nil } min := semver.New(strings.TrimPrefix(limaVersion, "v")) current, err := semver.NewVersion(strings.TrimPrefix(values.Version, "v")) if err != nil { return fmt.Errorf("invalid semver version for Lima: %w", err) } if min.Compare(*current) > 0 { return fmt.Errorf("minimum Lima version supported is %s, current version is %s", limaVersion, values.Version) } return nil } ================================================ FILE: daemon/daemon.go ================================================ package daemon import ( "context" "fmt" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/daemon/process" "github.com/abiosoft/colima/daemon/process/inotify" "github.com/abiosoft/colima/daemon/process/vmnet" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/fsutil" "github.com/abiosoft/colima/util/osutil" ) // Manager handles running background processes. type Manager interface { Start(context.Context, config.Config) error Stop(context.Context, config.Config) error Running(context.Context, config.Config) (Status, error) Dependency(ctx context.Context, conf config.Config, name string) (deps process.Dependency, root bool) } type Status struct { // Parent process Running bool // Subprocesses Processes []processStatus } type processStatus struct { Name string Running bool Error error } // NewManager creates a new process manager. func NewManager(host environment.HostActions) Manager { return &processManager{ host: host, } } func CtxKey(s string) any { return struct{ key string }{key: s} } var _ Manager = (*processManager)(nil) type processManager struct { host environment.HostActions } func (l processManager) Dependency(ctx context.Context, conf config.Config, name string) (deps process.Dependency, root bool) { processes := processesFromConfig(conf) for _, p := range processes { if p.Name() == name { return process.Dependencies(p) } } return process.Dependencies() } func (l processManager) init() error { // dependencies for network if err := fsutil.MkdirAll(process.Dir(), 0755); err != nil { return fmt.Errorf("error preparing vmnet: %w", err) } return nil } func (l processManager) Running(ctx context.Context, conf config.Config) (s Status, err error) { err = l.host.RunQuiet(osutil.Executable(), "daemon", "status", config.CurrentProfile().ShortName) if err != nil { return } s.Running = true ctx = context.WithValue(ctx, process.CtxKeyDaemon(), s.Running) for _, p := range processesFromConfig(conf) { pErr := p.Alive(ctx) s.Processes = append(s.Processes, processStatus{ Name: p.Name(), Running: pErr == nil, Error: pErr, }) } return } func (l processManager) Start(ctx context.Context, conf config.Config) error { _ = l.Stop(ctx, conf) // this is safe, nothing is done when not running if err := l.init(); err != nil { return fmt.Errorf("error preparing daemon directory: %w", err) } args := []string{osutil.Executable(), "daemon", "start", config.CurrentProfile().ShortName} if conf.Network.Address { args = append(args, "--vmnet") args = append(args, "--vmnet-mode", conf.Network.Mode) args = append(args, "--vmnet-interface", conf.Network.BridgeInterface) } if conf.MountINotify { args = append(args, "--inotify") args = append(args, "--inotify-runtime", conf.Runtime) for _, mount := range conf.MountsOrDefault() { p, err := util.CleanPath(mount.Location) if err != nil { return fmt.Errorf("error sanitising mount path for inotify: %w", err) } args = append(args, "--inotify-dir", p) } } if cli.Settings.Verbose { args = append(args, "--very-verbose") } host := l.host.WithDir(util.HomeDir()) return host.RunQuiet(args...) } func (l processManager) Stop(ctx context.Context, conf config.Config) error { if s, err := l.Running(ctx, conf); err != nil || !s.Running { return nil } return l.host.RunQuiet(osutil.Executable(), "daemon", "stop", config.CurrentProfile().ShortName) } func processesFromConfig(conf config.Config) []process.Process { var processes []process.Process if conf.Network.Address { processes = append(processes, vmnet.New(conf.Network.Mode, conf.Network.BridgeInterface)) } if conf.MountINotify { processes = append(processes, inotify.New()) } return processes } ================================================ FILE: daemon/process/inotify/events.go ================================================ package inotify import ( "context" "fmt" "io/fs" "time" ) type modEvent struct { path string // filename fs.FileMode } func (m modEvent) Mode() string { return fmt.Sprintf("%o", m.FileMode) } func (f *inotifyProcess) handleEvents(ctx context.Context, watcher dirWatcher) error { log := f.log log.Trace("begin inotify event handler") mod := make(chan modEvent) vols := make(chan []string) if err := f.monitorContainerVolumes(ctx, vols); err != nil { return fmt.Errorf("error watching container volumes: %w", err) } var last time.Time var cancelWatch context.CancelFunc var currentVols []string volsChanged := func(vols []string) bool { if len(currentVols) != len(vols) { return true } for i := range vols { if vols[i] != currentVols[i] { return true } } return false } cache := map[string]struct{}{} for { select { // exit signal case <-ctx.Done(): close(mod) return ctx.Err() // watch only container volumes case vols := <-vols: if !volsChanged(vols) { continue } log.Tracef("volumes changed from: %+v, to: %+v", currentVols, vols) currentVols = vols if cancel := cancelWatch; cancel != nil { // delay a bit to avoid zero downtime time.AfterFunc(time.Second*1, cancel) } ctx, cancel := context.WithCancel(ctx) cancelWatch = cancel go func(ctx context.Context, vols []string, mod chan<- modEvent) { if err := watcher.Watch(ctx, vols, mod); err != nil { log.Error(fmt.Errorf("error running watcher: %w", err)) } }(ctx, vols, mod) // handle modification events case ev := <-mod: now := time.Now() // rate limit, handle at most 50 unique items every 500 ms if now.Sub(last) < time.Millisecond*500 { if _, ok := cache[ev.path]; ok { continue // handled, ignore } if len(cache) > 50 { continue } } else { last = now cache = map[string]struct{}{} // >500ms, reset unique cache } // cache current event cache[ev.path] = struct{}{} // validate that file exists if err := f.guest.RunQuiet("stat", ev.path); err != nil { log.Trace(fmt.Errorf("cannot stat '%s': %w", ev.path, err)) continue } log.Infof("syncing inotify event for %s ", ev.path) if err := f.guest.RunQuiet("sudo", "/bin/chmod", ev.Mode(), ev.path); err != nil { log.Trace(fmt.Errorf("error syncing inotify event: %w", err)) } } } } ================================================ FILE: daemon/process/inotify/inotify.go ================================================ package inotify import ( "context" "fmt" "time" "github.com/abiosoft/colima/daemon/process" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/sirupsen/logrus" ) const Name = "inotify" const volumesInterval = 5 * time.Second type Args struct { environment.GuestActions Dirs []string Runtime string } func CtxKeyArgs() any { return struct{ name string }{name: "inotify_args"} } // New returns inotify process. func New() process.Process { return &inotifyProcess{ log: logrus.WithField("context", "inotify"), } } var _ process.Process = (*inotifyProcess)(nil) type inotifyProcess struct { vmVols []string guest environment.GuestActions runtime string log *logrus.Entry } // Alive implements process.Process func (f *inotifyProcess) Alive(ctx context.Context) error { daemonRunning, _ := ctx.Value(process.CtxKeyDaemon()).(bool) // if the parent is active, we can assume inotify is active. if daemonRunning { return nil } return fmt.Errorf("inotify not running") } // Dependencies implements process.Process func (*inotifyProcess) Dependencies() (deps []process.Dependency, root bool) { return nil, false } // Name implements process.Process func (*inotifyProcess) Name() string { return Name } // Start implements process.Process func (f *inotifyProcess) Start(ctx context.Context) error { args, ok := ctx.Value(CtxKeyArgs()).(Args) if !ok { return fmt.Errorf("args missing in context") } f.vmVols = omitChildrenDirectories(args.Dirs) f.guest = args.GuestActions f.runtime = args.Runtime log := f.log log.Info("waiting for VM to start") f.waitForLima(ctx) log.Info("VM started") watcher := &defaultWatcher{log: log} return f.handleEvents(ctx, watcher) } // waitForLima waits until lima starts and sets the directory to watch. func (f *inotifyProcess) waitForLima(ctx context.Context) { log := f.log // wait for Lima to finish starting for { log.Info("waiting 5 secs for VM") // 5 second interval after := time.After(time.Second * 5) select { case <-ctx.Done(): return case <-after: i, err := limautil.Instance() if err != nil || !i.Running() { continue } if err := f.guest.RunQuiet("uname", "-a"); err == nil { return } } } } ================================================ FILE: daemon/process/inotify/volumes.go ================================================ package inotify import ( "bytes" "context" "encoding/json" "fmt" "sort" "strings" "time" "github.com/abiosoft/colima/environment/container/containerd" "github.com/abiosoft/colima/environment/container/docker" ) func (f *inotifyProcess) monitorContainerVolumes(ctx context.Context, c chan<- []string) error { log := f.log if f.runtime == "" { return fmt.Errorf("empty runtime") } fetch := func() ([]string, error) { var vols []string switch f.runtime { case docker.Name: vols, err := f.fetchVolumes(docker.Name) if err != nil { return nil, fmt.Errorf("error fetching docker volumes: %w", err) } return vols, nil case containerd.Name: var namespaces []string out, err := f.guest.RunOutput("sudo", "nerdctl", "namespace", "list", "-q") if err != nil { return nil, fmt.Errorf("error retrieving containerd namespaces: %w", err) } if out != "" { namespaces = strings.Fields(out) } for _, ns := range namespaces { v, err := f.fetchVolumes("sudo", "nerdctl", "--namespace", ns) if err != nil { return nil, fmt.Errorf("error retrieving containerd volumes: %w", err) } if len(v) > 0 { vols = append(vols, v...) } } return vols, nil } return nil, nil } go func() { for { select { case <-ctx.Done(): log.Trace("stop signal received") err := ctx.Err() if err != nil { log.Trace(fmt.Errorf("error during stop: %w", err)) } case <-time.After(volumesInterval): if vols, err := fetch(); err != nil { log.Error(err) } else { c <- vols } } } }() return nil } func (f *inotifyProcess) fetchVolumes(cmdArgs ...string) ([]string, error) { log := f.log // fetch all containers var containers []string { args := append([]string{}, cmdArgs...) args = append(args, "ps", "-q") out, err := f.guest.RunOutput(args...) if err != nil { return nil, fmt.Errorf("error listing containers: %w", err) } containers = strings.Fields(out) if len(containers) == 0 { return nil, nil } } log.Tracef("found containers %+v", containers) // fetch volumes var resp []struct { Mounts []struct { Source string `json:"Source"` } `json:"Mounts"` } { args := append([]string{}, cmdArgs...) args = append(args, "inspect") args = append(args, containers...) var buf bytes.Buffer if err := f.guest.RunWith(nil, &buf, args...); err != nil { return nil, fmt.Errorf("error inspecting containers: %w", err) } if err := json.NewDecoder(&buf).Decode(&resp); err != nil { return nil, fmt.Errorf("error decoding docker response") } } // process and discard redundant volumes vols := []string{} { shouldMount := func(child string) bool { // ignore all invalid directories. // i.e. directories not within the mounted VM directories for _, parent := range f.vmVols { if strings.HasPrefix(child, parent) { return true } } return false } for _, r := range resp { for _, mount := range r.Mounts { if shouldMount(mount.Source) { vols = append(vols, mount.Source) } } } vols = omitChildrenDirectories(vols) log.Tracef("found volumes %+v", vols) } return vols, nil } func omitChildrenDirectories(dirs []string) []string { sort.Strings(dirs) // sort to put the parent directories first // keep track for uniqueness set := map[string]struct{}{} var newVols []string omitted := map[int]struct{}{} for i := 0; i < len(dirs); i++ { // if the index is omitted, skip if _, ok := omitted[i]; ok { continue } parent := dirs[i] if _, ok := set[parent]; !ok { newVols = append(newVols, parent) set[parent] = struct{}{} } for j := i + 1; j < len(dirs); j++ { child := dirs[j] if strings.HasPrefix(child, strings.TrimSuffix(parent, "/")+"/") { omitted[j] = struct{}{} } } } return newVols } ================================================ FILE: daemon/process/inotify/volumes_test.go ================================================ package inotify import ( "reflect" "strconv" "testing" ) func Test_omitChildrenDirectories(t *testing.T) { tests := []struct { args []string want []string }{ { args: []string{"/", "/user", "/user/someone", "/a", "/a/ee", "/a/bb"}, want: []string{"/"}, }, { args: []string{"/someone", "/user", "/user/someone", "/a", "/a/ee", "/a/bb", "/a"}, want: []string{"/a", "/someone", "/user"}, }, { args: []string{"/someone", "/user/colima/projects/myworks", "/user/colima/projects", "/user/colima/projects/myworks", "/user/colima/projects", "/someone"}, want: []string{"/someone", "/user/colima/projects"}, }, { args: []string{"/someone", "/user/colima/projects/myworks", "/user/colima/projects"}, want: []string{"/someone", "/user/colima/projects"}, }, { args: []string{"/user/colima/projects"}, want: []string{"/user/colima/projects"}, }, } for i, tt := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { if got := omitChildrenDirectories(tt.args); !reflect.DeepEqual(got, tt.want) { t.Errorf("omitChildrenDirectories() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: daemon/process/inotify/watch.go ================================================ package inotify import ( "context" "fmt" "os" "github.com/abiosoft/colima/util" "github.com/rjeczalik/notify" "github.com/sirupsen/logrus" ) type dirWatcher interface { // Watch watches directories recursively for changes and sends message via c on // modifications to files within the watched directories. // // Watch returns immediately and runs the watcher in the background. // An error is returned when the watcher can not be started in background. // // The watcher terminates on fatal error or when ctx is done. Watch(ctx context.Context, dirs []string, c chan<- modEvent) error } type defaultWatcher struct { log *logrus.Entry } // Watch implements dirWatcher func (d *defaultWatcher) Watch(ctx context.Context, dirs []string, mod chan<- modEvent) error { log := d.log c := make(chan notify.EventInfo, 1) for _, dir := range dirs { dir, err := util.CleanPath(dir) if err != nil { return fmt.Errorf("invalid directory: %w", err) } err = notify.Watch(dir+"...", c, notify.Write) if err != nil { return fmt.Errorf("error watching directory recursively '%s': %w", dir, err) } } go func(ctx context.Context, c chan notify.EventInfo, mod chan<- modEvent) { for { select { case <-ctx.Done(): notify.Stop(c) log.Trace("stopping watcher") if err := ctx.Err(); err != nil { log.Trace(fmt.Errorf("error found in ctx: %w", err)) return } case e := <-c: path := e.Path() log.Tracef("received event %s for %s", e.Event().String(), path) stat, err := os.Stat(path) if err != nil { log.Trace(fmt.Errorf("unable to stat inotify file '%s': %w", path, err)) continue } if stat.IsDir() { log.Tracef("'%s' is directory, ignoring.", path) continue } // send modification event mod <- modEvent{path: path, FileMode: stat.Mode()} } } }(ctx, c, mod) return nil } var _ dirWatcher = (*defaultWatcher)(nil) ================================================ FILE: daemon/process/process.go ================================================ package process import ( "context" "fmt" "path/filepath" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment" ) func CtxKeyDaemon() any { return struct{ key string }{key: "colima_daemon"} } // Process is a background process managed by the daemon. type Process interface { // Name for the background process Name() string // Start starts the background process. // The process is expected to terminate when ctx is done. Start(ctx context.Context) error // Alive checks if the process is the alive. Alive(ctx context.Context) error // Dependencies are requirements for start to succeed. // root should be true if root access is required for // installing any of the dependencies. Dependencies() (deps []Dependency, root bool) } // Dir is the directory for daemon files. func Dir() string { return filepath.Join(config.CurrentProfile().ConfigDir(), "daemon") } // Dependency is a requirement to be fulfilled before a process can be started. type Dependency interface { Installed() bool Install(environment.HostActions) error } // Dependencies returns the dependencies for the processes. // root returns if root access is required func Dependencies(processes ...Process) (deps Dependency, root bool) { // check rootful for user info message rootful := false for _, p := range processes { deps, root := p.Dependencies() for _, dep := range deps { if !dep.Installed() && root { rootful = true break } } } return processDeps(processes), rootful } type processDeps []Process func (p processDeps) Installed() bool { for _, process := range p { deps, _ := process.Dependencies() for _, d := range deps { if !d.Installed() { return false } } } return true } func (p processDeps) Install(host environment.HostActions) error { for _, process := range p { deps, _ := process.Dependencies() for _, d := range deps { if !d.Installed() { if err := d.Install(host); err != nil { return fmt.Errorf("error occurred installing dependencies for '%s': %w", process.Name(), err) } } } } return nil } ================================================ FILE: daemon/process/vmnet/deps.go ================================================ package vmnet import ( "fmt" "os" "path/filepath" "runtime" "github.com/abiosoft/colima/daemon/process" "github.com/abiosoft/colima/embedded" "github.com/abiosoft/colima/environment" ) var _ process.Dependency = sudoerFile{} type sudoerFile struct{} // Installed implements Dependency func (s sudoerFile) Installed() bool { return embedded.SudoersInstalled() } // Install implements Dependency func (s sudoerFile) Install(host environment.HostActions) error { return embedded.InstallSudoers(host) } var _ process.Dependency = vmnetFile{} const BinaryPath = "/opt/colima/bin/socket_vmnet" const ClientBinaryPath = "/opt/colima/bin/socket_vmnet_client" type vmnetFile struct{} // Installed implements Dependency func (v vmnetFile) Installed() bool { for _, bin := range v.bins() { if _, err := os.Stat(bin); err != nil { return false } } return true } func (v vmnetFile) bins() []string { return []string{BinaryPath, ClientBinaryPath} } func (v vmnetFile) Install(host environment.HostActions) error { arch := "x86_64" if runtime.GOARCH != "amd64" { arch = "arm64" } // read the embedded file gz, err := embedded.Read("network/vmnet_" + arch + ".tar.gz") if err != nil { return fmt.Errorf("error retrieving embedded vmnet file: %w", err) } // write tar to tmp directory f, err := os.CreateTemp("", "vmnet.tar.gz") if err != nil { return fmt.Errorf("error creating temp file: %w", err) } if _, err := f.Write(gz); err != nil { return fmt.Errorf("error writing temp file: %w", err) } _ = f.Close() // not a fatal error defer func() { _ = os.Remove(f.Name()) }() // extract tar to desired location dir := optDir if err := host.RunInteractive("sudo", "mkdir", "-p", dir); err != nil { return fmt.Errorf("error preparing colima privileged dir: %w", err) } if err := host.RunInteractive("sudo", "sh", "-c", fmt.Sprintf("cd %s && tar xfz %s 2>/dev/null", dir, f.Name())); err != nil { return fmt.Errorf("error extracting vmnet archive: %w", err) } return nil } var _ process.Dependency = vmnetRunDir{} type vmnetRunDir struct{} // Install implements Dependency func (v vmnetRunDir) Install(host environment.HostActions) error { return host.RunInteractive("sudo", "mkdir", "-p", runDir()) } // Installed implements Dependency func (v vmnetRunDir) Installed() bool { stat, err := os.Stat(runDir()) return err == nil && stat.IsDir() } const optDir = "/opt/colima" // runDir is the directory to the rootful daemon run related files. e.g. pid files func runDir() string { return filepath.Join(optDir, "run") } ================================================ FILE: daemon/process/vmnet/vmnet.go ================================================ package vmnet import ( "context" "fmt" "net" "os" "os/exec" "path/filepath" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/daemon/process" "github.com/abiosoft/colima/util/osutil" "github.com/sirupsen/logrus" ) const Name = "vmnet" const ( SubProcessEnvVar = "COLIMA_VMNET" NetGateway = "192.168.106.1" NetDHCPEnd = "192.168.106.254" ) var _ process.Process = (*vmnetProcess)(nil) func New(mode, netInterface string) process.Process { return &vmnetProcess{ mode: mode, netInterface: netInterface, } } type vmnetProcess struct { mode string netInterface string } func (*vmnetProcess) Alive(ctx context.Context) error { info := Info() pidFile := info.PidFile socketFile := info.Socket.File() if _, err := os.Stat(pidFile); err == nil { cmd := exec.CommandContext(ctx, "sudo", "/usr/bin/pkill", "-0", "-F", pidFile) if err := cmd.Run(); err != nil { return fmt.Errorf("error checking vmnet process: %w", err) } } if _, err := os.Stat(socketFile); err != nil { return fmt.Errorf("vmnet socket file not found error: %w", err) } if n, err := net.Dial("unix", socketFile); err != nil { return fmt.Errorf("vmnet socket file error: %w", err) } else { if err := n.Close(); err != nil { logrus.Debugln(fmt.Errorf("error closing ping socket connection: %w", err)) } } return nil } // Name implements process.BgProcess func (*vmnetProcess) Name() string { return Name } // Start implements process.BgProcess func (v *vmnetProcess) Start(ctx context.Context) error { info := Info() socket := info.Socket.File() pid := info.PidFile // delete existing sockets if exist // errors ignored on purpose _ = forceDeleteFileIfExists(socket) done := make(chan error, 1) go func() { // rootfully start the vmnet daemon var command *exec.Cmd if v.mode == "bridged" { command = cli.CommandInteractive("sudo", BinaryPath, "--vmnet-mode", "bridged", "--socket-group", "staff", "--vmnet-interface", v.netInterface, "--pidfile", pid, socket, ) } else { command = cli.CommandInteractive("sudo", BinaryPath, "--vmnet-mode", "shared", "--socket-group", "staff", "--vmnet-gateway", NetGateway, "--vmnet-dhcp-end", NetDHCPEnd, "--pidfile", pid, socket, ) } if cli.Settings.Verbose { command.Env = append(command.Env, os.Environ()...) command.Env = append(command.Env, "DEBUG=1") } done <- command.Run() }() select { case <-ctx.Done(): if err := stop(pid); err != nil { return fmt.Errorf("error stopping vmnet: %w", err) } case err := <-done: if err != nil { return fmt.Errorf("error running vmnet: %w", err) } } return nil } func (vmnetProcess) Dependencies() (deps []process.Dependency, root bool) { return []process.Dependency{ sudoerFile{}, vmnetFile{}, vmnetRunDir{}, }, true } func stop(pidFile string) error { // rootfully kill the vmnet process. // process is only assumed alive if the pidfile exists if _, err := os.Stat(pidFile); err == nil { if err := cli.CommandInteractive("sudo", "/usr/bin/pkill", "-F", pidFile).Run(); err != nil { return fmt.Errorf("error killing vmnet process: %w", err) } } return nil } func forceDeleteFileIfExists(name string) error { if stat, err := os.Stat(name); err == nil && !stat.IsDir() { return os.Remove(name) } return nil } func Info() struct { PidFile string Socket osutil.Socket } { return struct { PidFile string Socket osutil.Socket }{ PidFile: filepath.Join(runDir(), "vmnet-"+config.CurrentProfile().ShortName+".pid"), Socket: osutil.Socket(filepath.Join(process.Dir(), "vmnet.sock")), } } ================================================ FILE: default.nix ================================================ with import { }; callPackage (import ./colima.nix) { } ================================================ FILE: docs/CONTRIBUTE.md ================================================ # Contributing to Colima Thank you for your interest in contributing to Colima! ## Getting Started Colima is a Go project. To contribute, you will need Go installed (see the [Go installation guide](https://golang.org/doc/install)). ### 1. Fork the Repository First, fork the Colima repository on GitHub. Then, clone your fork locally: ```sh git clone https://github.com//colima.git cd colima ``` ### 2. Create a New Branch Create a new branch for your changes: ```sh git checkout -b my-feature-branch ``` ### 3. Commit Your Changes Commit your changes with a DCO signoff and the required commit message format. Each commit must include a signoff line: ``` Signed-off-by: Your Name ``` You can add this automatically with: ```sh git commit -s -m "component: " # Example: git commit -s -m "cli: add my-command to colima start" ``` ### 4. Push Your Branch Push your branch to your fork: ```sh git push origin my-feature-branch ``` ### 5. Open a Pull Request Open a Pull Request against the main Colima repository. ## 6. DCO Signoff Colima requires all commits to be signed off using the [Developer Certificate of Origin (DCO)](https://developercertificate.org/). This is a simple statement that you, as a contributor, have the right to submit your changes. ## Contribution Guidelines ### Major Contributions Major contributions (new features, significant changes) should be preceded by a GitHub issue discussing the proposed change. ### Minor Contributions Minor contributions (small fixes, documentation e.t.c.) do not require a prior issue. ### LLM Usage Disclosure If you use a Large Language Model (LLM) to generate code, you must fully disclose its usage in your pull request, including which parts were generated and to what extent. ### LLM Reviewability Large code contributions generated by LLMs that are not easily reviewable or understandable will be rejected. ## Reviewing and Merging All PRs are subject to review. Kindly ensure that your PR passes all CI checks. You are also obliged to respond to review comments and update your PR as needed. ## Need Help? If you have questions, open an [issue](https://github.com/abiosoft/colima/issues) or start a [discussion](https://github.com/abiosoft/colima/discussions) in the repository. --- Thank you for helping to improve Colima! ================================================ FILE: docs/FAQ.md ================================================ # FAQs - [FAQs](#faqs) - [How does Colima compare to Lima?](#how-does-colima-compare-to-lima) - [Are Apple Silicon Macs supported?](#are-apple-silicon-macs-supported) - [Are AI workloads supported?](#are-ai-workloads-supported) - [Are older macOS versions supported?](#are-older-macos-versions-supported) - [Does Colima support autostart?](#does-colima-support-autostart) - [Can config file be used instead of cli flags?](#can-config-file-be-used-instead-of-cli-flags) - [Specifying the config location](#specifying-the-config-location) - [Editing the config](#editing-the-config) - [Setting the default config](#setting-the-default-config) - [Specifying the config editor](#specifying-the-config-editor) - [How do I change where Colima files are stored?](#how-do-i-change-where-colima-files-are-stored) - [How do I pass custom environment variables into the VM?](#how-do-i-pass-custom-environment-variables-into-the-vm) - [Docker](#docker) - [Can it run alongside Docker for Mac?](#can-it-run-alongside-docker-for-mac) - [Docker socket location](#docker-socket-location) - [v0.3.4 or older](#v034-or-older) - [v0.4.0 or newer](#v040-or-newer) - [Listing Docker contexts](#listing-docker-contexts) - [Changing the active Docker context](#changing-the-active-docker-context) - [Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?](#cannot-connect-to-the-docker-daemon-at-unixvarrundockersock-is-the-docker-daemon-running) - [How to customize Docker config (e.g., adding insecure registries or registry mirrors)?](#how-to-customize-docker-config-eg-adding-insecure-registries-or-registry-mirrors) - [Docker buildx plugin is missing](#docker-buildx-plugin-is-missing) - [Installing Buildx](#installing-buildx) - [Containerd](#containerd) - [How to customize Containerd config?](#how-to-customize-containerd-config) - [Per-profile overrides](#per-profile-overrides) - [How does Colima compare to minikube, Kind, K3d?](#how-does-colima-compare-to-minikube-kind-k3d) - [For Kubernetes](#for-kubernetes) - [For Docker](#for-docker) - [Is another Distro supported?](#is-another-distro-supported) - [Version v0.5.6 and lower](#version-v056-and-lower) - [Enabling Ubuntu layer](#enabling-ubuntu-layer) - [Accessing the underlying Virtual Machine](#accessing-the-underlying-virtual-machine) - [Version v0.6.0 and newer](#version-v060-and-newer) - [The Virtual Machine's IP is not reachable](#the-virtual-machines-ip-is-not-reachable) - [Enable reachable IP address](#enable-reachable-ip-address) - [Incus instances are not reachable from the host](#incus-instances-are-not-reachable-from-the-host) - [How can disk space be recovered?](#how-can-disk-space-be-recovered) - [Automatic](#automatic) - [Manual](#manual) - [How can disk size be increased?](#how-can-disk-size-be-increased) - [Are Lima overrides supported?](#are-lima-overrides-supported) - [Example: Adding provision scripts](#example-adding-provision-scripts) - [How can the VM and its tools be updated?](#how-can-the-vm-and-its-tools-be-updated) - [Updating Colima](#updating-colima) - [Updating the container runtime](#updating-the-container-runtime) - [Accessing the Virtual Machine](#accessing-the-virtual-machine) - [Troubleshooting](#troubleshooting) - [Colima not starting](#colima-not-starting) - [Broken status](#broken-status) - [FATA\[0000\] error starting vm: error at 'starting': exit status 1](#fata0000-error-starting-vm-error-at-starting-exit-status-1) - [Issues after an upgrade](#issues-after-an-upgrade) - [Colima cannot access the internet.](#colima-cannot-access-the-internet) - [Docker Compose and Buildx showing runc error](#docker-compose-and-buildx-showing-runc-error) - [Version v0.5.6 or lower](#version-v056-or-lower) - [Issue with Docker bind mount showing empty](#issue-with-docker-bind-mount-showing-empty) - [How can Docker version be updated?](#how-can-docker-version-be-updated) - [How can I delete container data](#how-can-i-delete-container-data) ## How does Colima compare to Lima? Colima is basically a higher level usage of Lima and utilises Lima to provide Docker, Containerd and/or Kubernetes. ## Are Apple Silicon Macs supported? Colima supports and works on both Intel and Apple Silicon Macs. Feedbacks would be appreciated. ## Are AI workloads supported? Yes, Colima supports GPU accelerated containers for AI workloads on Apple Silicon Macs running macOS 13 or newer. To get started, start Colima with Docker runtime and krunkit VM type: ```sh colima start --runtime docker --vm-type krunkit ``` Then setup and run AI models: ```sh colima model setup colima model run gemma3 ``` Multiple model registries are supported including HuggingFace (default) and Ollama: ```sh colima model run hf://tinyllama colima model run ollama://tinyllama ``` For more options, run `colima model --help`. ## Are older macOS versions supported? Colima is supported and regularly tested on the latest macOS version. However, Colima requires macOS 13 or newer. You may be able to build Colima and it's dependencies from source on older macOS version. Colima requires [Lima](https://github.com/lima-vm/lima) and [Qemu](https://www.qemu.org/). ## Does Colima support autostart? Since v0.5.6 Colima supports foreground mode via the `--foreground` flag. i.e. `colima start --foreground`. If Colima has been installed using brew, the easiest way to autostart Colima is to use brew services. ```sh brew services start colima ``` ## Can config file be used instead of cli flags? Yes, from v0.4.0, Colima support YAML configuration file. ### Specifying the config location Set the `$COLIMA_HOME` environment variable, otherwise it defaults to `$HOME/.colima`. ### Editing the config ``` colima start --edit ``` For manual edit, the config file is located at `$HOME/.colima/default/colima.yaml`. For other profiles, `$HOME/.colima//colima.yaml` ### Setting the default config ``` colima template ``` For manual edit, the template file is located at `$HOME/.colima/_templates/default.yaml`. ### Specifying the config editor Set the `$EDITOR` environment variable or use the `--editor` flag. ```sh colima start --edit --editor code # one-off config colima template --editor code # default config ``` ## How do I change where Colima files are stored? Colima supports these environment variables, set on your host machine: | Variable | Description | |----------|-------------| | `COLIMA_HOME` | Colima configuration directory (default: `$HOME/.colima`) | | `COLIMA_CACHE_HOME` | Colima cache directory (default is host-specific, see [os.UserCacheDir()](https://pkg.go.dev/os#UserCacheDir)) | | `COLIMA_PROFILE` | Active profile name (default: `default`) | | `DOCKER_CONFIG` | Path to Docker client configuration directory (default: `~/.docker`) | ## How do I pass custom environment variables into the VM? Pass environment variables into the VM at startup using the YAML configuration file: ```yaml env: MY_VAR: value ``` You can also use command-line flags: ```bash session # On your host machine... $ colima start --env MY_VAR=value # Then, within the VM... $ colima ssh user@colima:~$ env | grep MY_VAR MY_VAR=value ``` ## Docker ### Can it run alongside Docker for Mac? Yes, from version v0.3.0 Colima leverages Docker contexts and can thereby run alongside Docker for Mac. Colima makes itself the default Docker context on startup and should work straight away. ### Docker socket location #### v0.3.4 or older Docker socket is located at `$HOME/.colima/docker.sock` #### v0.4.0 or newer Docker socket is located at `$HOME/.colima/default/docker.sock` It can also be retrieved by checking status ``` colima status ``` #### Listing Docker contexts ``` docker context list ``` #### Changing the active Docker context ``` docker context use ``` ### Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? Colima uses Docker contexts to allow co-existence with other Docker servers and sets itself as the default Docker context on startup. However, some applications are not aware of Docker contexts and may lead to the error. This can be fixed by any of the following approaches. Ensure the Docker socket path by checking the [socket location](#docker-socket-location). 1. Setting application specific Docker socket path if supported by the application. e.g. JetBrains IDEs. 2. Setting the `DOCKER_HOST` environment variable to point to Colima socket. ```sh export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock" ``` 3. Linking the Colima socket to the default socket path. **Note** that this may break other Docker servers. ```sh sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock ``` ### How to customize Docker config (e.g., adding insecure registries or registry mirrors)? * v0.3.4 or lower On first startup, Colima generates Docker daemon.json file at `$HOME/.colima/docker/daemon.json`. Modify the daemon.json file accordingly and restart Colima. * v0.4.0 or newer Start Colima with `--edit` flag. ```sh colima start --edit ``` Add the Docker config to the `docker` section. ```diff - docker: {} + docker: + insecure-registries: + - myregistry.com:5000 + - host.docker.internal:5000 ``` **Note:** In order for the Docker client to respect (at least some) configuration value changes, modification of the host ~/.docker/daemon.json file may also be required. For example, if adding registry mirrors, modifications are needed as follows: First, colima: ```sh colima start --edit ``` ```diff - docker: {} + docker: + registry-mirrors: + - https://my.dockerhub.mirror.something + - https://my.quayio.mirror.something ``` As an alternative approach to the **colima start --edit**, make the changes via the **template** command (affecting the configuration for any new instances): ```sh colima template ``` Then, the Docker ~/.docker/daemon.json file (as compared to the default): ```diff - "experimental": false, + "experimental": false, + "registry-mirrors": [ + "https://my.dockerhub.mirror.something", + "https://my.quayio.mirror.something" + ] ``` ### Docker buildx plugin is missing `buildx` can be installed as a Docker plugin #### Installing Buildx Using homebrew ```sh brew install docker-buildx # Follow the caveats mentioned in the install instructions: # mkdir -p ~/.docker/cli-plugins # ln -sfn $(which docker-buildx) ~/.docker/cli-plugins/docker-buildx docker buildx version # verify installation ``` Alternatively ```sh ARCH=amd64 # change to 'arm64' for m1 VERSION=v0.11.2 curl -LO https://github.com/docker/buildx/releases/download/${VERSION}/buildx-${VERSION}.darwin-${ARCH} mkdir -p ~/.docker/cli-plugins mv buildx-${VERSION}.darwin-${ARCH} ~/.docker/cli-plugins/docker-buildx chmod +x ~/.docker/cli-plugins/docker-buildx docker buildx version # verify installation ``` ## Containerd ### How to customize Containerd config? On first startup with the containerd runtime, Colima generates default config files at the standard user config locations: | File | Location | |------|----------| | Containerd config | `~/.config/containerd/config.toml` | | BuildKit config | `~/.config/buildkit/buildkitd.toml` | These follow the standard rootless containerd/buildkit config paths and are shared across all Colima profiles. Modify the files accordingly and restart Colima for changes to take effect. ```sh # edit the containerd config $EDITOR ~/.config/containerd/config.toml # restart colima colima stop && colima start --runtime containerd ``` #### Per-profile overrides To use a different config for a specific profile, place the config file at `$HOME/.colima//containerd/config.toml` (or `buildkitd.toml`). Per-profile configs take priority over the central config. The resolution order is: 1. `~/.colima//containerd/` (per-profile override) 2. `~/.config/containerd/` or `~/.config/buildkit/` (central) 3. Embedded default **Note:** `$XDG_CONFIG_HOME` is respected for the central config location if set. ## How does Colima compare to minikube, Kind, K3d? ### For Kubernetes Yes, you can create a Kubernetes cluster with minikube (with Docker driver), Kind or K3d instead of enabling Kubernetes in Colima. Those are better options if you need multiple clusters, or do not need Docker and Kubernetes to share the same images and runtime. Colima with Docker runtime is fully compatible with Minikube (with Docker driver), Kind and K3d. ### For Docker Minikube with Docker runtime can expose the cluster's Docker with `minikube docker-env`. But there are some caveats. - Kubernetes is not optional, even if you only need Docker. - All of minikube's free drivers for macOS fall-short in one of performance, port forwarding or volumes. While port-forwarding and volumes are non-issue for Kubernetes, they can be a deal breaker for Docker-only use. ## Is another Distro supported? ### Version v0.5.6 and lower Colima uses a lightweight Alpine image with bundled dependencies. Therefore, user interaction with the Virtual Machine is expected to be minimal (if any). However, Colima optionally provides Ubuntu container as a layer. #### Enabling Ubuntu layer * CLI ``` colima start --layer=true ``` * Config ```diff - layer: false + layer: true ``` #### Accessing the underlying Virtual Machine When the layer is enabled, the underlying Virtual Machine is abstracted and both the `ssh` and `ssh-config` commands routes to the layer. The underlying Virtual Machine is still accessible by specifying `--layer=false` to the `ssh` and `ssh-config` commands, or by running `colima` in the SSH session. ### Version v0.6.0 and newer Colima uses Ubuntu as the underlying image. Other distros are not supported. ## The Virtual Machine's IP is not reachable Reachable IP address is not enabled by default due to root privilege and slower startup time. ### Enable reachable IP address **NOTE:** this is only supported on macOS * CLI ``` colima start --network-address ``` * Config ```diff network: - address: false + address: true ``` ## Incus instances are not reachable from the host **Requires v0.10.0** Incus containers and virtual machines are not reachable from the host by default. This is because network address is not enabled by default. To fix this, stop Colima and restart with network address enabled: ```sh colima stop colima start --network-address ``` Or enable it in the config file: ```sh colima start --edit ``` ```diff network: - address: false + address: true ``` ## How can disk space be recovered? Disk space can be freed in the VM by removing containers or running `docker system prune`. However, it will not reflect on the host on Colima versions v0.4.x or lower. ### Automatic For Colima v0.5.0 and above, unused disk space in the VM is released on startup. A restart would suffice. ### Manual For Colima v0.5.0 and above, user can manually recover the disk space by running `sudo fstrim -a` in the VM. ```sh # '-v' may be added for verbose output colima ssh -- sudo fstrim -a ``` ## How can disk size be increased? Disk size is automatically increased on start up based on configuration in `colima.yaml` ```diff - disk: 150 + disk: 250 ``` __Note:__ This feature is available from Version 0.5.3. ## Are Lima overrides supported? Yes, however this should only be done by advanced users. Lima supports `override.yaml` and `default.yaml` files that can modify the VM configuration. The override file is located at `$HOME/.colima/_lima/_config/override.yaml` (or `$LIMA_HOME/_config/override.yaml` if `LIMA_HOME` is set). Settings in `override.yaml` are applied **before** the instance config, while settings in `default.yaml` are applied **after** (as fallback defaults). **Note:** Overriding the image is not supported as Colima's image includes bundled dependencies that would be missing in a user-specified image. ### Example: Adding provision scripts Provision scripts can be added via Lima overrides to run commands during VM boot. ```yaml # $HOME/.colima/_lima/_config/override.yaml provision: - mode: system script: | #!/bin/bash set -eux -o pipefail # install additional packages apt-get update && apt-get install -y curl ``` Alternatively, provision scripts can be specified directly in `colima.yaml`: ```sh colima start --edit ``` ```diff - provision: [] + provision: + - mode: system + script: | + #!/bin/bash + set -eux -o pipefail + apt-get update && apt-get install -y curl ``` ## How can the VM and its tools be updated? ### Updating Colima ```sh brew upgrade colima ``` After upgrading, delete and recreate the instance to use the latest VM image: ```sh colima delete colima start ``` To test the upgrade without affecting the existing setup, use a separate profile: ```sh colima start debug ``` ### Updating the container runtime From v0.7.6, the container runtime (Docker, containerd) can be updated independently: ```sh colima update ``` This updates Docker (or containerd) to the latest version without needing to update Colima itself. ### Accessing the Virtual Machine SSH into the VM to inspect or modify it directly: ```sh colima ssh ``` Run a single command without an interactive session: ```sh colima ssh -- uname -a ``` ## Troubleshooting These are some common issues reported by users and how to troubleshoot them. ### Colima not starting There are multiple reasons that could cause Colima to fail to start. #### Broken status This is the case when the output of `colima list` shows a broken status. This can happen due to macOS restart. ``` colima list PROFILE STATUS ARCH CPUS MEMORY DISK RUNTIME ADDRESS default Broken aarch64 2 2GiB 60GiB ``` This can be fixed by forcefully stopping Colima. The state will be changed to `Stopped` and it should start up normally afterwards. ``` colima stop --force ``` #### FATA[0000] error starting vm: error at 'starting': exit status 1 This indicates that a fatal error is preventing Colima from starting, you can enable the debug log with `--verbose` flag to get more info. If the log output includes `exiting, status={Running:false Degraded:false Exiting:true Errors:[] SSHLocalPort:0}` then it is most certainly due to one of the following. 1. Running on a device without virtualization support. 2. Running an x86_64 version of homebrew (and Colima) on an M1 device. ### Issues after an upgrade The recommended way to troubleshoot after an upgrade is to test with a separate profile. ```sh # start with a profile named 'debug' colima start debug ``` If the separate profile starts successfully without issues, then the issue would be resolved by resetting the default profile. ``` colima delete colima start ``` ### Colima cannot access the internet. Failure for Colima to access the internet is usually down to DNS. Try custom DNS server(s) ```sh colima start --dns 8.8.8.8 --dns 1.1.1.1 ``` Ping an internet address from within the VM to ascertain ``` colima ssh -- ping -c4 google.com PING google.com (216.58.223.238): 56 data bytes 64 bytes from 216.58.223.238: seq=0 ttl=42 time=0.082 ms 64 bytes from 216.58.223.238: seq=1 ttl=42 time=0.557 ms 64 bytes from 216.58.223.238: seq=2 ttl=42 time=0.465 ms 64 bytes from 216.58.223.238: seq=3 ttl=42 time=0.457 ms --- google.com ping statistics --- 4 packets transmitted, 4 packets received, 0% packet loss round-trip min/avg/max = 0.082/0.390/0.557 ms ``` ### Docker Compose and Buildx showing runc error #### Version v0.5.6 or lower Recent versions of Buildkit may show the following error. ```console runc run failed: unable to start container process: error during container init: error mounting "cgroup" to rootfs at "/sys/fs/cgroup": mount cgroup:/sys/fs/cgroup/openrc (via /proc/self/fd/6), flags: 0xf, data: openrc: invalid argument ``` From v0.5.6, start Colima with `--cgroups-v2` flag as a workaround. **This is fixed in v0.6.0.** ### Issue with Docker bind mount showing empty When using docker to bind mount a volume (e.g. using `-v` or `--mount`) from the host where the volume is not contained within `/Users/$USER`, the container will start without raising any errors but the mapped mountpoint on the container will be empty. This is rectified by mounting the volume on the VM, and only then can docker map the volume or any subdirectory. Edit `$HOME/.colima/default/colima.yaml` and add to the `mounts` section (examples are provided within the yaml file), and then run `colima restart`. Start the container again with the desired bind mount and it should show up correctly. ## How can Docker version be updated? Each Colima release includes the latest Docker version at the time of release. From v0.7.6, there is a new `colima update` command to update the container runtime without needing to update Colima or to wait for the next Colima release. ## How can I delete container data From v0.9.0, Colima utilises a different disk for the container runtime data. This guards against accidental data loss after deletion and the container data should be reinstated on `colima start`. To clear all data, `colima delete --data` should be run instead. The `--data` flag ensures that the container data is also deleted. ================================================ FILE: docs/INSTALL.md ================================================ # Installation Options ## Homebrew Stable Version ``` brew install colima ``` Development Version ``` brew install --HEAD colima ``` ## MacPorts Stable version ``` sudo port install colima ``` ## Nix Only stable Version ``` nix-env -i colima ``` Or using solely in a `nix-shell` ``` nix-shell -p colima ``` ## Arch Install dependencies ``` sudo pacman -S qemu-full go docker ``` Install Lima and Colima from Aur ``` yay -S lima-bin colima-bin ``` ## Binary Binaries are available with every release on the [releases page](https://github.com/abiosoft/colima/releases). ```sh # download binary curl -LO https://github.com/abiosoft/colima/releases/latest/download/colima-$(uname)-$(uname -m) # install in $PATH sudo install colima-$(uname)-$(uname -m) /usr/local/bin/colima ``` ## Building from Source Requires [Go](https://golang.org). ```sh # clone repo and cd into it git clone https://github.com/abiosoft/colima cd colima make sudo make install ``` ================================================ FILE: embedded/defaults/abort.yaml ================================================ # ============================================================================================ # # To abort, delete the contents of this file including the comments and save as an empty file # ============================================================================================ # ================================================ FILE: embedded/defaults/colima.yaml ================================================ # Number of CPUs to be allocated to the virtual machine. # Default: 2 cpu: 2 # Size of the disk in GiB to be allocated to the virtual machine for container data. # NOTE: value can only be increased after virtual machine has been created. # # Default: 100 disk: 100 # Size of the memory in GiB to be allocated to the virtual machine. # Default: 2 memory: 2 # Architecture of the virtual machine (x86_64, aarch64, host). # # NOTE: value cannot be changed after virtual machine is created. # Default: host arch: host # Container runtime to be used (docker, containerd). # # NOTE: value cannot be changed after virtual machine is created. # Default: docker runtime: docker # AI model runner (docker, ramalama). # Both require krunkit VM type for GPU access. # docker: Uses Docker Model Runner. # ramalama: Uses Ramalama. # # Default: docker modelRunner: docker # Set custom hostname for the virtual machine. # Default: colima # colima-profile_name for other profiles hostname: null # Kubernetes configuration for the virtual machine. kubernetes: # Enable kubernetes. # Default: false enabled: false # Kubernetes version to use. # This needs to exactly match a k3s version https://github.com/k3s-io/k3s/releases # Default: latest stable release version: v1.35.0+k3s1 # Additional args to pass to k3s https://docs.k3s.io/cli/server # Default: traefik is disabled k3sArgs: [--disable=traefik] # Kubernetes port to listen on # A common port is 6443, though left unbound to ensure no port conflicts # Default: pick random unbound port port: 0 # Auto-activate on the Host for client access. # Setting to true does the following on startup # - sets as active Docker context (for Docker runtime). # - sets as active Kubernetes context (if Kubernetes is enabled). # - sets as active Incus remote (for Incus runtime). # Default: true autoActivate: true # Network configurations for the virtual machine. network: # Assign reachable IP address to the virtual machine. # NOTE: this is currently macOS only and ignored on Linux. # Default: false address: false # Network mode for the virtual machine (shared, bridged). # NOTE: this is currently macOS only and ignored on Linux. # Default: shared mode: shared # Network interface to use for bridged mode. # This is only used when mode is set to bridged. # NOTE: this is currently macOS only and ignored on Linux. # Default: en0 interface: en0 # Use the assigned IP address as the preferred route for the VM. # Note: this only has an effect when `address` is set to true. # Default: false preferredRoute: false # Custom DNS resolvers for the virtual machine. # # EXAMPLE # dns: [8.8.8.8, 1.1.1.1] # # Default: [] dns: [] # DNS hostnames to resolve to custom targets using the internal resolver. # This setting has no effect if a custom DNS resolver list is supplied above. # It does not configure the /etc/hosts files of any machine or container. # The value can be an IP address or another host. # # EXAMPLE # dnsHosts: # example.com: 1.2.3.4 dnsHosts: host.docker.internal: host.lima.internal # Replicate host IP addresses in the VM. This enables port forwarding to specific # host IP addresses. # e.g. `docker run --port 10.0.1.2:8080:8080 alpine` would only forward to the # specified IP address. # # Default: false hostAddresses: false # Custom gateway address for the virtual machine. # The last octet needs to be 2. # # EXAMPLE # gatewayAddress: 192.168.10.2 # # Default: 192.168.5.2 gatewayAddress: 192.168.5.2 # ===================================================================== # # ADVANCED CONFIGURATION # ===================================================================== # # Forward the host's SSH agent to the virtual machine. # Default: false forwardAgent: false # Docker daemon configuration that maps directly to daemon.json. # https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file. # NOTE: some settings may affect Colima's ability to start docker. e.g. `hosts`. # # EXAMPLE - disable buildkit # docker: # features: # buildkit: false # # EXAMPLE - add insecure registries # docker: # insecure-registries: # - myregistry.com:5000 # - host.docker.internal:5000 # # Colima default behaviour: buildkit enabled # Default: {} docker: {} # Virtual Machine type (krunkit, qemu, vz) # NOTE: this is macOS 13 only. For Linux and macOS <13.0, qemu is always used. # # vz is macOS virtualization framework and requires macOS 13. # krunkit runs super‑light VMs on macOS/ARM64 with a focus on GPU access. It is experimental. # # NOTE: value cannot be changed after virtual machine is created. # Default: qemu vmType: qemu # Port forwarder for the virtual machine (ssh, grpc, none). # ssh is more stable but supports only TCP. # grpc supports both TCP and UDP, but is experimental. # none disables port forwarding. # # Default: ssh portForwarder: ssh # Utilise rosetta for amd64 emulation (requires m1 mac and vmType `vz`) # Default: false rosetta: false # Enable foreign architecture emulation via binfmt (e.g. amd64 on arm64, arm64 on amd64) # Default: true binfmt: true # Enable nested virtualization for the virtual machine (requires m3 mac and vmType `vz`) # Default: false nestedVirtualization: false # Volume mount driver for the virtual machine (virtiofs, 9p, sshfs). # # virtiofs is limited to macOS and vmType `vz`. It is the fastest of the options. # # 9p is the recommended and the most stable option for vmType `qemu`. # # sshfs is faster than 9p but the least reliable of the options (when there are lots # of concurrent reads or writes). # # NOTE: value cannot be changed after virtual machine is created. # Default: virtiofs (for vz), sshfs (for qemu) mountType: sshfs # Propagate inotify file events to the VM. # NOTE: this is experimental. mountInotify: false # The CPU type for the virtual machine (requires vmType `qemu`). # Options available for host emulation can be checked with: `qemu-system-$(arch) -cpu help`. # Instructions are also supported by appending to the cpu type e.g. "qemu64,+ssse3". # Default: host cpuType: host # Custom provision scripts for the virtual machine. # Provisioning scripts are executed on startup and therefore needs to be idempotent. # # EXAMPLE - script executed as root # provision: # - mode: system # script: apt-get install htop vim # # EXAMPLE - script executed as user # provision: # - mode: user # script: | # [ -f ~/.provision ] && exit 0; # echo provisioning as $USER... # touch ~/.provision # # EXAMPLE - script executed after VM boot, before container runtimes start # provision: # - mode: after-boot # script: echo "VM is up, containers not yet started" # # EXAMPLE - script executed after VM and container runtimes are ready # provision: # - mode: ready # script: echo "everything is ready" # # Default: [] provision: [] # Modify ~/.ssh/config automatically to include a SSH config for the virtual machine. # SSH config will still be generated in $COLIMA_HOME/ssh_config regardless. # Default: true sshConfig: true # The port number for the SSH server for the virtual machine. # When set to 0, a random available port is used. # # Default: 0 sshPort: 0 # Configure volume mounts for the virtual machine. # Colima mounts user's home directory by default to provide a familiar # user experience. # # EXAMPLE # mounts: # - location: ~/secrets # writable: false # - location: ~/projects # writable: true # # Colima default behaviour: $HOME is mounted as writable. # Default: [] mounts: [] # Specify a custom disk image for the virtual machine. # When not specified, Colima downloads an appropriate disk image from Github at # https://github.com/abiosoft/colima-core/releases. # The file path to a custom disk image can be specified to override the behaviour. # # Default: "" diskImage: "" # Size of the disk in GiB for the root filesystem of the virtual machine. # This value is ignored if no runtime is in use. i.e. `none` runtime. # Default: 20 rootDisk: 20 # Environment variables for the virtual machine. # # EXAMPLE # env: # KEY: value # ANOTHER_KEY: another value # # Default: {} env: {} ================================================ FILE: embedded/defaults/template.yaml ================================================ # New instances will be created with the following configurations. ================================================ FILE: embedded/embed.go ================================================ package embedded import ( "embed" ) //go:embed network k3s defaults images var fs embed.FS // FS returns the underlying embed.FS func FS() embed.FS { return fs } func read(file string) ([]byte, error) { return fs.ReadFile(file) } // Read reads the content of file func Read(file string) ([]byte, error) { return read(file) } // ReadString reads the content of file as string func ReadString(file string) (string, error) { b, err := read(file) return string(b), err } ================================================ FILE: embedded/images/images.txt ================================================ arm64 none https://github.com/abiosoft/colima-core/releases/download/v0.10.1/ubuntu-24.04-minimal-cloudimg-arm64-none.qcow2 8ea152c469d14f350402768624eb03f76aba6e69ce72794e73c948cc15faf064daa6047eb3d3e6b8a044d3fb1e7544d06549ead95ceb667b57b9ee69129e7f91 ubuntu-24.04-minimal-cloudimg-arm64-none.qcow2 arm64 docker https://github.com/abiosoft/colima-core/releases/download/v0.10.1/ubuntu-24.04-minimal-cloudimg-arm64-docker.qcow2 9bfcd83dadc9bef46aaf44374624986bab4c948428ff7d4146a854f2bb77a2fc4f16ebd96e7c5ff1a855a70458c44e89da05f79c2d501525e6113d9ae9efa7da ubuntu-24.04-minimal-cloudimg-arm64-docker.qcow2 arm64 containerd https://github.com/abiosoft/colima-core/releases/download/v0.10.1/ubuntu-24.04-minimal-cloudimg-arm64-containerd.qcow2 80fbdd77cec341f0fcc147fdc1a149b2effc920e9698faa5ddb7fde10cd62e71fb1d832250ae2dc3b3a92da5a8aa538ca98953ec2a87c92b8c09196b9dec3b87 ubuntu-24.04-minimal-cloudimg-arm64-containerd.qcow2 arm64 incus https://github.com/abiosoft/colima-core/releases/download/v0.10.1/ubuntu-24.04-minimal-cloudimg-arm64-incus.qcow2 6158678f4994562ec0db13dce292d0053e777f1f9b1c26f4ee455f933461dc788829f99808d756c135a76581383e844c581eda82566ffa280babea51633f4740 ubuntu-24.04-minimal-cloudimg-arm64-incus.qcow2 amd64 none https://github.com/abiosoft/colima-core/releases/download/v0.10.1/ubuntu-24.04-minimal-cloudimg-amd64-none.qcow2 9a1f8fbc4ddccdbe80e53bc516d9dd1cee36576d211c698b0c5cad3a909f853ad8500901f48ca44188f158f43377d4ca26130b2cf7c8fd8017275fa1c51c254e ubuntu-24.04-minimal-cloudimg-amd64-none.qcow2 amd64 docker https://github.com/abiosoft/colima-core/releases/download/v0.10.1/ubuntu-24.04-minimal-cloudimg-amd64-docker.qcow2 8d723c93ae621ccecfe93489332e366b940c860419c4e36f8d41f714bfa332829fbf1d94dfd8f526b10e84037d6d96b23586bbb2e35f11feaab1e94bfbd7bd77 ubuntu-24.04-minimal-cloudimg-amd64-docker.qcow2 amd64 containerd https://github.com/abiosoft/colima-core/releases/download/v0.10.1/ubuntu-24.04-minimal-cloudimg-amd64-containerd.qcow2 bd046d82e4e18bce54a292778038de79c723a2b884f8a4b6cab052b0843a42deb5710c83f4ec26c36b6f3a0118e8783d4b3c7d04f5e4f2d7f7ade412abbef49b ubuntu-24.04-minimal-cloudimg-amd64-containerd.qcow2 amd64 incus https://github.com/abiosoft/colima-core/releases/download/v0.10.1/ubuntu-24.04-minimal-cloudimg-amd64-incus.qcow2 6161712b22251f7a72c78c18ab8edd7c9a0d2cf86160b1a3006e0f2b6fdab7db8aeb2a748cdcc6207cffe0914ab6f8235dd5eb8aa6200fa1f8cc56e2d472d3bb ubuntu-24.04-minimal-cloudimg-amd64-incus.qcow2 ================================================ FILE: embedded/images/images_sha.sh ================================================ #!/usr/bin/env bash set -eux BASE_URL=https://github.com/abiosoft/colima-core/releases/download BASE_FILENAME=ubuntu-24.04-minimal-cloudimg VERSION=v0.10.1 RUNTIMES="none docker containerd incus" ARCHS="arm64 amd64" DIR="$(dirname $0)" FILE="${DIR}/images.txt" # reset output files echo -n >$FILE for arch in ${ARCHS}; do for runtime in ${RUNTIMES}; do URL="${BASE_URL}/${VERSION}/${BASE_FILENAME}-${arch}-${runtime}.qcow2" SHA="$(curl -sL ${URL}.sha512sum)" echo "$arch $runtime ${URL} ${SHA}" >>$FILE done done ================================================ FILE: embedded/k3s/flannel.json ================================================ { "name": "cbr0", "cniVersion": "0.3.1", "plugins": [ { "type": "flannel", "delegate": { "hairpinMode": true, "forceAddress": true, "isDefaultGateway": true } }, { "type": "portmap", "capabilities": { "portMappings": true } } ] } ================================================ FILE: embedded/network/sudo.txt ================================================ # starting vmnet daemon %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 * %staff ALL=(root:wheel) NOPASSWD:NOSETENV: /opt/colima/bin/socket_vmnet --vmnet-mode bridged --socket-group staff * # terminating vmnet daemon %staff ALL=(root:wheel) NOPASSWD:NOSETENV: /usr/bin/pkill -F /opt/colima/run/*.pid # validating vmnet daemon %staff ALL=(root:wheel) NOPASSWD:NOSETENV: /usr/bin/pkill -0 -F /opt/colima/run/*.pid # adding route to Incus container network %staff ALL=(root:wheel) NOPASSWD:NOSETENV: /sbin/route add -net 192.168.100.0/24 * # removing route to Incus container network %staff ALL=(root:wheel) NOPASSWD:NOSETENV: /sbin/route delete -net 192.168.100.0/24 ================================================ FILE: embedded/sudoers.go ================================================ package embedded import ( "bytes" "fmt" "io" "os" "path/filepath" "strings" log "github.com/sirupsen/logrus" ) const sudoersPath = "/etc/sudoers.d/colima" const sudoersEmbeddedPath = "network/sudo.txt" // SudoersInstaller provides the ability to run commands on the host // for installing the sudoers file. type SudoersInstaller interface { RunInteractive(args ...string) error RunWith(stdin io.Reader, stdout io.Writer, args ...string) error } // SudoersInstalled checks if the sudoers file contains the expected embedded content. func SudoersInstalled() bool { txt, err := Read(sudoersEmbeddedPath) if err != nil { return false } b, err := os.ReadFile(sudoersPath) if err != nil { return false } return bytes.Contains(b, txt) } // InstallSudoers installs the embedded sudoers file if it is not already // installed with the expected content. This may prompt for a sudo password. func InstallSudoers(host SudoersInstaller) error { if SudoersInstalled() { return nil } txt, err := ReadString(sudoersEmbeddedPath) if err != nil { return fmt.Errorf("error reading embedded sudoers file: %w", err) } log.Println("setting up network permissions, sudo password may be required") dir := filepath.Dir(sudoersPath) if err := host.RunInteractive("sudo", "mkdir", "-p", dir); err != nil { return fmt.Errorf("error preparing sudoers directory: %w", err) } stdin := strings.NewReader(txt) stdout := &bytes.Buffer{} if err := host.RunWith(stdin, stdout, "sudo", "sh", "-c", "cat > "+sudoersPath); err != nil { return fmt.Errorf("error writing sudoers file: %w", err) } return nil } ================================================ FILE: environment/container/containerd/buildkitd.toml ================================================ [worker.oci] enabled = false [worker.containerd] enabled = true [grpc] gid = 1000 ================================================ FILE: environment/container/containerd/config.toml ================================================ [grpc] gid = 1000 ================================================ FILE: environment/container/containerd/containerd.go ================================================ package containerd import ( "context" _ "embed" "fmt" "os" "path/filepath" "time" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/guest/systemctl" ) // Name is container runtime name const Name = "containerd" var configDir = func() string { return config.CurrentProfile().ConfigDir() } // HostSocketFiles returns the path to the socket files on host. func HostSocketFiles() (files struct { Containerd string Buildkitd string }) { files.Containerd = filepath.Join(configDir(), "containerd.sock") files.Buildkitd = filepath.Join(configDir(), "buildkitd.sock") return files } // This is written with assumption that Lima is the VM, // which provides nerdctl/containerd support out of the box. // There may be need to make this flexible for non-Lima VMs. //go:embed config.toml var containerdConf []byte //go:embed buildkitd.toml var buildKitConf []byte const containerdConfFile = "/etc/containerd/config.toml" const buildKitConfFile = "/etc/buildkit/buildkitd.toml" func newRuntime(host environment.HostActions, guest environment.GuestActions) environment.Container { return &containerdRuntime{ host: host, guest: guest, systemctl: systemctl.New(guest), CommandChain: cli.New(Name), } } func init() { environment.RegisterContainer(Name, newRuntime, false) } var _ environment.Container = (*containerdRuntime)(nil) type containerdRuntime struct { host environment.HostActions guest environment.GuestActions systemctl systemctl.Systemctl cli.CommandChain } func (c containerdRuntime) Name() string { return Name } func (c containerdRuntime) Provision(ctx context.Context) error { a := c.Init(ctx) // containerd config a.Add(func() error { profilePath := filepath.Join(configDir(), "containerd", "config.toml") centralPath := filepath.Join(userConfigDir(), "containerd", "config.toml") return c.provisionConfig(profilePath, centralPath, containerdConfFile, containerdConf) }) // buildkitd config a.Add(func() error { profilePath := filepath.Join(configDir(), "containerd", "buildkitd.toml") centralPath := filepath.Join(userConfigDir(), "buildkit", "buildkitd.toml") return c.provisionConfig(profilePath, centralPath, buildKitConfFile, buildKitConf) }) return a.Exec() } // userConfigDir returns the user config directory following XDG conventions. // This is ~/.config on Linux/macOS, used for central config file locations // that follow the containerd/buildkit rootless conventions. func userConfigDir() string { if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" { return dir } home, err := os.UserHomeDir() if err != nil { return "" } return filepath.Join(home, ".config") } // provisionConfig writes a config file to the VM. Config files are resolved // in the following order: // 1. Per-profile override at ~/.colima//containerd/ // 2. Central config at ~/.config/containerd/ (or ~/.config/buildkit/) // 3. Embedded default // // On first run, the default config is written to the central location for // user discovery and editing. func (c containerdRuntime) provisionConfig(profilePath, centralPath, guestPath string, defaultConf []byte) error { // 1. per-profile override takes highest priority if data, err := os.ReadFile(profilePath); err == nil { return c.guest.Write(guestPath, data) } // 2. central config if data, err := os.ReadFile(centralPath); err == nil { return c.guest.Write(guestPath, data) } // 3. no user config found; write the default to the central location // for discoverability and use it if err := os.MkdirAll(filepath.Dir(centralPath), 0755); err != nil { return fmt.Errorf("error creating config directory: %w", err) } if err := os.WriteFile(centralPath, defaultConf, 0644); err != nil { return fmt.Errorf("error writing default config: %w", err) } return c.guest.Write(guestPath, defaultConf) } func (c containerdRuntime) Start(ctx context.Context) error { a := c.Init(ctx) a.Add(func() error { return c.systemctl.Restart("containerd.service") }) // service startup takes few seconds, retry at most 10 times before giving up. a.Retry("", time.Second*5, 10, func(int) error { return c.guest.RunQuiet("sudo", "nerdctl", "info") }) a.Add(func() error { return c.systemctl.Start("buildkit.service") }) return a.Exec() } func (c containerdRuntime) Running(ctx context.Context) bool { return c.systemctl.Active("containerd.service") } func (c containerdRuntime) Stop(ctx context.Context, force bool) error { a := c.Init(ctx) a.Add(func() error { return c.systemctl.Stop("containerd.service", force) }) return a.Exec() } func (c containerdRuntime) Teardown(context.Context) error { // teardown not needed, will be part of VM teardown return nil } func (c containerdRuntime) Dependencies() []string { // no dependencies return nil } func (c containerdRuntime) Version(ctx context.Context) string { version, _ := c.guest.RunOutput("sudo", "nerdctl", "version", "--format", `client: {{.Client.Version}}{{printf "\n"}}server: {{(index .Server.Components 0).Version}}`) return version } func (c *containerdRuntime) Update(ctx context.Context) (bool, error) { return false, fmt.Errorf("update not supported for the %s runtime", Name) } // DataDirs represents the data disk for the container runtime. func DataDisk() environment.DataDisk { return environment.DataDisk{ Dirs: diskDirs, FSType: "ext4", PreMount: []string{ "systemctl stop containerd.service", "systemctl stop buildkit.service", }, } } var diskDirs = []environment.DiskDir{ {Name: "containerd", Path: "/var/lib/containerd"}, {Name: "buildkit", Path: "/var/lib/buildkit"}, {Name: "nerdctl", Path: "/var/lib/nerdctl"}, {Name: "rancher", Path: "/var/lib/rancher"}, {Name: "cni", Path: "/var/lib/cni"}, } ================================================ FILE: environment/container/docker/config.toml ================================================ disabled_plugins = ["cri"] [grpc] gid = 1000 ================================================ FILE: environment/container/docker/containerd.go ================================================ package docker import ( "context" _ "embed" "fmt" ) const containerdConfFile = "/etc/containerd/config.toml" const containerdConfFileBackup = "/etc/containerd/config.colima.bak.toml" //go:embed config.toml var containerdConf []byte func (d dockerRuntime) provisionContainerd(ctx context.Context) error { a := d.Init(ctx) // containerd config a.Add(func() error { if _, err := d.guest.Stat(containerdConfFileBackup); err == nil { // backup already exists, no need to overwrite return nil } // backup existing containerd config if err := d.guest.Run("sudo", "cp", containerdConfFile, containerdConfFileBackup); err != nil { return fmt.Errorf("error backing up %s: %w", containerdConfFile, err) } // write new containerd config if err := d.guest.Write(containerdConfFile, containerdConf); err != nil { return fmt.Errorf("error writing %s: %w", containerdConfFile, err) } return nil }) a.Add(func() error { // restart containerd service return d.systemctl.Restart("containerd.service") }) return a.Exec() } ================================================ FILE: environment/container/docker/context.go ================================================ package docker import ( "path/filepath" "github.com/abiosoft/colima/config" ) var configDir = func() string { return config.CurrentProfile().ConfigDir() } // HostSocketFile returns the path to the docker socket on host. func HostSocketFile() string { return filepath.Join(configDir(), "docker.sock") } func LegacyDefaultHostSocketFile() string { return filepath.Join(filepath.Dir(configDir()), "docker.sock") } func (d dockerRuntime) contextCreated() bool { return d.host.RunQuiet("docker", "context", "inspect", config.CurrentProfile().ID) == nil } func (d dockerRuntime) setupContext() error { if d.contextCreated() { return nil } profile := config.CurrentProfile() return d.host.Run("docker", "context", "create", profile.ID, "--description", profile.DisplayName, "--docker", "host=unix://"+HostSocketFile(), ) } func (d dockerRuntime) useContext() error { return d.host.Run("docker", "context", "use", config.CurrentProfile().ID) } func (d dockerRuntime) teardownContext() error { if !d.contextCreated() { return nil } return d.host.Run("docker", "context", "rm", "--force", config.CurrentProfile().ID) } ================================================ FILE: environment/container/docker/daemon.go ================================================ package docker import ( "encoding/json" "fmt" "net" "net/url" ) const daemonFile = "/etc/docker/daemon.json" const hostGatewayIPKey = "host-gateway-ip" func getHostGatewayIp(d dockerRuntime, conf map[string]any) (string, error) { // get host-gateway ip from the guest ip, err := d.guest.RunOutput("sh", "-c", "grep 'host.lima.internal' /etc/hosts | awk -F' ' '{print $1}'") if err != nil { return "", fmt.Errorf("error retrieving host gateway IP address: %w", err) } // if set by the user, use the user specified value if _, ok := conf[hostGatewayIPKey]; ok { if gip, ok := conf[hostGatewayIPKey].(string); ok { ip = gip } } if net.ParseIP(ip) == nil { return "", fmt.Errorf("invalid host gateway IP address: '%s'", ip) } return ip, nil } func resolveHostProxy(hostProxy, hostGateway string) string { u, err := url.Parse(hostProxy) if err != nil { return hostProxy } ips, err := net.LookupIP(u.Hostname()) if err != nil { return hostProxy } for _, ip := range ips { if ip.IsLoopback() { newHost := hostGateway if u.Port() != "" { newHost = net.JoinHostPort(newHost, u.Port()) } u.Host = newHost hostProxy = u.String() break } } return hostProxy } func (d dockerRuntime) createDaemonFile(conf map[string]any, env map[string]string) error { if conf == nil { conf = map[string]any{} } // enable buildkit (if not set by user) if _, ok := conf["features"]; !ok { conf["features"] = map[string]any{ "buildkit": true, "containerd-snapshotter": true, } } // enable cgroupfs for k3s (if not set by user) if _, ok := conf["exec-opts"]; !ok { conf["exec-opts"] = []string{"native.cgroupdriver=cgroupfs"} } else if opts, ok := conf["exec-opts"].([]string); ok { conf["exec-opts"] = append(opts, "native.cgroupdriver=cgroupfs") } // remove host-gateway-ip if set by the user // to avoid clash with systemd configuration delete(conf, hostGatewayIPKey) // add proxy vars if set // according to https://docs.docker.com/config/daemon/systemd/#httphttps-proxy if vars := d.proxyEnvVars(env); !vars.empty() { proxyConf := map[string]any{} hostGatewayIP, err := getHostGatewayIp(d, conf) if err != nil { return err } if vars.http != "" { proxyConf["http-proxy"] = resolveHostProxy(vars.http, hostGatewayIP) } if vars.https != "" { proxyConf["https-proxy"] = resolveHostProxy(vars.https, hostGatewayIP) } if vars.no != "" { proxyConf["no-proxy"] = vars.no } conf["proxies"] = proxyConf } b, err := json.MarshalIndent(conf, "", " ") if err != nil { return fmt.Errorf("error marshaling daemon.json: %w", err) } return d.guest.Write(daemonFile, b) } func (d dockerRuntime) addHostGateway(conf map[string]any) error { // get host-gateway ip from the guest ip, err := getHostGatewayIp(d, conf) if err != nil { return err } // set host-gateway ip as systemd service file content := fmt.Sprintf(systemdUnitFileContent, ip) if err := d.guest.Write(systemdUnitFilename, []byte(content)); err != nil { return fmt.Errorf("error creating systemd unit file: %w", err) } return nil } func (d dockerRuntime) reloadAndRestartSystemdService() error { if err := d.systemctl.DaemonReload(); err != nil { return fmt.Errorf("error reloading systemd daemon: %w", err) } if err := d.systemctl.Restart("docker.service"); err != nil { return fmt.Errorf("error restarting docker: %w", err) } return nil } const systemdUnitFilename = "/etc/systemd/system/docker.service.d/docker.conf" const systemdUnitFileContent string = ` [Service] LimitNOFILE=infinity ExecStart= ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --host-gateway-ip=%s ` ================================================ FILE: environment/container/docker/docker.go ================================================ package docker import ( "context" "os" "path/filepath" "time" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/guest/systemctl" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/debutil" ) // Name is container runtime name. const Name = "docker" var _ environment.Container = (*dockerRuntime)(nil) func init() { environment.RegisterContainer(Name, newRuntime, false) } type dockerRuntime struct { host environment.HostActions guest environment.GuestActions systemctl systemctl.Systemctl cli.CommandChain } // newRuntime creates a new docker runtime. func newRuntime(host environment.HostActions, guest environment.GuestActions) environment.Container { return &dockerRuntime{ host: host, guest: guest, systemctl: systemctl.New(guest), CommandChain: cli.New(Name), } } func (d dockerRuntime) Name() string { return Name } func (d dockerRuntime) Provision(ctx context.Context) error { a := d.Init(ctx) log := d.Logger(ctx) conf, _ := ctx.Value(config.CtxKey()).(config.Config) // provision containerd a.Add(func() error { return d.provisionContainerd(ctx) }) // daemon.json a.Add(func() error { // these are not fatal errors if err := d.createDaemonFile(conf.Docker, conf.Env); err != nil { log.Warnln(err) } if err := d.addHostGateway(conf.Docker); err != nil { log.Warnln(err) } if err := d.reloadAndRestartSystemdService(); err != nil { log.Warnln(err) } return nil }) // docker context a.Add(d.setupContext) if conf.AutoActivate() { a.Add(d.useContext) } return a.Exec() } func (d dockerRuntime) Start(ctx context.Context) error { a := d.Init(ctx) a.Retry("", time.Second, 60, func(int) error { return d.systemctl.Start("docker.service") }) // service startup takes few seconds, retry for a minute before giving up. a.Retry("", time.Second, 60, func(int) error { return d.guest.RunQuiet("sudo", "docker", "info") }) // ensure docker is accessible without root // otherwise, restart to ensure user is added to docker group a.Add(func() error { if err := d.guest.RunQuiet("docker", "info"); err == nil { return nil } ctx := context.WithValue(ctx, cli.CtxKeyQuiet, true) return d.guest.Restart(ctx) }) return a.Exec() } func (d dockerRuntime) Running(ctx context.Context) bool { return d.systemctl.Active("docker.service") } func (d dockerRuntime) Stop(ctx context.Context, force bool) error { a := d.Init(ctx) a.Add(func() error { if !d.Running(ctx) { return nil } return d.systemctl.Stop("docker.service", force) }) // clear docker context settings // since the container runtime can be changed on startup, // it is better to not leave unnecessary traces behind a.Add(d.teardownContext) return a.Exec() } func (d dockerRuntime) Teardown(ctx context.Context) error { a := d.Init(ctx) // clear docker context settings a.Add(d.teardownContext) return a.Exec() } func (d dockerRuntime) Dependencies() []string { return []string{"docker"} } func (d dockerRuntime) Version(ctx context.Context) string { version, _ := d.host.RunOutput("docker", "--context", config.CurrentProfile().ID, "version", "--format", `client: v{{.Client.Version}}{{printf "\n"}}server: v{{.Server.Version}}`) return version } func (d *dockerRuntime) Update(ctx context.Context) (bool, error) { packages := []string{ "docker-ce", "docker-ce-cli", "containerd.io", } return debutil.UpdateRuntime(ctx, d.guest, d, packages...) } // DataDirs represents the data disk for the container runtime. func DataDisk() environment.DataDisk { return environment.DataDisk{ Dirs: diskDirs, FSType: "ext4", PreMount: []string{ "systemctl stop docker.service", "systemctl stop containerd.service", }, } } var diskDirs = []environment.DiskDir{ {Name: "docker", Path: "/var/lib/docker"}, {Name: "containerd", Path: "/var/lib/containerd"}, {Name: "rancher", Path: "/var/lib/rancher"}, {Name: "cni", Path: "/var/lib/cni"}, {Name: "ramalama", Path: "/var/lib/ramalama"}, } // DockerDir returns the path to Docker config. func DockerDir() string { // if DOCKER_CONFIG env var is set, obey it. if dir := os.Getenv("DOCKER_CONFIG"); dir != "" { return dir } return filepath.Join(util.HomeDir(), ".docker") } ================================================ FILE: environment/container/docker/proxy.go ================================================ package docker import ( "os" "strings" ) type proxyVars struct { http string https string no string } func (p proxyVars) empty() bool { return p.http == "" && p.https == "" } type proxyVarKey string var ( httpProxy proxyVarKey = "http_proxy" httpsProxy proxyVarKey = "https_proxy" noProxy proxyVarKey = "no_proxy" ) // keys return both the lower case and upper case env var keys. // e.g. http_proxy and HTTP_PROXY func (p proxyVarKey) Keys() []string { return []string{string(p), strings.ToUpper(string(p))} } func (d dockerRuntime) proxyEnvVars(env map[string]string) proxyVars { getVal := func(key proxyVarKey) string { for _, k := range key.Keys() { // config if val, ok := env[k]; ok { return val } // os if val := os.Getenv(k); val != "" { return val } } return "" } return proxyVars{ http: getVal(httpProxy), https: getVal(httpsProxy), no: getVal(noProxy), } } ================================================ FILE: environment/container/incus/config.yaml ================================================ networks: - config: ipv4.address: {{.BridgeGateway}} ipv4.nat: "true" ipv6.address: auto description: "" name: {{.Interface}} type: "" project: default {{ if .SetStorage }} storage_pools: - config: size: {{.Disk}}GiB description: "" name: default driver: zfs {{ end }} profiles: - config: {} description: "" devices: eth0: name: eth0 network: {{.Interface}} type: nic root: path: / pool: default type: disk name: default projects: [] cluster: null ================================================ FILE: environment/container/incus/incus.go ================================================ package incus import ( "bytes" "context" _ "embed" "encoding/json" "fmt" "path/filepath" "strings" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/guest/systemctl" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/debutil" ) const incusBridgeInterface = "incusbr0" func newRuntime(host environment.HostActions, guest environment.GuestActions) environment.Container { return &incusRuntime{ host: host, guest: guest, systemctl: systemctl.New(guest), CommandChain: cli.New(Name), } } var configDir = func() string { return config.CurrentProfile().ConfigDir() } // HostSocketFile returns the path to the containerd socket on host. func HostSocketFile() string { return filepath.Join(configDir(), "incus.sock") } const ( Name = "incus" storageDriver = "zfs" poolName = "default" poolMetaDir = "/var/lib/incus/storage-pools/" + poolName poolDisksDir = "/var/lib/incus/disks" poolDiskFile = poolDisksDir + "/" + poolName + ".img" ) func init() { environment.RegisterContainer(Name, newRuntime, false) } var _ environment.Container = (*incusRuntime)(nil) type incusRuntime struct { host environment.HostActions guest environment.GuestActions systemctl systemctl.Systemctl cli.CommandChain } // Dependencies implements environment.Container. func (c *incusRuntime) Dependencies() []string { return []string{"incus"} } // Provision implements environment.Container. func (c *incusRuntime) Provision(ctx context.Context) error { conf := ctx.Value(config.CtxKey()).(config.Config) log := c.Logger(ctx) // start incus to check if already fully provisioned. // after a full /var/lib/incus restore from external disk, incus // reads its previous database and restores networks/pools automatically. _ = c.systemctl.Start("incus.service") if found, _, _ := c.findNetwork(incusBridgeInterface); found { // already provisioned (e.g. full restore from external disk) return nil } emptyDisk := true recoverStorage := false if limautil.DiskProvisioned(Name) { emptyDisk = false // previous disk exists // ignore storage, recovery would be attempted later recoverStorage = cli.Prompt("existing Incus data found, would you like to recover the storage pool(s)") } var value struct { Disk int Interface string BridgeGateway string SetStorage bool } value.Disk = conf.Disk value.Interface = incusBridgeInterface value.BridgeGateway = bridgeGateway value.SetStorage = emptyDisk // set only when the disk is empty buf, err := util.ParseTemplate(configYaml, value) if err != nil { return fmt.Errorf("error parsing incus config template: %w", err) } stdin := bytes.NewReader(buf) if err := c.guest.RunWith(stdin, nil, "sudo", "incus", "admin", "init", "--preseed"); err != nil { return fmt.Errorf("error setting up incus: %w", err) } // provision successful if emptyDisk { return nil } if !recoverStorage { return c.wipeDisk(conf.Disk) } if _, err := c.guest.Stat(poolDiskFile); err != nil { log.Warnln(fmt.Errorf("cannot recover disk: %w, creating new storage pool", err)) return c.wipeDisk(conf.Disk) } for { if err := c.recoverDisk(ctx); err != nil { log.Warnln(err) if cli.Prompt("recovery failed for default storage pool, try again") { continue } log.Warnln("discarding disk, creating new storage pool") return c.wipeDisk(conf.Disk) } break } return nil } // Running implements environment.Container. func (c *incusRuntime) Running(ctx context.Context) bool { return c.systemctl.Active("incus.service") } // Start implements environment.Container. func (c *incusRuntime) Start(ctx context.Context) error { conf, _ := ctx.Value(config.CtxKey()).(config.Config) a := c.Init(ctx) // incus should already be started // this is mainly to ascertain it has started if c.poolImported() { a.Add(func() error { return c.systemctl.Start("incus.service") }) } else { // pool not yet imported // restart incus to import pool a.Add(func() error { return c.systemctl.Restart("incus.service") }) } // sync disk size for the default pool if conf.Disk > 0 { a.Add(func() error { // this can fail silently _ = c.guest.RunQuiet("sudo", "incus", "storage", "set", "default", "size="+config.Disk(conf.Disk).GiB()) return nil }) } a.Add(func() error { // attempt to set remote if err := c.setRemote(conf.AutoActivate()); err == nil { return nil } // workaround missing user in incus-admin by restarting ctx := context.WithValue(ctx, cli.CtxKeyQuiet, true) if err := c.guest.Restart(ctx); err != nil { return err } // attempt once again to set remote return c.setRemote(conf.AutoActivate()) }) a.Add(func() error { if err := c.addDockerRemote(); err != nil { return cli.ErrNonFatal(err) } return nil }) a.Add(func() error { if err := c.addContainerRoute(); err != nil { return cli.ErrNonFatal(err) } return nil }) return a.Exec() } // Stop implements environment.Container. func (c *incusRuntime) Stop(ctx context.Context, force bool) error { a := c.Init(ctx) a.Add(func() error { _ = c.removeContainerRoute() return nil }) a.Add(func() error { return c.systemctl.Stop("incus.service", force) }) a.Add(c.unsetRemote) return a.Exec() } // Teardown implements environment.Container. func (c *incusRuntime) Teardown(ctx context.Context) error { a := c.Init(ctx) a.Add(func() error { _ = c.removeContainerRoute() return nil }) a.Add(c.unsetRemote) return a.Exec() } // Version implements environment.Container. func (c *incusRuntime) Version(ctx context.Context) string { version, _ := c.host.RunOutput("incus", "version", config.CurrentProfile().ID+":") return version } func (c incusRuntime) Name() string { return Name } func (c incusRuntime) setRemote(activate bool) error { name := config.CurrentProfile().ID // add remote if !c.hasRemote(name) { if err := c.host.RunQuiet("incus", "remote", "add", name, "unix://"+HostSocketFile()); err != nil { return err } } // if activate, set default to new remote if activate { return c.host.RunQuiet("incus", "remote", "switch", name) } return nil } func (c incusRuntime) unsetRemote() error { // if default remote, set default to local if c.isDefaultRemote() { if err := c.host.RunQuiet("incus", "remote", "switch", "local"); err != nil { return err } } // if has remote, remove remote if c.hasRemote(config.CurrentProfile().ID) { return c.host.RunQuiet("incus", "remote", "remove", config.CurrentProfile().ID) } return nil } func (c incusRuntime) hasRemote(name string) bool { remotes, err := c.fetchRemotes() if err != nil { return false } _, ok := remotes[name] return ok } func (c incusRuntime) fetchRemotes() (remoteInfo, error) { b, err := c.host.RunOutput("incus", "remote", "list", "--format", "json") if err != nil { return nil, fmt.Errorf("error fetching remotes: %w", err) } var remotes remoteInfo if err := json.NewDecoder(strings.NewReader(b)).Decode(&remotes); err != nil { return nil, fmt.Errorf("error decoding remotes response: %w", err) } return remotes, nil } func (c incusRuntime) isDefaultRemote() bool { remote, _ := c.host.RunOutput("incus", "remote", "get-default") return remote == config.CurrentProfile().ID } func (c incusRuntime) addDockerRemote() error { if c.hasRemote("docker") { // already added return nil } return c.host.RunQuiet("incus", "remote", "add", "docker", "https://docker.io", "--protocol=oci") } func (c incusRuntime) findNetwork(interfaceName string) (found bool, info networkInfo, err error) { b, err := c.guest.RunOutput("sudo", "incus", "network", "list", "--format", "json") if err != nil { return found, info, fmt.Errorf("error listing networks: %w", err) } var resp []networkInfo if err := json.NewDecoder(strings.NewReader(b)).Decode(&resp); err != nil { return found, info, fmt.Errorf("error decoding networks into struct: %w", err) } for _, n := range resp { if n.Name == interfaceName { return true, n, nil } } return } //go:embed config.yaml var configYaml string type remoteInfo map[string]struct { Addr string `json:"Addr"` } type networkInfo struct { Name string `json:"name"` Managed bool `json:"managed"` Type string `json:"type"` } func (c *incusRuntime) Update(ctx context.Context) (bool, error) { packages := []string{ "incus", "incus-base", "incus-client", "incus-extra", "incus-ui-canonical", } return debutil.UpdateRuntime(ctx, c.guest, c, packages...) } func (c *incusRuntime) poolImported() bool { script := strings.NewReplacer( "{pool_name}", poolName, ).Replace("sudo zpool list -H -o name | grep '^{pool_name}$'") return c.guest.RunQuiet("sh", "-c", script) == nil } func (c *incusRuntime) recoverDisk(ctx context.Context) error { var disks []string str, err := c.guest.RunOutput("sh", "-c", "sudo ls "+poolDisksDir+" | grep '.img$'") if err != nil { return fmt.Errorf("cannot list storage pool disks: %w", err) } disks = strings.Fields(str) if len(disks) == 0 { return fmt.Errorf("no existing storage pool disks found") } log := c.Logger(ctx) log.Println() log.Println("Running 'incus admin recover' ...") log.Println() log.Println(fmt.Sprintf("Found %d storage pool source(s):", len(disks))) for _, disk := range disks { log.Println(" " + poolDisksDir + "/" + disk) } log.Println() if err := c.guest.RunInteractive("sudo", "incus", "admin", "recover"); err != nil { return fmt.Errorf("error recovering storage pool: %w", err) } out, err := c.guest.RunOutput("sudo", "incus", "storage", "list", "name="+poolName, "-c", "n", "--format", "compact,noheader") if err != nil { return err } if out != poolName { return fmt.Errorf("default storage pool recovery failure") } return nil } func (c *incusRuntime) wipeDisk(size int) error { // prepare by deleting relevant files/directories deleteScript := strings.NewReplacer( "{disk_file}", poolDiskFile, "{meta_dir}", poolMetaDir, ).Replace("sudo rm -rf {disk_file} {meta_dir}") if err := c.guest.RunQuiet("sh", "-c", deleteScript); err != nil { return fmt.Errorf("error preparing storage pools directory: %w", err) } // create new storage pool var diskSize = fmt.Sprintf("%dGiB", size) return c.guest.RunQuiet("sudo", "incus", "storage", "create", poolName, storageDriver, "size="+diskSize) } // migrationScript returns a script that migrates from the old disk layout // (separate incus-disks and incus-backups subdirectories) to the new layout // (full /var/lib/incus directory). func migrationScript() string { mountPoint := limautil.MountPoint() return `MOUNT_POINT="` + mountPoint + `" if [ -d "$MOUNT_POINT/incus-disks" ] && [ ! -d "$MOUNT_POINT/incus" ]; then mkdir -p "$MOUNT_POINT/incus" if [ -d /var/lib/incus ]; then cp -a /var/lib/incus/. "$MOUNT_POINT/incus/" fi rm -rf "$MOUNT_POINT/incus/disks" mv "$MOUNT_POINT/incus-disks" "$MOUNT_POINT/incus/disks" if [ -d "$MOUNT_POINT/incus-backups" ]; then rm -rf "$MOUNT_POINT/incus/backups" mv "$MOUNT_POINT/incus-backups" "$MOUNT_POINT/incus/backups" fi fi` } // DataDisk represents the data disk for the container runtime. func DataDisk() environment.DataDisk { return environment.DataDisk{ FSType: "ext4", Dirs: []environment.DiskDir{ {Name: "incus", Path: "/var/lib/incus"}, }, PreMount: []string{ "systemctl stop incus.service || true", "systemctl stop incus.socket || true", migrationScript(), }, } } ================================================ FILE: environment/container/incus/route.go ================================================ package incus import ( "fmt" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/embedded" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/abiosoft/colima/util" log "github.com/sirupsen/logrus" ) const BridgeSubnet = "192.168.100.0/24" const bridgeGateway = "192.168.100.1/24" // addContainerRoute adds a macOS route for the Incus container subnet // via the VM's col0 IP address, making containers directly reachable from the host. func (c *incusRuntime) addContainerRoute() error { if !util.MacOS() { return nil } vmIP := limautil.IPAddress(config.CurrentProfile().ID) if vmIP == "127.0.0.1" || vmIP == "" { return nil } if !util.SubnetAvailable(BridgeSubnet) { log.Warnf("subnet %s conflicts with host network, skipping route setup", BridgeSubnet) return nil } if err := embedded.InstallSudoers(c.host); err != nil { return fmt.Errorf("error setting up sudoers for route: %w", err) } // delete any stale route first (ignore errors) _ = c.removeContainerRoute() if err := c.host.RunQuiet("sudo", "/sbin/route", "add", "-net", BridgeSubnet, vmIP); err != nil { return fmt.Errorf("error adding route for %s via %s: %w", BridgeSubnet, vmIP, err) } return nil } // removeContainerRoute removes the macOS route for the Incus container subnet. func (c *incusRuntime) removeContainerRoute() error { if !util.MacOS() { return nil } if !util.RouteExists(BridgeSubnet) { return nil } return c.host.RunQuiet("sudo", "/sbin/route", "delete", "-net", BridgeSubnet) } ================================================ FILE: environment/container/kubernetes/cni.go ================================================ package kubernetes import ( _ "embed" "fmt" "path/filepath" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/embedded" "github.com/abiosoft/colima/environment" ) func installCniConfig(guest environment.GuestActions, a *cli.ActiveCommandChain) { // fix cni config a.Add(func() error { flannelFile := "/etc/cni/net.d/10-flannel.conflist" cniConfDir := filepath.Dir(flannelFile) if err := guest.Run("sudo", "mkdir", "-p", cniConfDir); err != nil { return fmt.Errorf("error creating cni config dir: %w", err) } flannel, err := embedded.Read("k3s/flannel.json") if err != nil { return fmt.Errorf("error reading embedded flannel config: %w", err) } return guest.Write(flannelFile, flannel) }) } ================================================ FILE: environment/container/kubernetes/k3s.go ================================================ package kubernetes import ( "fmt" "strconv" "strings" "time" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/container/containerd" "github.com/abiosoft/colima/environment/container/docker" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/downloader" "github.com/sirupsen/logrus" ) const listenPortKey = "k3s_listen_port" func hasK3sArg(k3sArgs []string, argName string) bool { for _, arg := range k3sArgs { if strings.HasPrefix(arg, argName+"=") { return true } if strings.HasPrefix(arg, argName+" ") { return true } if arg == argName { return true } } return false } func installK3s(host environment.HostActions, guest environment.GuestActions, a *cli.ActiveCommandChain, log *logrus.Entry, containerRuntime string, k3sVersion string, k3sArgs []string, k3sListenPort int, ) { installK3sBinary(host, guest, a, k3sVersion) installK3sCache(host, guest, a, log, containerRuntime, k3sVersion) installK3sCluster(host, guest, a, containerRuntime, k3sVersion, k3sArgs, k3sListenPort) } func installK3sBinary( host environment.HostActions, guest environment.GuestActions, a *cli.ActiveCommandChain, k3sVersion string, ) { downloadPath := "/tmp/k3s" baseURL := "https://github.com/k3s-io/k3s/releases/download/" + k3sVersion + "/" shaSumTxt := "sha256sum-" + guest.Arch().GoArch() + ".txt" url := baseURL + "k3s" shaURL := baseURL + shaSumTxt if guest.Arch().GoArch() == "arm64" { url += "-arm64" } a.Add(func() error { r := downloader.Request{ URL: url, SHA: &downloader.SHA{Size: 256, URL: shaURL}, } return downloader.DownloadToGuest(host, guest, r, downloadPath) }) a.Add(func() error { return guest.Run("sudo", "install", downloadPath, "/usr/local/bin/k3s") }) } func installK3sCache( host environment.HostActions, guest environment.GuestActions, a *cli.ActiveCommandChain, log *logrus.Entry, containerRuntime string, k3sVersion string, ) { baseURL := "https://github.com/k3s-io/k3s/releases/download/" + k3sVersion + "/" imageTar := "k3s-airgap-images-" + guest.Arch().GoArch() + ".tar" shaSumTxt := "sha256sum-" + guest.Arch().GoArch() + ".txt" imageTarGz := imageTar + ".gz" downloadPathTar := "/tmp/" + imageTar downloadPathTarGz := "/tmp/" + imageTarGz url := baseURL + imageTarGz shaURL := baseURL + shaSumTxt a.Add(func() error { r := downloader.Request{ URL: url, SHA: &downloader.SHA{Size: 256, URL: shaURL}, } return downloader.DownloadToGuest(host, guest, r, downloadPathTarGz) }) a.Add(func() error { return guest.Run("gzip", "-f", "-d", downloadPathTarGz) }) airGapDir := "/var/lib/rancher/k3s/agent/images/" a.Add(func() error { return guest.Run("sudo", "mkdir", "-p", airGapDir) }) a.Add(func() error { return guest.Run("sudo", "cp", downloadPathTar, airGapDir) }) // load OCI images for K3s // this can be safely ignored if failed as the images would be pulled afterwards. switch containerRuntime { case containerd.Name: a.Stage("loading oci images") a.Add(func() error { if err := guest.Run("sudo", "nerdctl", "-n", "k8s.io", "load", "-i", downloadPathTar, "--all-platforms"); err != nil { log.Warnln(fmt.Errorf("error loading oci images: %w", err)) log.Warnln("startup may delay a bit as images will be pulled from oci registry") } return nil }) case docker.Name: a.Stage("loading oci images") a.Add(func() error { if err := guest.Run("sudo", "docker", "load", "-i", downloadPathTar); err != nil { log.Warnln(fmt.Errorf("error loading oci images: %w", err)) log.Warnln("startup may delay a bit as images will be pulled from oci registry") } return nil }) } } func installK3sCluster( host environment.HostActions, guest environment.GuestActions, a *cli.ActiveCommandChain, containerRuntime string, k3sVersion string, k3sArgs []string, k3sListenPort int, ) { // install k3s last to ensure it is the last step downloadPath := "/tmp/k3s-install.sh" url := "https://raw.githubusercontent.com/k3s-io/k3s/" + k3sVersion + "/install.sh" a.Add(func() error { r := downloader.Request{URL: url} return downloader.DownloadToGuest(host, guest, r, downloadPath) }) a.Add(func() error { return guest.Run("sudo", "install", downloadPath, "/usr/local/bin/k3s-install.sh") }) args := append([]string{ "--write-kubeconfig-mode", "644", }, k3sArgs...) a.Retry("waiting for VM IP address", time.Second*5, 4, func(retryCount int) error { ipAddress := limautil.IPAddress(config.CurrentProfile().ID) if ipAddress == "" { return fmt.Errorf("no IP address assigned to network interface") } if ipAddress == "127.0.0.1" { args = append(args, "--flannel-iface", "eth0") } else { if !hasK3sArg(k3sArgs, "--advertise-address") { args = append(args, "--advertise-address", ipAddress) } if !hasK3sArg(k3sArgs, "--flannel-iface") { args = append(args, "--flannel-iface", limautil.NetInterface) } } return nil }) switch containerRuntime { case docker.Name: args = append(args, "--docker") case containerd.Name: args = append(args, "--container-runtime-endpoint", "unix:///run/containerd/containerd.sock") } a.Add(func() error { port, err := getPortNumber(guest, k3sListenPort) if err != nil { return err } args = append(args, "--https-listen-port", strconv.Itoa(port)) return nil }) a.Add(func() error { return guest.Run("sh", "-c", "INSTALL_K3S_SKIP_DOWNLOAD=true INSTALL_K3S_SKIP_ENABLE=true k3s-install.sh "+strings.Join(args, " ")) }) } // getPortNumber retrieves the previously set port number. // If missing, an available random port is set and return. func getPortNumber(guest environment.GuestActions, k3sListenPort int) (int, error) { // port previously set, reuse it if port, err := strconv.Atoi(guest.Get(listenPortKey)); err == nil && port > 0 { return port, nil } // for backward compatibility // if the instance already exists, assume default port 6443 if m := guest.Get(masterAddressKey); m != "" { return 6443, nil } var port int if k3sListenPort > 0 { // template configured port port = k3sListenPort } else { // new instance, assign random port port = util.RandomAvailablePort() } if err := guest.Set(listenPortKey, strconv.Itoa(port)); err != nil { return 0, err } return port, nil } ================================================ FILE: environment/container/kubernetes/kubeconfig.go ================================================ package kubernetes import ( "context" "fmt" "path/filepath" "strings" "time" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment/vm/lima/limautil" ) const masterAddressKey = "master_address" func (c kubernetesRuntime) provisionKubeconfig(ctx context.Context) error { ip := limautil.IPAddress(config.CurrentProfile().ID) if ip == c.guest.Get(masterAddressKey) { return nil } log := c.Logger(ctx) a := c.Init(ctx) a.Stage("updating config") // remove existing configs (if any) // this is safe as the profile name is unique to colima c.unsetKubeconfig(a) // ensure host kube directory exists hostHome := c.host.Env("HOME") if hostHome == "" { return fmt.Errorf("error retrieving home directory on host") } profile := config.CurrentProfile().ID hostKubeDir := filepath.Join(hostHome, ".kube") a.Add(func() error { return c.host.Run("mkdir", "-p", filepath.Join(hostKubeDir, "."+profile)) }) kubeconfFile := filepath.Join(hostKubeDir, "config") envKubeConfFile := c.host.Env("KUBECONFIG") if envKubeConfFile != "" { kubeconfFile = filepath.SplitList(envKubeConfFile)[0] } tmpkubeconfFile := filepath.Join(hostKubeDir, "."+profile, "colima-temp") // manipulate in VM and save to host a.Add(func() error { kubeconfig, err := c.guest.Read("/etc/rancher/k3s/k3s.yaml") if err != nil { return fmt.Errorf("error fetching kubeconfig on guest: %w", err) } // replace name kubeconfig = strings.ReplaceAll(kubeconfig, ": default", ": "+profile) // replace IP if ip != "" && ip != "127.0.0.1" { kubeconfig = strings.ReplaceAll(kubeconfig, "https://127.0.0.1:", "https://"+ip+":") } // save on the host return c.host.Write(tmpkubeconfFile, []byte(kubeconfig)) }) // merge on host a.Add(func() (err error) { // prepare new host with right env var. envVar := fmt.Sprintf("KUBECONFIG=%s:%s", kubeconfFile, tmpkubeconfFile) host := c.host.WithEnv(envVar) // get merged config kubeconfig, err := host.RunOutput("kubectl", "config", "view", "--raw") if err != nil { return err } // save return host.Write(tmpkubeconfFile, []byte(kubeconfig)) }) // backup current settings and save new config a.Add(func() error { // backup existing file if exists if stat, err := c.host.Stat(kubeconfFile); err == nil && !stat.IsDir() { backup := filepath.Join(filepath.Dir(tmpkubeconfFile), fmt.Sprintf("config-bak-%d", time.Now().Unix())) if err := c.host.Run("cp", kubeconfFile, backup); err != nil { return fmt.Errorf("error backing up kubeconfig: %w", err) } } // save new config if err := c.host.Run("cp", tmpkubeconfFile, kubeconfFile); err != nil { return fmt.Errorf("error updating kubeconfig: %w", err) } return nil }) // set new context conf, _ := ctx.Value(config.CtxKey()).(config.Config) if conf.AutoActivate() { a.Add(func() error { out, err := c.host.RunOutput("kubectl", "config", "use-context", profile) if err != nil { return err } log.Println(out) return nil }) } // save settings a.Add(func() error { return c.guest.Set(masterAddressKey, ip) }) return a.Exec() } func (c kubernetesRuntime) unsetKubeconfig(a *cli.ActiveCommandChain) { profile := config.CurrentProfile().ID a.Add(func() error { return c.host.Run("kubectl", "config", "unset", "users."+profile) }) a.Add(func() error { return c.host.Run("kubectl", "config", "unset", "contexts."+profile) }) a.Add(func() error { return c.host.Run("kubectl", "config", "unset", "clusters."+profile) }) // kubectl config unset current-context a.Add(func() error { if c, _ := c.host.RunOutput("kubectl", "config", "current-context"); c != config.CurrentProfile().ID { return nil } return c.host.Run("kubectl", "config", "unset", "current-context") }) } func (c kubernetesRuntime) teardownKubeconfig(a *cli.ActiveCommandChain) { a.Stage("reverting config") c.unsetKubeconfig(a) a.Add(func() error { return c.guest.Set(masterAddressKey, "") }) } ================================================ FILE: environment/container/kubernetes/kubernetes.go ================================================ package kubernetes import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/container/containerd" "github.com/abiosoft/colima/environment/container/docker" "github.com/abiosoft/colima/environment/guest/systemctl" ) // Name is container runtime name const ( Name = "kubernetes" DefaultVersion = "v1.35.0+k3s1" ConfigKey = "kubernetes_config" ) func newRuntime(host environment.HostActions, guest environment.GuestActions) environment.Container { return &kubernetesRuntime{ host: host, guest: guest, systemctl: systemctl.New(guest), CommandChain: cli.New(Name), } } func init() { environment.RegisterContainer(Name, newRuntime, true) } var _ environment.Container = (*kubernetesRuntime)(nil) type kubernetesRuntime struct { host environment.HostActions guest environment.GuestActions systemctl systemctl.Systemctl cli.CommandChain } func (c kubernetesRuntime) Name() string { return Name } func (c kubernetesRuntime) isInstalled() bool { // it is installed if uninstall script is present. return c.guest.RunQuiet("command", "-v", "k3s-uninstall.sh") == nil } func (c kubernetesRuntime) isVersionInstalled(version string) bool { // validate version change via cli flag/config. out, err := c.guest.RunOutput("k3s", "--version") if err != nil { return false } return strings.Contains(out, version) } func (c kubernetesRuntime) Running(context.Context) bool { return c.guest.RunQuiet("sudo", "service", "k3s", "status") == nil } func (c kubernetesRuntime) runtime() string { return c.guest.Get(environment.ContainerRuntimeKey) } func (c kubernetesRuntime) config() config.Kubernetes { conf := config.Kubernetes{Version: DefaultVersion} if b := c.guest.Get(ConfigKey); b != "" { _ = json.Unmarshal([]byte(b), &conf) } return conf } func (c kubernetesRuntime) setConfig(conf config.Kubernetes) error { b, err := json.Marshal(conf) if err != nil { return fmt.Errorf("error encoding kubernetes config to json: %w", err) } return c.guest.Set(ConfigKey, string(b)) } func (c *kubernetesRuntime) Provision(ctx context.Context) error { log := c.Logger(ctx) a := c.Init(ctx) if c.Running(ctx) { return nil } appConf, ok := ctx.Value(config.CtxKey()).(config.Config) runtime := appConf.Runtime conf := appConf.Kubernetes if !ok { // this should be a restart/start while vm is active // retrieve value in the vm runtime = c.runtime() conf = c.config() } if conf.Version == "" { // this ensure if `version` tag in `kubernetes` section in yaml is empty, // it should assign with the `DefaultVersion` for the baseURL conf.Version = DefaultVersion } if c.isVersionInstalled(conf.Version) { // runtime has changed, ensure the required images are in the registry if currentRuntime := c.runtime(); currentRuntime != "" && currentRuntime != runtime { a.Stagef("changing runtime to %s", runtime) installK3sCache(c.host, c.guest, a, log, runtime, conf.Version) } // other settings may have changed e.g. ingress installK3sCluster(c.host, c.guest, a, runtime, conf.Version, conf.K3sArgs, conf.Port) } else { if c.isInstalled() { a.Stagef("version changed to %s, downloading and installing", conf.Version) } else { if ok { a.Stage("downloading and installing") } else { a.Stage("installing") } } installK3s(c.host, c.guest, a, log, runtime, conf.Version, conf.K3sArgs, conf.Port) } // this needs to happen on each startup { // cni is used by both cri-dockerd and containerd installCniConfig(c.guest, a) } // provision successful, now we can persist the version a.Add(func() error { return c.setConfig(conf) }) return a.Exec() } func (c kubernetesRuntime) Start(ctx context.Context) error { log := c.Logger(ctx) a := c.Init(ctx) if c.Running(ctx) { log.Println("already running") return nil } a.Add(func() error { return c.systemctl.Start("k3s.service") }) a.Retry("", time.Second*2, 10, func(int) error { return c.guest.RunQuiet("kubectl", "cluster-info") }) if err := a.Exec(); err != nil { return err } return c.provisionKubeconfig(ctx) } func (c kubernetesRuntime) Stop(ctx context.Context, force bool) error { a := c.Init(ctx) a.Add(func() error { return c.guest.Run("k3s-killall.sh") }) // k3s is buggy with external containerd for now // cleanup is manual if !force { a.Add(c.stopAllContainers) } return a.Exec() } func (c kubernetesRuntime) deleteAllContainers() error { ids := c.runningContainerIDs() if ids == "" { return nil } var args []string switch c.runtime() { case containerd.Name: args = []string{"nerdctl", "-n", "k8s.io", "rm", "-f"} case docker.Name: args = []string{"docker", "rm", "-f"} default: return nil } args = append(args, strings.Fields(ids)...) return c.guest.Run("sudo", "sh", "-c", strings.Join(args, " ")) } func (c kubernetesRuntime) stopAllContainers() error { ids := c.runningContainerIDs() if ids == "" { return nil } var args []string switch c.runtime() { case containerd.Name: args = []string{"nerdctl", "-n", "k8s.io", "kill"} case docker.Name: args = []string{"docker", "kill"} default: return nil } args = append(args, strings.Fields(ids)...) return c.guest.Run("sudo", "sh", "-c", strings.Join(args, " ")) } func (c kubernetesRuntime) runningContainerIDs() string { var args []string switch c.runtime() { case containerd.Name: args = []string{"sudo", "nerdctl", "-n", "k8s.io", "ps", "-q"} case docker.Name: args = []string{"sudo", "sh", "-c", `docker ps --format '{{.Names}}'| grep "k8s_"`} default: return "" } ids, _ := c.guest.RunOutput(args...) if ids == "" { return "" } return strings.ReplaceAll(ids, "\n", " ") } func (c kubernetesRuntime) Teardown(ctx context.Context) error { a := c.Init(ctx) if c.isInstalled() { a.Add(func() error { return c.guest.Run("k3s-uninstall.sh") }) } // k3s is buggy with external containerd for now // cleanup is manual a.Add(c.deleteAllContainers) c.teardownKubeconfig(a) return a.Exec() } func (c kubernetesRuntime) Dependencies() []string { return []string{"kubectl"} } func (c kubernetesRuntime) Version(context.Context) string { version, _ := c.host.RunOutput("kubectl", "--context", config.CurrentProfile().ID, "version", "--short") return version } func (c *kubernetesRuntime) Update(ctx context.Context) (bool, error) { return false, fmt.Errorf("update not supported for the %s runtime", Name) } ================================================ FILE: environment/container.go ================================================ package environment import ( "context" "fmt" "log" ) // IsNoneRuntime returns if runtime is none. func IsNoneRuntime(runtime string) bool { return runtime == "none" } // Container is container environment. type Container interface { // Name is the name of the container runtime. e.g. docker, containerd Name() string // Provision provisions/installs the container runtime. // Should be idempotent. Provision(ctx context.Context) error // Start starts the container runtime. Start(ctx context.Context) error // Stop stops the container runtime. // If force is true, the runtime is killed immediately without graceful shutdown. Stop(ctx context.Context, force bool) error // Teardown tears down/uninstall the container runtime. Teardown(ctx context.Context) error // Update the container runtime. Update(ctx context.Context) (bool, error) // Version returns the container runtime version. Version(ctx context.Context) string // Running returns if the container runtime is currently running. Running(ctx context.Context) bool Dependencies } // NewContainer creates a new container environment. func NewContainer(runtime string, host HostActions, guest GuestActions) (Container, error) { if _, ok := containerRuntimes[runtime]; !ok { return nil, fmt.Errorf("unsupported container runtime '%s'", runtime) } return containerRuntimes[runtime].Func(host, guest), nil } // NewContainerFunc is implemented by container runtime implementations to create a new instance. type NewContainerFunc func(host HostActions, guest GuestActions) Container var containerRuntimes = map[string]containerRuntimeFunc{} type containerRuntimeFunc struct { Func NewContainerFunc Hidden bool } // RegisterContainer registers a new container runtime. // If hidden is true, the container is not displayed as an available runtime. func RegisterContainer(name string, f NewContainerFunc, hidden bool) { if _, ok := containerRuntimes[name]; ok { log.Fatalf("container runtime '%s' already registered", name) } containerRuntimes[name] = containerRuntimeFunc{Func: f, Hidden: hidden} } // ContainerRuntimes return the names of available container runtimes. func ContainerRuntimes() (names []string) { for name, cont := range containerRuntimes { if cont.Hidden { continue } names = append(names, name) } return } // DataDisk holds the configuration for mounting an external runtime disk. type DataDisk struct { Dirs []DiskDir // the directories to be mounted PreMount []string // the scripts to run before mounting the directories FSType string // the filesystem type for the disk e.g. ext4 } // DiskDir is a directory mounted in a data disk. type DiskDir struct { Name string Path string } ================================================ FILE: environment/environment.go ================================================ package environment import ( "context" "io" "os" "github.com/abiosoft/colima/config" ) type runActions interface { // Run runs command Run(args ...string) error // RunQuiet runs command whilst suppressing the output. // Useful for commands that only the exit code matters. RunQuiet(args ...string) error // RunOutput runs command and returns its output. RunOutput(args ...string) (string, error) // RunInteractive runs command interactively. RunInteractive(args ...string) error // RunWith runs with stdin and stdout. RunWith(stdin io.Reader, stdout io.Writer, args ...string) error } type fileActions interface { Read(fileName string) (string, error) Write(fileName string, body []byte) error Stat(fileName string) (os.FileInfo, error) } // HostActions are actions performed on the host. type HostActions interface { runActions fileActions // WithEnv creates a new instance based on the current instance // with the specified environment variables. WithEnv(env ...string) HostActions // WithDir creates a new instance based on the current instance // with the working directory set to dir. WithDir(dir string) HostActions // Env retrieves environment variable on the host. Env(string) string } // GuestActions are actions performed on the guest i.e. VM. type GuestActions interface { runActions fileActions // Start starts up the VM Start(ctx context.Context, conf config.Config) error // Stop shuts down the VM Stop(ctx context.Context, force bool) error // Restart restarts the VM Restart(ctx context.Context) error // SSH performs an ssh connection to the VM SSH(workingDir string, args ...string) error // Created returns if the VM has been previously created. Created() bool // Running returns if the VM is currently running. Running(ctx context.Context) bool // Env retrieves environment variable in the VM. Env(string) (string, error) // Get retrieves a configuration in the VM. Get(key string) string // Set sets configuration in the VM. Set(key, value string) error // User returns the username of the user in the VM. User() (string, error) // Arch returns the architecture of the VM. Arch() Arch } // Dependencies are dependencies that must exist on the host. type Dependencies interface { // Dependencies are dependencies that must exist on the host. // TODO this may need to accommodate non-brew installable dependencies Dependencies() []string } ================================================ FILE: environment/guest/systemctl/systemctl.go ================================================ package systemctl import "github.com/abiosoft/colima/environment" // Runner is the subset of environment.GuestActions that Systemctl requires. // Using a narrow interface makes Systemctl easier to test and more loosely coupled. type Runner interface { Run(args ...string) error RunQuiet(args ...string) error } // compile-time check: environment.GuestActions satisfies runner. var _ Runner = (environment.GuestActions)(nil) // Systemctl provides a typed wrapper for running systemctl commands in the guest VM. type Systemctl struct { runner Runner } // New creates a new Systemctl instance backed by the given guest. func New(guest Runner) Systemctl { return Systemctl{runner: guest} } // Start starts a systemd service. func (s Systemctl) Start(service string) error { return s.runner.Run("sudo", "systemctl", "start", service) } // Restart restarts a systemd service. func (s Systemctl) Restart(service string) error { return s.runner.Run("sudo", "systemctl", "restart", service) } // Stop stops a systemd service. If force is true, it is killed immediately without graceful shutdown. func (s Systemctl) Stop(service string, force bool) error { verb := "stop" if force { verb = "kill" } return s.runner.Run("sudo", "systemctl", verb, service) } // Active returns whether a systemd service is currently active. func (s Systemctl) Active(service string) bool { return s.runner.RunQuiet("systemctl", "is-active", service) == nil } // DaemonReload reloads the systemd manager configuration. func (s Systemctl) DaemonReload() error { return s.runner.Run("sudo", "systemctl", "daemon-reload") } ================================================ FILE: environment/guest/systemctl/systemctl_test.go ================================================ package systemctl import ( "os" "testing" ) // mockGuest records args passed to Run/RunQuiet and controls whether they succeed. type mockGuest struct { lastArgs []string err error } func (m *mockGuest) Run(args ...string) error { m.lastArgs = args; return m.err } func (m *mockGuest) RunQuiet(args ...string) error { m.lastArgs = args; return m.err } func TestStart(t *testing.T) { g := &mockGuest{} s := New(g) if err := s.Start("docker.service"); err != nil { t.Fatalf("unexpected error: %v", err) } assertArgs(t, g.lastArgs, []string{"sudo", "systemctl", "start", "docker.service"}) } func TestRestart(t *testing.T) { g := &mockGuest{} s := New(g) if err := s.Restart("containerd.service"); err != nil { t.Fatalf("unexpected error: %v", err) } assertArgs(t, g.lastArgs, []string{"sudo", "systemctl", "restart", "containerd.service"}) } func TestStop(t *testing.T) { tests := []struct { name string force bool wantVerb string }{ {name: "graceful", force: false, wantVerb: "stop"}, {name: "force", force: true, wantVerb: "kill"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := &mockGuest{} s := New(g) if err := s.Stop("docker.service", tt.force); err != nil { t.Fatalf("unexpected error: %v", err) } assertArgs(t, g.lastArgs, []string{"sudo", "systemctl", tt.wantVerb, "docker.service"}) }) } } func TestActive(t *testing.T) { tests := []struct { name string guestOK bool want bool }{ {name: "active", guestOK: true, want: true}, {name: "inactive", guestOK: false, want: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { g := &mockGuest{} if !tt.guestOK { g.err = os.ErrProcessDone } s := New(g) got := s.Active("docker.service") if got != tt.want { t.Errorf("Active() = %v, want %v", got, tt.want) } assertArgs(t, g.lastArgs, []string{"systemctl", "is-active", "docker.service"}) }) } } func TestDaemonReload(t *testing.T) { g := &mockGuest{} s := New(g) if err := s.DaemonReload(); err != nil { t.Fatalf("unexpected error: %v", err) } assertArgs(t, g.lastArgs, []string{"sudo", "systemctl", "daemon-reload"}) } // assertArgs fails the test if got and want differ. func assertArgs(t *testing.T, got, want []string) { t.Helper() if len(got) != len(want) { t.Errorf("args = %v, want %v", got, want) return } for i := range want { if got[i] != want[i] { t.Errorf("args[%d] = %q, want %q (full: %v)", i, got[i], want[i], got) } } } ================================================ FILE: environment/host/host.go ================================================ package host import ( "bytes" "errors" "fmt" "io" "os" "os/exec" "strconv" "strings" "github.com/abiosoft/colima/util/terminal" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/environment" ) // New creates a new host environment. func New() environment.Host { return &hostEnv{} } var _ environment.Host = (*hostEnv)(nil) type hostEnv struct { env []string dir string // working directory } func (h hostEnv) clone() hostEnv { var newHost hostEnv newHost.env = append(newHost.env, h.env...) newHost.dir = h.dir return newHost } func (h hostEnv) WithEnv(env ...string) environment.HostActions { newHost := h.clone() // append new env vars newHost.env = append(newHost.env, env...) return newHost } func (h hostEnv) WithDir(dir string) environment.HostActions { newHost := h.clone() newHost.dir = dir return newHost } func (h hostEnv) Run(args ...string) error { if len(args) == 0 { return errors.New("args not specified") } cmd := cli.Command(args[0], args[1:]...) cmd.Env = append(os.Environ(), h.env...) if h.dir != "" { cmd.Dir = h.dir } lineHeight := 6 if cli.Settings.Verbose { lineHeight = -1 // disable scrolling } out := terminal.NewVerboseWriter(lineHeight) cmd.Stdout = out cmd.Stderr = out err := cmd.Run() if err == nil { return out.Close() } return err } func (h hostEnv) RunQuiet(args ...string) error { if len(args) == 0 { return errors.New("args not specified") } cmd := cli.Command(args[0], args[1:]...) cmd.Env = append(os.Environ(), h.env...) if h.dir != "" { cmd.Dir = h.dir } var errBuf bytes.Buffer cmd.Stdout = nil cmd.Stderr = &errBuf err := cmd.Run() if err != nil { return errCmd(cmd.Args, errBuf, err) } return nil } func (h hostEnv) RunOutput(args ...string) (string, error) { if len(args) == 0 { return "", errors.New("args not specified") } cmd := cli.Command(args[0], args[1:]...) cmd.Env = append(os.Environ(), h.env...) if h.dir != "" { cmd.Dir = h.dir } var buf, errBuf bytes.Buffer cmd.Stdout = &buf cmd.Stderr = &errBuf err := cmd.Run() if err != nil { return "", errCmd(cmd.Args, errBuf, err) } return strings.TrimSpace(buf.String()), nil } func errCmd(args []string, stderr bytes.Buffer, err error) error { // this is going to be part of a log output, // reading the first line of the error should suffice output, _ := stderr.ReadString('\n') if len(output) > 0 { output = output[:len(output)-1] } return fmt.Errorf("error running %v, output: %s, err: %s", args, strconv.Quote(output), strconv.Quote(err.Error())) } func (h hostEnv) RunInteractive(args ...string) error { if len(args) == 0 { return errors.New("args not specified") } cmd := cli.CommandInteractive(args[0], args[1:]...) cmd.Env = append(os.Environ(), h.env...) if h.dir != "" { cmd.Dir = h.dir } return cmd.Run() } func (h hostEnv) RunWith(stdin io.Reader, stdout io.Writer, args ...string) error { if len(args) == 0 { return errors.New("args not specified") } cmd := cli.CommandInteractive(args[0], args[1:]...) cmd.Env = append(os.Environ(), h.env...) if h.dir != "" { cmd.Dir = h.dir } cmd.Stdin = stdin cmd.Stdout = stdout var buf bytes.Buffer cmd.Stderr = &buf if err := cmd.Run(); err != nil { return errCmd(cmd.Args, buf, err) } return nil } func (h hostEnv) Env(s string) string { return os.Getenv(s) } func (h hostEnv) Read(fileName string) (string, error) { b, err := os.ReadFile(fileName) return string(b), err } func (h hostEnv) Write(fileName string, body []byte) error { return os.WriteFile(fileName, body, 0644) } func (h hostEnv) Stat(fileName string) (os.FileInfo, error) { return os.Stat(fileName) } // IsInstalled checks if dependencies are installed. func IsInstalled(dependencies environment.Dependencies) error { var missing []string check := func(p string) error { _, err := exec.LookPath(p) return err } for _, p := range dependencies.Dependencies() { if check(p) != nil { missing = append(missing, p) } } if len(missing) > 0 { return fmt.Errorf("%s not found, run 'brew install %s' to install", strings.Join(missing, ", "), strings.Join(missing, " ")) } return nil } ================================================ FILE: environment/host.go ================================================ package environment // Host is the host environment. type Host interface { HostActions } ================================================ FILE: environment/vm/lima/certs.go ================================================ package lima import ( "context" "fmt" "path/filepath" "github.com/abiosoft/colima/environment/container/docker" "github.com/abiosoft/colima/util/downloader" ) func (l limaVM) copyCerts() error { log := l.Logger(context.Background()) err := func() error { dockerCertsDirHost := filepath.Join(docker.DockerDir(), "certs.d") dockerCertsDirsGuest := []string{"/etc/docker/certs.d", "/etc/ssl/certs"} if _, err := l.host.Stat(dockerCertsDirHost); err != nil { // no certs found return nil } // copy certs from host to a temp location in the guest using limactl copy, // then use sudo to move them to the final destinations. tmpDir := "/tmp/docker-certs" if err := l.RunQuiet("rm", "-rf", tmpDir); err != nil { return err } if err := l.RunQuiet("mkdir", "-p", tmpDir); err != nil { return err } if err := downloader.CopyToGuest(l.host, dockerCertsDirHost, tmpDir); err != nil { return err } // move from temp to final destinations for _, dir := range dockerCertsDirsGuest { if err := l.RunQuiet("sudo", "mkdir", "-p", dir); err != nil { return err } if err := l.RunQuiet("sudo", "cp", "-R", tmpDir+"/.", dir); err != nil { return err } } // cleanup temp _ = l.RunQuiet("rm", "-rf", tmpDir) return nil }() // not a fatal error, a warning suffices. if err != nil { log.Warnln(fmt.Errorf("cannot copy registry certs to vm: %w", err)) } return nil } ================================================ FILE: environment/vm/lima/config.go ================================================ package lima import ( "context" "encoding/json" "fmt" "path/filepath" ) const configFile = "/etc/colima/colima.json" func (l limaVM) getConf() map[string]string { log := l.Logger(context.Background()) obj := map[string]string{} b, err := l.Read(configFile) if err != nil { log.Trace(fmt.Errorf("error reading config file: %w", err)) return obj } // we do not care if it fails _ = json.Unmarshal([]byte(b), &obj) return obj } func (l limaVM) Get(key string) string { if val, ok := l.getConf()[key]; ok { return val } return "" } func (l limaVM) Set(key, value string) error { obj := l.getConf() obj[key] = value b, err := json.Marshal(obj) if err != nil { return fmt.Errorf("error marshalling settings to json: %w", err) } if err := l.Run("sudo", "mkdir", "-p", filepath.Dir(configFile)); err != nil { return fmt.Errorf("error saving settings: %w", err) } if err := l.Write(configFile, b); err != nil { return fmt.Errorf("error saving settings: %w", err) } return nil } ================================================ FILE: environment/vm/lima/daemon.go ================================================ package lima import ( "context" "fmt" "time" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/daemon" "github.com/abiosoft/colima/daemon/process/inotify" "github.com/abiosoft/colima/daemon/process/vmnet" "github.com/abiosoft/colima/environment/vm/lima/limaconfig" "github.com/abiosoft/colima/util" ) func (l *limaVM) startDaemon(ctx context.Context, conf config.Config) (context.Context, error) { // vmnet is used by QEMU or bridged mode useVmnet := conf.VMType == limaconfig.QEMU || conf.Network.Mode == "bridged" // network daemon is only needed for vmnet conf.Network.Address = conf.Network.Address && useVmnet // limited to macOS (with vmnet required) // or with inotify enabled if !util.MacOS() || (!conf.MountINotify && !conf.Network.Address) { return ctx, nil } ctxKeyVmnet := daemon.CtxKey(vmnet.Name) ctxKeyInotify := daemon.CtxKey(inotify.Name) // use a nested chain for convenience a := l.Init(ctx) log := a.Logger() networkInstalledKey := struct{ key string }{key: "network_installed"} // add inotify to daemon if conf.MountINotify { a.Add(func() error { ctx = context.WithValue(ctx, ctxKeyInotify, true) deps, _ := l.daemon.Dependency(ctx, conf, inotify.Name) if err := deps.Install(l.host); err != nil { return fmt.Errorf("error setting up inotify dependencies: %w", err) } return nil }) } // add network processes to daemon if useVmnet { a.Add(func() error { if conf.Network.Address { a.Stage("preparing network") ctx = context.WithValue(ctx, ctxKeyVmnet, true) } deps, root := l.daemon.Dependency(ctx, conf, vmnet.Name) if deps.Installed() { ctx = context.WithValue(ctx, networkInstalledKey, true) return nil } // if user interaction is not required (i.e. root), // no need for another verbose info. if root { log.Println("setting up reachable IP address") log.Println("sudo password may be required") } // install deps err := deps.Install(l.host) if err != nil { ctx = context.WithValue(ctx, networkInstalledKey, false) } return err }) } // start daemon a.Add(func() error { return l.daemon.Start(ctx, conf) }) statusKey := struct{ key string }{key: "daemonStatus"} // delay to ensure that the processes have started if conf.Network.Address || conf.MountINotify { a.Retry("", time.Second*1, 15, func(i int) error { s, err := l.daemon.Running(ctx, conf) ctx = context.WithValue(ctx, statusKey, s) if err != nil { return err } if !s.Running { return fmt.Errorf("daemon is not running") } for _, p := range s.Processes { if !p.Running { return p.Error } } return nil }) } // network failure is not fatal if err := a.Exec(); err != nil { if useVmnet { func() { installed, _ := ctx.Value(networkInstalledKey).(bool) if !installed { log.Warnln(fmt.Errorf("error setting up network dependencies: %w", err)) return } status, ok := ctx.Value(statusKey).(daemon.Status) if !ok { return } if !status.Running { log.Warnln(fmt.Errorf("error starting network: %w", err)) return } for _, p := range status.Processes { // TODO: handle inotify separate from network if p.Name == inotify.Name { continue } if !p.Running { ctx = context.WithValue(ctx, daemon.CtxKey(p.Name), false) log.Warnln(fmt.Errorf("error starting %s: %w", p.Name, err)) } } }() } } // check if inotify is running if conf.MountINotify { if inotifyEnabled, _ := ctx.Value(ctxKeyInotify).(bool); !inotifyEnabled { log.Warnln("error occurred enabling inotify daemon") } } // preserve vmnet context if vmnetEnabled, _ := ctx.Value(ctxKeyVmnet).(bool); vmnetEnabled { // env var for subprocess to detect vmnet l.host = l.host.WithEnv(vmnet.SubProcessEnvVar + "=1") } return ctx, nil } ================================================ FILE: environment/vm/lima/disk.go ================================================ package lima import ( "context" _ "embed" "fmt" "os" "strings" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/config/configmanager" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/container/containerd" "github.com/abiosoft/colima/environment/container/docker" "github.com/abiosoft/colima/environment/container/incus" "github.com/abiosoft/colima/environment/vm/lima/limaconfig" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/abiosoft/colima/store" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/downloader" "gopkg.in/yaml.v3" ) //go:embed disk.sh var diskScript string func (l *limaVM) createRuntimeDisk(conf config.Config) error { if environment.IsNoneRuntime(conf.Runtime) { // runtime disk is not required when no runtime is in use return nil } disk := dataDisk(conf.Runtime) s, _ := store.Load() format := !s.DiskFormatted // only format if not previously formatted if !limautil.HasDisk() { if err := limautil.CreateDisk(conf.Disk); err != nil { return fmt.Errorf("error creating runtime disk: %w", err) } format = true // new disk should be formated } // when disk is formatted for the wrong runtime, prevent use if s.DiskFormatted && s.DiskRuntime != "" && s.DiskRuntime != conf.Runtime { return fmt.Errorf("runtime disk provisioned for %s runtime. Delete container data with 'colima delete --data' before using another runtime", s.DiskRuntime) } l.limaConf.Disk = config.Disk(conf.RootDisk).GiB() l.limaConf.AdditionalDisks = append(l.limaConf.AdditionalDisks, limaconfig.Disk{ Name: config.CurrentProfile().ID, Format: format, FSType: disk.FSType, }) l.mountRuntimeDisk(conf, format) return nil } func (l *limaVM) useRuntimeDisk(conf config.Config) { if !limautil.HasDisk() { l.limaConf.Disk = config.Disk(conf.Disk).GiB() return } disk := dataDisk(conf.Runtime) s, _ := store.Load() format := !s.DiskFormatted // only format if not previously formatted l.limaConf.Disk = config.Disk(conf.RootDisk).GiB() l.limaConf.AdditionalDisks = append(l.limaConf.AdditionalDisks, limaconfig.Disk{ Name: config.CurrentProfile().ID, Format: format, FSType: disk.FSType, }) l.mountRuntimeDisk(conf, format) } func dataDisk(runtime string) environment.DataDisk { switch runtime { case docker.Name: return docker.DataDisk() case containerd.Name: return containerd.DataDisk() case incus.Name: return incus.DataDisk() } return environment.DataDisk{} } func diskMountScript(format bool) string { var values = struct { Format bool InstanceId string }{ Format: format, InstanceId: config.CurrentProfile().ID, } b, err := util.ParseTemplate(diskScript, values) if err != nil { // must never happen panic(fmt.Sprintf("error parsing disk mount script template: %v", err)) } return string(b) } func (l *limaVM) mountRuntimeDisk(conf config.Config, format bool) { // provision script to prepare disk l.limaConf.Provision = append(l.limaConf.Provision, limaconfig.Provision{ Mode: "dependency", Script: diskMountScript(format), }) // handle disk mounts disk := dataDisk(conf.Runtime) // pre mount script for _, script := range disk.PreMount { l.limaConf.Provision = append(l.limaConf.Provision, limaconfig.Provision{ Mode: "dependency", Script: script, }) } mountPoint := limautil.MountPoint() for _, dir := range disk.Dirs { script := strings.NewReplacer( "{mount_point}", mountPoint, "{name}", dir.Name, "{data_path}", dir.Path, ).Replace("[ -d {mount_point} ] && mkdir -p {mount_point}/{name} {data_path} && mount --bind {mount_point}/{name} {data_path}") l.limaConf.Provision = append(l.limaConf.Provision, limaconfig.Provision{ Mode: "dependency", Script: script, }) } } func (l *limaVM) downloadDiskImage(ctx context.Context, conf config.Config) error { log := l.Logger(ctx) // use a user specified disk image if conf.DiskImage != "" { if _, err := os.Stat(conf.DiskImage); err != nil { return fmt.Errorf("invalid disk image: %w", err) } image, err := limautil.Image(l.limaConf.Arch, conf.Runtime) if err != nil { return fmt.Errorf("error getting disk image details: %w", err) } sha := downloader.SHA{Size: 512, Digest: image.Digest} if err := sha.ValidateFile(l.host, conf.DiskImage); err != nil { return fmt.Errorf("disk image must be downloaded from '%s', hash failure: %w", image.Location, err) } image.Location = conf.DiskImage l.limaConf.Images = []limaconfig.File{image} return nil } // use a previously cached image if image, ok := limautil.ImageCached(l.limaConf.Arch, conf.Runtime); ok { l.limaConf.Images = []limaconfig.File{image} return nil } // download image log.Infoln("downloading disk image ...") image, err := limautil.DownloadImage(l.limaConf.Arch, conf.Runtime) if err != nil { return fmt.Errorf("error getting qcow image: %w", err) } l.limaConf.Images = []limaconfig.File{image} return nil } func (l *limaVM) setDiskImage() error { var c limaconfig.Config b, err := os.ReadFile(config.CurrentProfile().LimaFile()) if err != nil { return err } if err := yaml.Unmarshal(b, &c); err != nil { return err } l.limaConf.Images = c.Images return nil } func (l *limaVM) syncDiskSize(ctx context.Context, conf config.Config) config.Config { log := l.Logger(ctx) instance, err := configmanager.LoadInstance() if err != nil { // instance config missing, ignore return conf } resized := func() bool { if instance.Disk == conf.Disk { // nothing to do return false } size := conf.Disk - instance.Disk if size < 0 { log.Warnln("disk size cannot be reduced, ignoring...") return false } if err := limautil.ResizeDisk(conf.Disk); err != nil { log.Warnln(fmt.Errorf("unable to resize disk: %w", err)) return false } log.Printf("resizing disk to %dGiB...", conf.Disk) return true }() if !resized { conf.Disk = instance.Disk } return conf } ================================================ FILE: environment/vm/lima/disk.sh ================================================ #!/usr/bin/env sh # Steps: # 1. Check if directory is already mounted, if yes, skip setup # 2. Idenify disk e.g. /dev/vdb or /dev/vdc # 3. Format disk with ext4 if not already formatted # 4. Label disk with instance id # 5. Mount disk DISK_LABEL="lima-{{ .InstanceId }}" MOUNT_POINT="/mnt/${DISK_LABEL}" # Directory already mounted, skip setup if [ -d "$MOUNT_POINT" ]; then if [ -n "$(find "$DIR" -mindepth 1 -print -quit 2>/dev/null)" ]; then echo "Disk already mounted, skipping setup." exit 0 fi fi # Detect the disk to use e.g. /dev/vdb or /dev/vdc DISK="/dev/vdb" if df -h /mnt/lima-cidata/ | tail -n +2 | grep '^/dev/vdb'; then DISK="/dev/vdc" fi DISK_PART="${DISK}1" {{ if .Format }} echo 'type=83' | sudo sfdisk "$DISK" mkfs.ext4 "$DISK_PART" e2label "$DISK_PART" "$DISK_LABEL" {{ end }} # mount disk mkdir -p "$MOUNT_POINT" mount "$DISK_PART" "$MOUNT_POINT" ================================================ FILE: environment/vm/lima/dns.go ================================================ package lima import ( "bytes" "fmt" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment/vm/lima/limautil" ) const ( localhostAddr = "127.0.0.1" defaultGatewayAddress = "192.168.5.2" ) func hasDnsmasq(l *limaVM) bool { // check if dnsmasq is installed return l.RunQuiet("sh", "-c", `apt list | grep 'dnsmasq\/' | grep '\[installed'`) == nil } func (l *limaVM) setupDNS(conf config.Config) error { if !hasDnsmasq(l) { // older image still using systemd-resolved // ignore return nil } // use custom gateway address var gatewayAddr = defaultGatewayAddress customGatewayAddress := conf.Network.GatewayAddress if customGatewayAddress != nil { gatewayAddr = customGatewayAddress.String() } var dnsHosts = map[string]string{ "host.docker.internal": gatewayAddr, "host.lima.internal": gatewayAddr, } internalIP := limautil.InternalIPAddress(config.CurrentProfile().ID) // extra dns entries dnsHosts["colima.internal"] = internalIP if (conf.Hostname) != "" { dnsHosts[conf.Hostname] = localhostAddr } var buf bytes.Buffer // generate dns hosts fmt.Fprintln(&buf, "# Generated by Colima") fmt.Fprintln(&buf, "# Do not edit this file manually") fmt.Fprintln(&buf) for k, v := range dnsHosts { fmt.Fprintf(&buf, "address=/%s/%s", k, v) fmt.Fprintln(&buf) } fmt.Fprintln(&buf) // for cleaner output // generate dns servers dnsServers := []string{gatewayAddr} if len(conf.Network.DNSResolvers) > 0 { dnsServers = nil for _, dns := range conf.Network.DNSResolvers { dnsServers = append(dnsServers, dns.String()) } } for _, dns := range dnsServers { fmt.Fprintf(&buf, "server=%s", dns) fmt.Fprintln(&buf) } fmt.Fprintln(&buf) // for cleaner output // set dnsmasq listening interface and address fmt.Fprintln(&buf, "interface=eth0") fmt.Fprintln(&buf, "listen-address="+internalIP) fmt.Fprintln(&buf, "bind-interfaces") // ensure dnsmasq config directory exists if err := l.RunQuiet("sudo", "mkdir", "-p", "/etc/dnsmasq.d"); err != nil { return fmt.Errorf("failed to create dnsmasq config directory: %w", err) } // write config to dnsmasq directory if err := l.Write("/etc/dnsmasq.d/01-colima.conf", buf.Bytes()); err != nil { return fmt.Errorf("failed to write dnsmasq config: %w", err) } // remove existing resolv.conf file if err := l.RunQuiet("sudo", "rm", "-f", "/etc/resolv.conf"); err != nil { return fmt.Errorf("failed to remove existing resolv.conf: %w", err) } // replace resolv.conf with a custom one resolvConf := fmt.Sprintf("# Generated by Colima\n\nnameserver %s\n", internalIP) if err := l.Write("/etc/resolv.conf", []byte(resolvConf)); err != nil { return fmt.Errorf("failed to write resolv.conf: %w", err) } // restart dnsmasq service to apply changes if err := l.RunQuiet("sudo", "systemctl", "restart", "dnsmasq"); err != nil { return fmt.Errorf("failed to restart dnsmasq service: %w", err) } return nil } ================================================ FILE: environment/vm/lima/file.go ================================================ package lima import ( "bytes" "fmt" "io/fs" "os" "path/filepath" "strconv" "strings" "time" "github.com/abiosoft/colima/environment" ) func (l limaVM) Read(fileName string) (string, error) { s, err := l.RunOutput("sudo", "cat", fileName) if err != nil { return "", fmt.Errorf("cannot read file '%s': %w", fileName, err) } return s, err } func (l *limaVM) Write(fileName string, body []byte) error { var stdin = bytes.NewReader(body) dir := filepath.Dir(fileName) if err := l.RunQuiet("sudo", "mkdir", "-p", dir); err != nil { return fmt.Errorf("error creating directory '%s': %w", dir, err) } return l.RunWith(stdin, nil, "sudo", "sh", "-c", "cat > "+fileName) } func (l *limaVM) Stat(fileName string) (os.FileInfo, error) { return newFileInfo(l, fileName) } var _ os.FileInfo = (*fileInfo)(nil) type fileInfo struct { isDir bool modTime time.Time mode fs.FileMode name string size int64 } func newFileInfo(guest environment.GuestActions, filename string) (fileInfo, error) { info := fileInfo{} // "%s,%a,%Y,%F" -> size, permission, modified time, type stat, err := guest.RunOutput("sudo", "stat", "-c", "%s,%a,%Y,%F", filename) if err != nil { return info, statError(filename, err) } stats := strings.Split(stat, ",") if len(stats) < 4 { return info, statError(filename, err) } info.name = filename info.size, _ = strconv.ParseInt(stats[0], 10, 64) info.mode = func() fs.FileMode { mode, _ := strconv.ParseUint(stats[1], 10, 32) return fs.FileMode(mode) }() info.modTime = func() time.Time { unix, _ := strconv.ParseInt(stats[2], 10, 64) return time.Unix(unix, 0) }() info.isDir = stats[3] == "directory" return info, nil } func statError(filename string, err error) error { return fmt.Errorf("cannot stat file '%s': %w", filename, err) } // IsDir implements fs.FileInfo func (f fileInfo) IsDir() bool { return f.isDir } // ModTime implements fs.FileInfo func (f fileInfo) ModTime() time.Time { return f.modTime } // Mode implements fs.FileInfo func (f fileInfo) Mode() fs.FileMode { return f.mode } // Name implements fs.FileInfo func (f fileInfo) Name() string { return f.name } // Size implements fs.FileInfo func (f fileInfo) Size() int64 { return f.size } // Sys implements fs.FileInfo func (fileInfo) Sys() any { return nil } ================================================ FILE: environment/vm/lima/lima.go ================================================ package lima import ( "context" "fmt" "os" "path/filepath" "strconv" "time" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/config/configmanager" "github.com/abiosoft/colima/core" "github.com/abiosoft/colima/daemon" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/vm/lima/limaconfig" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/abiosoft/colima/store" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/osutil" "github.com/abiosoft/colima/util/yamlutil" "github.com/sirupsen/logrus" ) // New creates a new virtual machine. func New(host environment.HostActions) environment.VM { // lima config directory limaHome := config.LimaDir() // environment variables for the subprocesses var envs []string envHome := limautil.EnvLimaHome + "=" + limaHome envLimaInstance := envLimaInstance + "=" + config.CurrentProfile().ID envBinary := osutil.EnvColimaBinary + "=" + osutil.Executable() envs = append(envs, envHome, envLimaInstance, envBinary) // consider making this truly flexible to support other VMs return &limaVM{ host: host.WithEnv(envs...), limaHome: limaHome, CommandChain: cli.New("vm"), daemon: daemon.NewManager(host), } } const ( envLimaInstance = "LIMA_INSTANCE" lima = "lima" limactl = limautil.LimactlCommand ) var _ environment.VM = (*limaVM)(nil) type limaVM struct { host environment.HostActions cli.CommandChain // keep config in case of restart conf config.Config // lima config limaConf limaconfig.Config // lima config directory limaHome string // network between host and the vm daemon daemon.Manager } func (l limaVM) Dependencies() []string { return []string{ "lima", } } func (l *limaVM) Start(ctx context.Context, conf config.Config) error { a := l.Init(ctx) l.prepareHost(conf) if l.Created() { return l.resume(ctx, conf) } a.Add(func() (err error) { ctx, err = l.startDaemon(ctx, conf) return err }) a.Stage("creating and starting") confFile := filepath.Join(os.TempDir(), config.CurrentProfile().ID+".yaml") a.Add(func() (err error) { l.limaConf, err = newConf(ctx, conf) return err }) a.Add(l.assertQemu) a.Add(func() error { return l.createRuntimeDisk(conf) }) a.Add(func() error { return l.downloadDiskImage(ctx, conf) }) a.Add(func() error { return yamlutil.WriteYAML(l.limaConf, confFile) }) a.Add(func() error { return l.writeNetworkFile(conf) }) a.Add(func() error { return l.host.Run(limactl, "start", "--tty=false", confFile) }) a.Add(func() error { return os.Remove(confFile) }) // adding it to command chain to execute only after successful startup. a.Add(func() error { l.conf = conf return nil }) l.addPostStartActions(a, conf) return a.Exec() } func (l *limaVM) resume(ctx context.Context, conf config.Config) error { log := l.Logger(ctx) a := l.Init(ctx) if l.Running(ctx) { log.Println("already running") return nil } a.Add(func() (err error) { ctx, err = l.startDaemon(ctx, conf) return err }) a.Add(func() (err error) { // disk must be resized before starting conf = l.syncDiskSize(ctx, conf) l.limaConf, err = newConf(ctx, conf) return err }) a.Add(l.assertQemu) a.Add(func() error { l.useRuntimeDisk(conf) return nil }) a.Add(l.setDiskImage) a.Add(func() error { err := yamlutil.WriteYAML(l.limaConf, config.CurrentProfile().LimaFile()) return err }) a.Add(func() error { return l.writeNetworkFile(conf) }) a.Stage("starting") a.Add(func() error { return l.host.Run(limactl, "start", config.CurrentProfile().ID) }) l.addPostStartActions(a, conf) return a.Exec() } func (l limaVM) Running(_ context.Context) bool { i, err := limautil.Instance() if err != nil { logrus.Trace(fmt.Errorf("error retrieving running instance: %w", err)) return false } return i.Running() } func (l limaVM) Stop(ctx context.Context, force bool) error { log := l.Logger(ctx) a := l.Init(ctx) if !l.Running(ctx) && !force { log.Println("not running") return nil } a.Stage("stopping") if util.MacOS() { conf, _ := configmanager.LoadInstance() a.Retry("", time.Second*1, 10, func(retryCount int) error { err := l.daemon.Stop(ctx, conf) if err != nil { err = cli.ErrNonFatal(err) } return err }) } a.Add(func() error { l.removeHostAddresses(); return nil }) a.Add(func() error { l.removeIncusContainerRoute(); return nil }) a.Add(func() error { if force { return l.host.Run(limactl, "stop", "--force", config.CurrentProfile().ID) } return l.host.Run(limactl, "stop", config.CurrentProfile().ID) }) return a.Exec() } func (l limaVM) Teardown(ctx context.Context) error { a := l.Init(ctx) if util.MacOS() { conf, _ := configmanager.LoadInstance() a.Retry("", time.Second*1, 10, func(retryCount int) error { return l.daemon.Stop(ctx, conf) }) } a.Add(func() error { return l.host.Run(limactl, "delete", "--force", config.CurrentProfile().ID) }) return a.Exec() } func (l limaVM) Restart(ctx context.Context) error { if l.conf.Empty() { return fmt.Errorf("cannot restart, VM not previously started") } if err := l.Stop(ctx, false); err != nil { return err } // minor delay to prevent possible race condition. time.Sleep(time.Second * 2) if err := l.Start(ctx, l.conf); err != nil { return err } return nil } func (l limaVM) Host() environment.HostActions { return l.host } func (l limaVM) Env(s string) (string, error) { ctx := context.Background() if !l.Running(ctx) { return "", fmt.Errorf("not running") } return l.RunOutput("echo", "$"+s) } func (l limaVM) Created() bool { stat, err := os.Stat(config.CurrentProfile().LimaFile()) return err == nil && !stat.IsDir() } func (l limaVM) User() (string, error) { return l.RunOutput("whoami") } func (l limaVM) Arch() environment.Arch { a, _ := l.RunOutput("uname", "-m") return environment.Arch(a) } func (l *limaVM) addPostStartActions(a *cli.ActiveCommandChain, conf config.Config) { // setup dns a.Add(func() error { if err := l.setupDNS(conf); err != nil { return fmt.Errorf("error setting up DNS: %w", err) } return nil }) // registry certs a.Add(l.copyCerts) // cross-platform emulation a.Add(func() error { // use binfmt when emulation is disabled i.e. host arch if conf.Binfmt != nil && *conf.Binfmt { if arch := environment.HostArch(); arch == environment.Arch(conf.Arch).Value() { if err := core.SetupBinfmt(l.host, l, environment.Arch(conf.Arch)); err != nil { logrus.Warn(fmt.Errorf("unable to enable qemu %s emulation: %w", arch, err)) } } } if l.limaConf.VMOpts.VZOpts.Rosetta.Enabled { // enable rosetta err := l.Run("sudo", "sh", "-c", `stat /proc/sys/fs/binfmt_misc/rosetta || echo ':rosetta:M::\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x3e\x00:\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/mnt/lima-rosetta/rosetta:OCF' > /proc/sys/fs/binfmt_misc/register`) if err != nil { logrus.Warn(fmt.Errorf("unable to enable rosetta: %w", err)) return nil } // disable qemu if err := l.RunQuiet("stat", "/proc/sys/fs/binfmt_misc/qemu-x86_64"); err == nil { err = l.Run("sudo", "sh", "-c", `echo 0 > /proc/sys/fs/binfmt_misc/qemu-x86_64`) if err != nil { logrus.Warn(fmt.Errorf("unable to disable qemu x86_84 emulation: %w", err)) } } } return nil }) // replicate addresses when network address is disabled a.Add(func() error { if err := l.replicateHostAddresses(conf); err != nil { logrus.Warnln(fmt.Errorf("unable to assign host IP addresses to the VM: %w", err)) } return nil }) // preserve state a.Add(func() error { if err := configmanager.SaveToFile(conf, config.CurrentProfile().StateFile()); err != nil { logrus.Warnln(fmt.Errorf("error persisting Colima state: %w", err)) } return nil }) // save store settings a.Add(func() error { if len(l.limaConf.AdditionalDisks) == 0 { return nil } // startup is successful // if additional disk is present, then it must've been formatted correctly. if err := store.Set(func(s *store.Store) { s.DiskFormatted = true }); err != nil { // not fatal, but should be logged logrus.Warnln(fmt.Errorf("error persisting store settings: %w", err)) } return nil }) } func (l *limaVM) assertQemu() error { // assert qemu requirement sameArchitecture := environment.HostArch() == l.limaConf.Arch if err := util.AssertQemuImg(); err != nil && l.limaConf.VMType == limaconfig.QEMU { if !sameArchitecture { return fmt.Errorf("qemu is required to emulate %s: %w", l.limaConf.Arch, err) } return err } return nil } const envLimaSSHPortForwarder = "LIMA_SSH_PORT_FORWARDER" func (l *limaVM) prepareHost(conf config.Config) { useSSHPortForwarder := conf.PortForwarder != "grpc" l.host = l.host.WithEnv(envLimaSSHPortForwarder + "=" + strconv.FormatBool(useSSHPortForwarder)) } ================================================ FILE: environment/vm/lima/limaconfig/config.go ================================================ package limaconfig import ( "net" "github.com/abiosoft/colima/environment" ) type Arch = environment.Arch // Config is lima config. Code copied from lima and modified. type Config struct { VMType VMType `yaml:"vmType,omitempty" json:"vmType,omitempty"` VMOpts VMOpts `yaml:"vmOpts,omitempty" json:"vmOpts,omitempty"` Arch Arch `yaml:"arch,omitempty"` Images []File `yaml:"images"` CPUs *int `yaml:"cpus,omitempty"` Memory string `yaml:"memory,omitempty"` Disk string `yaml:"disk,omitempty"` AdditionalDisks []Disk `yaml:"additionalDisks,omitempty" json:"additionalDisks,omitempty"` Mounts []Mount `yaml:"mounts,omitempty"` MountType MountType `yaml:"mountType,omitempty" json:"mountType,omitempty"` SSH SSH `yaml:"ssh"` Containerd Containerd `yaml:"containerd"` Env map[string]string `yaml:"env,omitempty"` DNS []net.IP `yaml:"dns"` Firmware Firmware `yaml:"firmware"` HostResolver HostResolver `yaml:"hostResolver"` PortForwards []PortForward `yaml:"portForwards,omitempty"` Networks []Network `yaml:"networks,omitempty"` Provision []Provision `yaml:"provision,omitempty" json:"provision,omitempty"` NestedVirtualization bool `yaml:"nestedVirtualization,omitempty" json:"nestedVirtualization,omitempty"` } type File struct { Location string `yaml:"location"` // REQUIRED Arch Arch `yaml:"arch,omitempty"` Digest string `yaml:"digest,omitempty"` } type Mount struct { Location string `yaml:"location"` // REQUIRED MountPoint string `yaml:"mountPoint,omitempty"` Writable bool `yaml:"writable"` NineP NineP `yaml:"9p,omitempty" json:"9p,omitempty"` } type Disk struct { Name string `yaml:"name" json:"name"` // REQUIRED Format bool `yaml:"format" json:"format"` FSType string `yaml:"fsType,omitempty" json:"fsType,omitempty"` FSArgs []string `yaml:"fsArgs,omitempty" json:"fsArgs,omitempty"` } type SSH struct { LocalPort int `yaml:"localPort,omitempty"` LoadDotSSHPubKeys bool `yaml:"loadDotSSHPubKeys"` ForwardAgent bool `yaml:"forwardAgent"` // default: false } type Containerd struct { System bool `yaml:"system"` // default: false User bool `yaml:"user"` // default: true } type Firmware struct { // LegacyBIOS disables UEFI if set. // LegacyBIOS is ignored for aarch64. LegacyBIOS bool `yaml:"legacyBIOS"` } type ( Proto = string MountType = string VMType = string ) const ( TCP Proto = "tcp" UDP Proto = "udp" REVSSHFS MountType = "reverse-sshfs" NINEP MountType = "9p" VIRTIOFS MountType = "virtiofs" Krunkit VMType = "krunkit" QEMU VMType = "qemu" VZ VMType = "vz" ) type PortForward struct { GuestIPMustBeZero bool `yaml:"guestIPMustBeZero,omitempty" json:"guestIPMustBeZero,omitempty"` GuestIP net.IP `yaml:"guestIP,omitempty" json:"guestIP,omitempty"` GuestPort int `yaml:"guestPort,omitempty" json:"guestPort,omitempty"` GuestPortRange [2]int `yaml:"guestPortRange,omitempty" json:"guestPortRange,omitempty"` GuestSocket string `yaml:"guestSocket,omitempty" json:"guestSocket,omitempty"` HostIP net.IP `yaml:"hostIP,omitempty" json:"hostIP,omitempty"` HostPort int `yaml:"hostPort,omitempty" json:"hostPort,omitempty"` HostPortRange [2]int `yaml:"hostPortRange,omitempty" json:"hostPortRange,omitempty"` HostSocket string `yaml:"hostSocket,omitempty" json:"hostSocket,omitempty"` Proto Proto `yaml:"proto,omitempty" json:"proto,omitempty"` Ignore bool `yaml:"ignore,omitempty" json:"ignore,omitempty"` } type HostResolver struct { Enabled bool `yaml:"enabled" json:"enabled"` IPv6 bool `yaml:"ipv6,omitempty" json:"ipv6,omitempty"` Hosts map[string]string `yaml:"hosts,omitempty" json:"hosts,omitempty"` } type Network struct { // `Lima`, `Socket`, and `VNL` are mutually exclusive; exactly one is required Lima string `yaml:"lima,omitempty" json:"lima,omitempty"` // Socket is a QEMU-compatible socket Socket string `yaml:"socket,omitempty" json:"socket,omitempty"` // VZNAT uses VZNATNetworkDeviceAttachment. Needs VZ. No root privilege is required. VZNAT bool `yaml:"vzNAT,omitempty" json:"vzNAT,omitempty"` // VNLDeprecated is a Virtual Network Locator (https://github.com/rd235/vdeplug4/commit/089984200f447abb0e825eb45548b781ba1ebccd). // On macOS, only VDE2-compatible form (optionally with vde:// prefix) is supported. // VNLDeprecated is deprecated. Use Socket. VNLDeprecated string `yaml:"vnl,omitempty" json:"vnl,omitempty"` SwitchPortDeprecated uint16 `yaml:"switchPort,omitempty" json:"switchPort,omitempty"` // VDE Switch port, not TCP/UDP port (only used by VDE networking) MACAddress string `yaml:"macAddress,omitempty" json:"macAddress,omitempty"` Interface string `yaml:"interface,omitempty" json:"interface,omitempty"` Metric uint32 `yaml:"metric,omitempty" json:"metric,omitempty"` } type ProvisionMode = string const ( ProvisionModeSystem ProvisionMode = "system" ProvisionModeUser ProvisionMode = "user" ProvisionModeBoot ProvisionMode = "boot" ProvisionModeDependency ProvisionMode = "dependency" ) type Provision struct { Mode ProvisionMode `yaml:"mode" json:"mode"` // default: "system" Script string `yaml:"script" json:"script"` SkipResolution bool `yaml:"skipDefaultDependencyResolution,omitempty" json:"skipDefaultDependencyResolution,omitempty"` } type NineP struct { SecurityModel string `yaml:"securityModel,omitempty" json:"securityModel,omitempty"` ProtocolVersion string `yaml:"protocolVersion,omitempty" json:"protocolVersion,omitempty"` Msize string `yaml:"msize,omitempty" json:"msize,omitempty"` Cache string `yaml:"cache,omitempty" json:"cache,omitempty"` } type VMOpts struct { QEMU QEMUOpts `yaml:"qemu,omitempty" json:"qemu,omitempty"` VZOpts VZOpts `yaml:"vz,omitempty" json:"vz,omitempty"` } type QEMUOpts struct { MinimumVersion *string `yaml:"minimumVersion,omitempty" json:"minimumVersion,omitempty"` CPUType map[Arch]string `yaml:"cpuType,omitempty" json:"cpuType,omitempty"` } type VZOpts struct { Rosetta Rosetta `yaml:"rosetta,omitempty" json:"rosetta,omitempty"` } type Rosetta struct { Enabled bool `yaml:"enabled" json:"enabled"` BinFmt bool `yaml:"binfmt" json:"binfmt"` } ================================================ FILE: environment/vm/lima/limautil/disk.go ================================================ package limautil import ( "bytes" "encoding/json" "fmt" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/store" ) // HasDisk checks if a lima disk exists for the current instance. func HasDisk() bool { name := config.CurrentProfile().ID var resp struct { Name string `json:"name"` } cmd := Limactl("disk", "list", "--json", name) var buf bytes.Buffer cmd.Stdout = &buf cmd.Stderr = nil if err := cmd.Run(); err != nil { return false } if err := json.NewDecoder(&buf).Decode(&resp); err != nil { return false } return resp.Name == name } // CreateDisk creates a lima disk with size in GiB. func CreateDisk(size int) error { name := config.CurrentProfile().ID var buf bytes.Buffer cmd := Limactl("disk", "create", name, "--size", fmt.Sprintf("%dGiB", size)) cmd.Stderr = &buf cmd.Stdout = &buf if err := cmd.Run(); err != nil { return fmt.Errorf("error creating lima disk: %w, output: %s", err, buf.String()) } return nil } // ResizeDisk resizes disk to new size func ResizeDisk(size int) error { name := config.CurrentProfile().ID var buf bytes.Buffer cmd := Limactl("disk", "resize", name, "--size", fmt.Sprintf("%dGiB", size)) cmd.Stderr = &buf cmd.Stdout = &buf if err := cmd.Run(); err != nil { return fmt.Errorf("error resizing disk: %w, output: %s", err, buf.String()) } return nil } // DeleteDisk deletes lima disk for the current instance. func DeleteDisk() error { name := config.CurrentProfile().ID var buf bytes.Buffer cmd := Limactl("disk", "delete", name) cmd.Stderr = &buf cmd.Stdout = &buf if err := cmd.Run(); err != nil { return fmt.Errorf("error deleting lima disk: %w, output: %s", err, buf.String()) } return nil } // MountPoint returns the lima disk mount point for the current instance. func MountPoint() string { return fmt.Sprintf("/mnt/lima-%s", config.CurrentProfile().ID) } // DiskPrivisioned returns if the disk exists and has been provisioned for the specified runtime. func DiskProvisioned(runtime string) bool { if !HasDisk() { return false } s, _ := store.Load() return s.DiskFormatted && s.DiskRuntime == runtime } ================================================ FILE: environment/vm/lima/limautil/files.go ================================================ package limautil import ( "path/filepath" "github.com/abiosoft/colima/config" ) const colimaDiffDiskFile = "diffdisk" // ColimaDiffDisk returns path to the diffdisk for the colima VM. func ColimaDiffDisk(profileID string) string { return filepath.Join(config.ProfileFromName(profileID).LimaInstanceDir(), colimaDiffDiskFile) } const networkFile = "networks.yaml" // NetworkFile returns path to the network file. func NetworkFile() string { return filepath.Join(config.LimaDir(), "_config", networkFile) } // NetworkAssetsDirecotry returns the directory for the generated network assets. func NetworkAssetsDirectory() string { return filepath.Join(config.LimaDir(), "_networks") } ================================================ FILE: environment/vm/lima/limautil/image.go ================================================ package limautil import ( "bufio" "bytes" "fmt" "io" "os" "strings" "github.com/abiosoft/colima/embedded" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/host" "github.com/abiosoft/colima/environment/vm/lima/limaconfig" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/downloader" "github.com/sirupsen/logrus" ) func init() { if err := loadImages(); err != nil { logrus.Fatal(err) } } // ImageCached returns if the image for architecture and runtime // has been previously downloaded and cached. func ImageCached(arch environment.Arch, runtime string) (limaconfig.File, bool) { img, err := findImage(arch, runtime) if err != nil { return img, false } image := diskImageFile(downloader.CacheFilename(img.Location)) img.Location = image.Location() img.Digest = "" return img, image.Generated() } func findImage(arch environment.Arch, runtime string) (f limaconfig.File, err error) { err = fmt.Errorf("cannot find %s image for %s runtime", arch, runtime) imgFile, ok := diskImageMap[runtime] if !ok { return } img, ok := imgFile[arch.GoArch()] if !ok { return } return img, nil } // Image returns the details of the disk image to download for the arch and runtime. func Image(arch environment.Arch, runtime string) (limaconfig.File, error) { return findImage(arch, runtime) } // DownloadImage downloads the image for arch and runtime. func DownloadImage(arch environment.Arch, runtime string) (f limaconfig.File, err error) { img, err := findImage(arch, runtime) if err != nil { return img, err } host := host.New() // download image qcow2, err := downloadImage(host, img) if err != nil { return f, err } diskImage := diskImageFile(qcow2) // if qemu-img is missing, ignore raw conversion if err := util.AssertQemuImg(); err != nil { img.Location = diskImage.String() img.Digest = "" // remove digest return img, nil } // convert from qcow2 to raw raw, err := qcow2ToRaw(host, diskImage) if err != nil { return f, err } img.Location = raw img.Digest = "" // remove digest return img, nil } // map of runtime to disk images. var diskImageMap = map[string]diskImages{} // map of architecture to disk image type diskImages map[string]limaconfig.File func loadImages() error { filename := "images/images.txt" b, err := embedded.Read(filename) if err != nil { logrus.Fatalf("error reading embedded file: %s", filename) } return loadImagesFromBytes(b) } func loadImagesFromBytes(b []byte) error { scanner := bufio.NewScanner(bytes.NewReader(b)) for scanner.Scan() { line := scanner.Bytes() var arch environment.Arch var runtime, url, sha string _, err := fmt.Fscan(bytes.NewReader(line), &arch, &runtime, &url, &sha) if err != nil && err != io.EOF { return err } // sanitise the value arch = arch.Value() file := limaconfig.File{Location: url, Arch: arch} if sha != "" { file.Digest = "sha512:" + sha } var files = diskImages{} if m, ok := diskImageMap[runtime]; ok { files = m } files[arch.GoArch()] = file diskImageMap[runtime] = files } return nil } // downloadImage downloads the file and returns the location of the downloaded file. func downloadImage(host environment.HostActions, file limaconfig.File) (string, error) { // download image request := downloader.Request{URL: file.Location} if file.Digest != "" { request.SHA = &downloader.SHA{Size: 512, Digest: file.Digest} } location, err := downloader.Download(host, request) if err != nil { return "", fmt.Errorf("error during image download: %w", err) } return location, nil } // qcow2ToRaw uses qemu-img to conver the image from qcow to raw. // Returns the filename of the raw file and an error (if any). func qcow2ToRaw(host environment.Host, image diskImageFile) (string, error) { if _, err := os.Stat(image.Raw()); err == nil { // already exists, return return image.Raw(), nil } err := host.Run("qemu-img", "convert", "-f", "qcow2", "-O", "raw", image.String(), image.Raw()) if err != nil { // remove the incomplete raw file _ = host.RunQuiet("rm", "-f", image.Raw()) return "", err } return image.Raw(), err } type diskImageFile string func (d diskImageFile) String() string { return strings.TrimSuffix(string(d), ".raw") } func (d diskImageFile) Raw() string { return d.String() + ".raw" } func (d diskImageFile) Generated() bool { stat, err := os.Stat(d.Location()) return err == nil && !stat.IsDir() } // Location returns the expected location of the image based on availability of qemu. func (d diskImageFile) Location() string { if err := util.AssertQemuImg(); err == nil { return d.Raw() } return d.String() } ================================================ FILE: environment/vm/lima/limautil/instance.go ================================================ package limautil import ( "bufio" "bytes" "encoding/json" "fmt" "strings" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/config/configmanager" ) // Instance returns current instance. func Instance() (InstanceInfo, error) { return getInstance(config.CurrentProfile().ID) } // InstanceInfo is the information about a Lima instance type InstanceInfo struct { Name string `json:"name,omitempty"` Status string `json:"status,omitempty"` Arch string `json:"arch,omitempty"` CPU int `json:"cpus,omitempty"` Memory int64 `json:"memory,omitempty"` Disk int64 `json:"disk,omitempty"` Dir string `json:"dir,omitempty"` Network []struct { VNL string `json:"vnl,omitempty"` Interface string `json:"interface,omitempty"` } `json:"network,omitempty"` IPAddress string `json:"address,omitempty"` Runtime string `json:"runtime,omitempty"` } // Running checks if the instance is running. func (i InstanceInfo) Running() bool { return i.Status == limaStatusRunning } // Config returns the current Colima config func (i InstanceInfo) Config() (config.Config, error) { return configmanager.LoadFrom(config.ProfileFromName(i.Name).StateFile()) } // Lima statuses const ( limaStatusRunning = "Running" ) func getInstance(profileID string) (InstanceInfo, error) { var i InstanceInfo var buf bytes.Buffer cmd := Limactl("list", profileID, "--json") cmd.Stderr = nil cmd.Stdout = &buf if err := cmd.Run(); err != nil { return i, fmt.Errorf("error retrieving instance: %w", err) } if buf.Len() == 0 { return i, fmt.Errorf("instance '%s' does not exist", config.ProfileFromName(profileID).DisplayName) } if err := json.Unmarshal(buf.Bytes(), &i); err != nil { return i, fmt.Errorf("error retrieving instance: %w", err) } if conf, err := i.Config(); err == nil { if conf.Disk > 0 { i.Disk = config.Disk(conf.Disk).Int() } } return i, nil } // Instances returns Lima instances created by colima. func Instances(ids ...string) ([]InstanceInfo, error) { limaIDs := make([]string, len(ids)) for i := range ids { limaIDs[i] = config.ProfileFromName(ids[i]).ID } args := append([]string{"list", "--json"}, limaIDs...) var buf bytes.Buffer cmd := Limactl(args...) cmd.Stderr = nil cmd.Stdout = &buf if err := cmd.Run(); err != nil { return nil, fmt.Errorf("error retrieving instances: %w", err) } var instances []InstanceInfo scanner := bufio.NewScanner(&buf) for scanner.Scan() { var i InstanceInfo line := scanner.Bytes() if err := json.Unmarshal(line, &i); err != nil { return nil, fmt.Errorf("error retrieving instances: %w", err) } // limit to colima instances if !strings.HasPrefix(i.Name, "colima") { continue } conf, _ := i.Config() if i.Running() { for _, n := range i.Network { if n.Interface == NetInterface { i.IPAddress = getIPAddress(i.Name, NetInterface) } } i.Runtime = getRuntime(conf) } // rename to local friendly names i.Name = config.ProfileFromName(i.Name).ShortName // network is low level, remove i.Network = nil // report correct disk usage if conf.Disk > 0 { i.Disk = config.Disk(conf.Disk).Int() } instances = append(instances, i) } return instances, nil } // RunningInstances return Lima instances that are has a running status. func RunningInstances() ([]InstanceInfo, error) { allInstances, err := Instances() if err != nil { return nil, err } var runningInstances []InstanceInfo for _, instance := range allInstances { if instance.Running() { runningInstances = append(runningInstances, instance) } } return runningInstances, nil } func getRuntime(conf config.Config) string { var runtime string switch conf.Runtime { case "docker", "containerd", "incus": runtime = conf.Runtime case "none": return "none" default: return "" } if conf.Kubernetes.Enabled { runtime += "+k3s" } return runtime } ================================================ FILE: environment/vm/lima/limautil/limautil.go ================================================ package limautil import ( "os" "os/exec" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" ) // EnvLimaHome is the environment variable for the Lima directory. const EnvLimaHome = "LIMA_HOME" // EnvLimaDrivers is the environment variable for the path to external Lima drivers. const EnvLimaDrivers = "LIMA_DRIVERS_PATH" // LimactlCommand is the limactl command. const LimactlCommand = "limactl" // Limactl prepares a limactl command. func Limactl(args ...string) *exec.Cmd { cmd := cli.Command(LimactlCommand, args...) cmd.Env = append(cmd.Env, os.Environ()...) cmd.Env = append(cmd.Env, EnvLimaHome+"="+config.LimaDir()) return cmd } ================================================ FILE: environment/vm/lima/limautil/network.go ================================================ package limautil import ( "bytes" "net" "strings" ) // network interface for shared network in the virtual machine. const NetInterface = "col0" // network metric for the route const NetMetric uint32 = 300 const NetMetricPreferred uint32 = 100 // IPAddress returns the ip address for profile. // It returns the PTP address if networking is enabled or falls back to 127.0.0.1. // It is guaranteed to return a value. // // TODO: unnecessary round-trip is done to get instance details from Lima. func IPAddress(profileID string) string { const fallback = "127.0.0.1" instance, err := getInstance(profileID) if err != nil { return fallback } if len(instance.Network) > 0 { for _, n := range instance.Network { if n.Interface == NetInterface { return getIPAddress(profileID, n.Interface) } } } return fallback } // InternalIPAddress returns the internal IP address for the profile. func InternalIPAddress(profileID string) string { return getIPAddress(profileID, "eth0") } func getIPAddress(profileID, interfaceName string) string { var buf bytes.Buffer // TODO: this should be less hacky cmd := Limactl("shell", profileID, "sh", "-c", `ip -4 addr show `+interfaceName+` | grep inet | awk -F' ' '{print $2 }' | cut -d/ -f1`) cmd.Stderr = nil cmd.Stdout = &buf _ = cmd.Run() return strings.TrimSpace(buf.String()) } type LimaNetworkConfig struct { Mode string `yaml:"mode"` Gateway net.IP `yaml:"gateway"` Netmask string `yaml:"netmask"` } type LimaNetwork struct { Networks struct { UserV2 LimaNetworkConfig `yaml:"user-v2"` } `yaml:"networks"` } ================================================ FILE: environment/vm/lima/limautil/ssh.go ================================================ package limautil import ( "bufio" "bytes" "fmt" "os" "path/filepath" "strings" "github.com/abiosoft/colima/config" ) // ShowSSH runs the show-ssh command in Lima. // returns the ssh output, if in layer, and an error if any func ShowSSH(profileID string) (resp struct { Output string File struct { Lima string Colima string } }, err error) { ssh := sshConfig(profileID) sshConf, err := ssh.Contents() if err != nil { return resp, fmt.Errorf("error retrieving ssh config: %w", err) } resp.Output = replaceSSHConfig(sshConf, profileID) resp.File.Lima = ssh.File() resp.File.Colima = config.SSHConfigFile() return resp, nil } func replaceSSHConfig(conf string, profileID string) string { profileID = config.ProfileFromName(profileID).ID var out bytes.Buffer scanner := bufio.NewScanner(strings.NewReader(conf)) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "Host ") { line = "Host " + profileID } _, _ = fmt.Fprintln(&out, line) } return out.String() } const sshConfigFile = "ssh.config" // sshConfig is the ssh configuration file for a Colima profile. type sshConfig string // Contents returns the content of the SSH config file. func (s sshConfig) Contents() (string, error) { profile := config.ProfileFromName(string(s)) b, err := os.ReadFile(s.File()) if err != nil { return "", fmt.Errorf("error retrieving Lima SSH config file for profile '%s': %w", strings.TrimPrefix(profile.DisplayName, "lima"), err) } return string(b), nil } // File returns the path to the SSH config file. func (s sshConfig) File() string { profile := config.ProfileFromName(string(s)) return filepath.Join(profile.LimaInstanceDir(), sshConfigFile) } ================================================ FILE: environment/vm/lima/network.go ================================================ package lima import ( "fmt" "net" "os" "path/filepath" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/config/configmanager" "github.com/abiosoft/colima/environment/container/incus" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/abiosoft/colima/util" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" ) var defaultLimaNetworkConfig = limautil.LimaNetwork{ Networks: struct { UserV2 limautil.LimaNetworkConfig `yaml:"user-v2"` }{ UserV2: limautil.LimaNetworkConfig{ Mode: "user-v2", Gateway: net.ParseIP("192.168.5.2"), Netmask: "255.255.255.0", }, }, } func (l *limaVM) writeNetworkFile(conf config.Config) error { networkFile := limautil.NetworkFile() // use custom gateway address gatewayAddress := conf.Network.GatewayAddress if gatewayAddress != nil { defaultLimaNetworkConfig.Networks.UserV2.Gateway = gatewayAddress } // if there are no running instances, clear network directory if instances, err := limautil.RunningInstances(); err == nil && len(instances) == 0 { if err := os.RemoveAll(limautil.NetworkAssetsDirectory()); err != nil { logrus.Warnln(fmt.Errorf("could not clear network assets directory: %w", err)) } } if err := os.MkdirAll(filepath.Dir(networkFile), 0755); err != nil { return fmt.Errorf("error creating Lima config directory: %w", err) } networkFileMarshalled, err := yaml.Marshal(&defaultLimaNetworkConfig) if err != nil { return fmt.Errorf("error marshalling Lima network config file: %w", err) } if err := os.WriteFile(networkFile, networkFileMarshalled, 0755); err != nil { return fmt.Errorf("error writing Lima network config file: %w", err) } return nil } func (l *limaVM) replicateHostAddresses(conf config.Config) error { if !conf.Network.Address && conf.Network.HostAddresses { for _, ip := range util.HostIPAddresses() { if err := l.RunQuiet("sudo", "ip", "address", "add", ip.String()+"/24", "dev", "lo"); err != nil { return err } } } return nil } func (l *limaVM) removeHostAddresses() { conf, _ := configmanager.LoadInstance() if !conf.Network.Address && conf.Network.HostAddresses { for _, ip := range util.HostIPAddresses() { _ = l.RunQuiet("sudo", "ip", "address", "del", ip.String()+"/24", "dev", "lo") } } } // removeIncusContainerRoute is a safety net for force-stop, // where the Incus container Stop() is skipped. func (l *limaVM) removeIncusContainerRoute() { if !util.MacOS() { return } if l.conf.Runtime != incus.Name { return } if !util.RouteExists(incus.BridgeSubnet) { return } _ = l.host.RunQuiet("sudo", "/sbin/route", "delete", "-net", incus.BridgeSubnet) } ================================================ FILE: environment/vm/lima/shell.go ================================================ package lima import ( "context" "io" "github.com/abiosoft/colima/config" ) func (l limaVM) Run(args ...string) error { args = append([]string{lima}, args...) a := l.Init(context.Background()) a.Add(func() error { return l.host.Run(args...) }) return a.Exec() } func (l limaVM) SSH(workingDir string, args ...string) error { if workingDir == "" { args = append([]string{limactl, "shell", config.CurrentProfile().ID}, args...) } else { args = append([]string{limactl, "shell", "--workdir", workingDir, config.CurrentProfile().ID}, args...) } a := l.Init(context.Background()) a.Add(func() error { return l.host.RunInteractive(args...) }) return a.Exec() } func (l limaVM) RunInteractive(args ...string) error { args = append([]string{lima}, args...) a := l.Init(context.Background()) a.Add(func() error { return l.host.RunInteractive(args...) }) return a.Exec() } func (l limaVM) RunWith(stdin io.Reader, stdout io.Writer, args ...string) error { args = append([]string{lima}, args...) a := l.Init(context.Background()) a.Add(func() error { return l.host.RunWith(stdin, stdout, args...) }) return a.Exec() } func (l limaVM) RunOutput(args ...string) (out string, err error) { args = append([]string{lima}, args...) a := l.Init(context.Background()) a.Add(func() (err error) { out, err = l.host.RunOutput(args...) return }) err = a.Exec() return } func (l limaVM) RunQuiet(args ...string) (err error) { args = append([]string{lima}, args...) a := l.Init(context.Background()) a.Add(func() (err error) { return l.host.RunQuiet(args...) }) err = a.Exec() return } ================================================ FILE: environment/vm/lima/yaml.go ================================================ package lima import ( "context" "fmt" "net" "os" "strings" "github.com/abiosoft/colima/daemon" "github.com/abiosoft/colima/daemon/process/vmnet" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/container/containerd" "github.com/abiosoft/colima/environment/container/docker" "github.com/abiosoft/colima/environment/container/incus" "github.com/abiosoft/colima/environment/vm/lima/limaconfig" "github.com/abiosoft/colima/environment/vm/lima/limautil" "github.com/abiosoft/colima/util" "github.com/sirupsen/logrus" ) func newConf(ctx context.Context, conf config.Config) (l limaconfig.Config, err error) { l.Arch = environment.Arch(conf.Arch).Value() // VM type is qemu except in few scenarios l.VMType = limaconfig.QEMU sameArchitecture := environment.HostArch() == l.Arch // when vz is chosen and OS version supports it if util.MacOS13OrNewer() && conf.VMType == limaconfig.VZ && sameArchitecture { l.VMType = limaconfig.VZ // Rosetta is only available on Apple Silicon if conf.VZRosetta && util.MacOS13OrNewerOnArm() { if util.RosettaRunning() { l.VMOpts.VZOpts.Rosetta.Enabled = true l.VMOpts.VZOpts.Rosetta.BinFmt = true } else { logrus.Warnln("Unable to enable Rosetta: Rosetta2 is not installed") logrus.Warnln("Run 'softwareupdate --install-rosetta' to install Rosetta2") } } if util.MacOSNestedVirtualizationSupported() { l.NestedVirtualization = conf.NestedVirtualization } } // when krunkit is chosen and OS version supports it if util.MacOS13OrNewerOnArm() && conf.VMType == limaconfig.Krunkit && sameArchitecture { l.VMType = limaconfig.Krunkit if util.MacOSNestedVirtualizationSupported() { l.NestedVirtualization = conf.NestedVirtualization } } if conf.CPUType != "" && conf.CPUType != "host" { l.VMOpts.QEMU.CPUType = map[environment.Arch]string{ l.Arch: conf.CPUType, } } if conf.CPU > 0 { l.CPUs = &conf.CPU } if conf.Memory > 0 { l.Memory = fmt.Sprintf("%dMiB", uint32(conf.Memory*1024)) } if conf.RootDisk > 0 { l.Disk = fmt.Sprintf("%dGiB", conf.RootDisk) } l.SSH = limaconfig.SSH{LocalPort: conf.SSHPort, LoadDotSSHPubKeys: false, ForwardAgent: conf.ForwardAgent} l.Containerd = limaconfig.Containerd{System: false, User: false} l.DNS = conf.Network.DNSResolvers l.HostResolver.Enabled = len(conf.Network.DNSResolvers) == 0 l.HostResolver.Hosts = conf.Network.DNSHosts if l.HostResolver.Hosts == nil { l.HostResolver.Hosts = make(map[string]string) } if _, ok := l.HostResolver.Hosts["host.docker.internal"]; !ok { l.HostResolver.Hosts["host.docker.internal"] = "host.lima.internal" } l.Env = conf.Env if l.Env == nil { l.Env = make(map[string]string) } // extra required provision commands { // fix inotify l.Provision = append(l.Provision, limaconfig.Provision{ Mode: limaconfig.ProvisionModeSystem, Script: "sysctl -w fs.inotify.max_user_watches=1048576", }) // add user to docker group // "sudo", "usermod", "-aG", "docker", user if conf.Runtime == docker.Name { l.Provision = append(l.Provision, limaconfig.Provision{ Mode: limaconfig.ProvisionModeDependency, Script: "groupadd -f docker && usermod -aG docker {{ .User }}", }) } // add user to incus-admin group // "sudo", "usermod", "-aG", "incus-admin", user if conf.Runtime == incus.Name { l.Provision = append(l.Provision, limaconfig.Provision{ Mode: limaconfig.ProvisionModeDependency, Script: "groupadd -f incus-admin && usermod -aG incus-admin {{ .User }}", }) } // set hostname hostname := config.CurrentProfile().ID if conf.Hostname != "" { hostname = conf.Hostname } l.Provision = append(l.Provision, limaconfig.Provision{ Mode: limaconfig.ProvisionModeSystem, Script: "grep '127.0.0.1 " + hostname + "' /etc/hosts || echo '127.0.0.1 " + hostname + "' >> /etc/hosts", }) l.Provision = append(l.Provision, limaconfig.Provision{ Mode: limaconfig.ProvisionModeSystem, Script: "hostnamectl set-hostname " + hostname, }) } // network setup { l.Networks = append(l.Networks, limaconfig.Network{ Lima: "user-v2", }) reachableIPAddress := true if conf.Network.Address { metric := limautil.NetMetric if conf.Network.PreferredRoute { metric = limautil.NetMetricPreferred } // vmnet is used for bridged mode, otherwise VZ uses VZNAT if l.VMType == limaconfig.VZ && conf.Network.Mode != "bridged" { l.Networks = append(l.Networks, limaconfig.Network{ VZNAT: true, Interface: limautil.NetInterface, Metric: metric, }) } else { reachableIPAddress, _ = ctx.Value(daemon.CtxKey(vmnet.Name)).(bool) // network is currently limited to macOS. if util.MacOS() && reachableIPAddress { if err := func() error { socketFile := vmnet.Info().Socket.File() // ensure the socket file exists if _, err := os.Stat(socketFile); err != nil { return fmt.Errorf("vmnet socket file not found: %w", err) } l.Networks = append(l.Networks, limaconfig.Network{ Socket: socketFile, Interface: limautil.NetInterface, Metric: metric, }) return nil }(); err != nil { reachableIPAddress = false logrus.Warn(fmt.Errorf("error setting up reachable IP address: %w", err)) } } } // disable ports 80 and 443 when k8s is enabled and there is a reachable IP address // to prevent ingress (traefik) from occupying relevant host ports. if reachableIPAddress && conf.Kubernetes.Enabled && !ingressDisabled(conf.Kubernetes.K3sArgs) { l.PortForwards = append(l.PortForwards, limaconfig.PortForward{ GuestIP: net.IPv4zero, GuestPort: 80, GuestIPMustBeZero: true, Ignore: true, Proto: limaconfig.TCP, }, limaconfig.PortForward{ GuestIP: net.IPv4zero, GuestPort: 443, GuestIPMustBeZero: true, Ignore: true, Proto: limaconfig.TCP, }, ) } // disable port forwarding for Incus when there is a reachable IP address for consistent behaviour if reachableIPAddress && conf.Runtime == incus.Name { l.PortForwards = append(l.PortForwards, limaconfig.PortForward{ GuestIP: net.IPv4zero, GuestIPMustBeZero: true, GuestPortRange: [2]int{1, 65535}, HostPortRange: [2]int{1, 65535}, Ignore: true, Proto: limaconfig.TCP, }, limaconfig.PortForward{ GuestIP: net.ParseIP("127.0.0.1"), GuestPortRange: [2]int{1, 65535}, HostPortRange: [2]int{1, 65535}, Ignore: true, Proto: limaconfig.TCP, }, ) } } } // ports and sockets { // docker socket if conf.Runtime == docker.Name { l.PortForwards = append(l.PortForwards, limaconfig.PortForward{ GuestSocket: "/var/run/docker.sock", HostSocket: docker.HostSocketFile(), Proto: limaconfig.TCP, }, limaconfig.PortForward{ GuestSocket: "/var/run/containerd/containerd.sock", HostSocket: containerd.HostSocketFiles().Containerd, Proto: limaconfig.TCP, }) if config.CurrentProfile().ShortName == "default" { // for backward compatibility, will be removed in future releases l.PortForwards = append(l.PortForwards, limaconfig.PortForward{ GuestSocket: "/var/run/docker.sock", HostSocket: docker.LegacyDefaultHostSocketFile(), Proto: limaconfig.TCP, }) } } // containerd socket if conf.Runtime == containerd.Name { l.PortForwards = append(l.PortForwards, limaconfig.PortForward{ GuestSocket: "/var/run/containerd/containerd.sock", HostSocket: containerd.HostSocketFiles().Containerd, Proto: limaconfig.TCP, }, limaconfig.PortForward{ GuestSocket: "/var/run/buildkit/buildkitd.sock", HostSocket: containerd.HostSocketFiles().Buildkitd, Proto: limaconfig.TCP, }) } // incus socket if conf.Runtime == incus.Name { l.PortForwards = append(l.PortForwards, limaconfig.PortForward{ GuestSocket: "/var/lib/incus/unix.socket", HostSocket: incus.HostSocketFile(), Proto: limaconfig.TCP, }) } if conf.PortForwarder == "none" { // disable port forwarding l.PortForwards = append(l.PortForwards, limaconfig.PortForward{ GuestIP: net.IPv4zero, Proto: "any", Ignore: true, }) } else { // handle port forwarding to allow listening on 0.0.0.0 // bind 0.0.0.0 l.PortForwards = append(l.PortForwards, limaconfig.PortForward{ GuestIPMustBeZero: true, GuestIP: net.IPv4zero, GuestPortRange: [2]int{1, 65535}, HostIP: net.IPv4zero, HostPortRange: [2]int{1, 65535}, Proto: limaconfig.TCP, }, limaconfig.PortForward{ GuestIPMustBeZero: true, GuestIP: net.IPv4zero, GuestPortRange: [2]int{1, 65535}, HostIP: net.IPv4zero, HostPortRange: [2]int{1, 65535}, Proto: limaconfig.UDP, }, ) // bind 127.0.0.1 l.PortForwards = append(l.PortForwards, limaconfig.PortForward{ GuestIP: net.ParseIP("127.0.0.1"), GuestPortRange: [2]int{1, 65535}, HostIP: net.ParseIP("127.0.0.1"), HostPortRange: [2]int{1, 65535}, Proto: limaconfig.TCP, }, limaconfig.PortForward{ GuestIP: net.ParseIP("127.0.0.1"), GuestPortRange: [2]int{1, 65535}, HostIP: net.ParseIP("127.0.0.1"), HostPortRange: [2]int{1, 65535}, Proto: limaconfig.UDP, }, ) // bind all host addresses when network address is not enabled if !conf.Network.Address && conf.Network.HostAddresses { for _, ip := range util.HostIPAddresses() { l.PortForwards = append(l.PortForwards, limaconfig.PortForward{ GuestIP: ip, GuestPortRange: [2]int{1, 65535}, HostIP: ip, HostPortRange: [2]int{1, 65535}, Proto: limaconfig.TCP, }, ) } } } } switch strings.ToLower(conf.MountType) { case "ssh", "sshfs", "reversessh", "reverse-ssh", "reversesshfs", limaconfig.REVSSHFS: l.MountType = limaconfig.REVSSHFS default: if l.VMType == limaconfig.VZ { l.MountType = limaconfig.VIRTIOFS } else { // qemu l.MountType = limaconfig.NINEP } } /* provision scripts for disk actions */ // ensure all volumes are mounted. l.Provision = append(l.Provision, limaconfig.Provision{ Mode: limaconfig.ProvisionModeSystem, Script: "mount -a", }) // trim mounted drive to recover disk space // however problematic for incus if conf.Runtime != incus.Name { l.Provision = append(l.Provision, limaconfig.Provision{ Mode: limaconfig.ProvisionModeSystem, Script: `readlink /usr/sbin/fstrim || fstrim -a`, }) } // grow partition in case disk size has increased l.Provision = append(l.Provision, limaconfig.Provision{ Mode: limaconfig.ProvisionModeSystem, Script: "resize2fs " + diskByLabelPath(config.CurrentProfile().ID) + " || true", }) /* end */ if conf.Mounts != nil && len(conf.Mounts) == 0 { l.Mounts = append(l.Mounts, limaconfig.Mount{Location: "~", Writable: true}, ) } else { // overlapping mounts are problematic in Lima https://github.com/lima-vm/lima/issues/302 if err = checkOverlappingMounts(conf.Mounts); err != nil { err = fmt.Errorf("overlapping mounts not supported: %w", err) return } for _, m := range conf.Mounts { var location, mountPoint string location, err = util.CleanPath(m.Location) if err != nil { return } mountPoint, err = util.CleanPath(m.MountPoint) if err != nil { return } mount := limaconfig.Mount{Location: location, MountPoint: mountPoint, Writable: m.Writable} l.Mounts = append(l.Mounts, mount) } } // provision scripts (only pass Lima-managed modes) for _, script := range conf.Provision { if script.IsColimaMode() { continue } l.Provision = append(l.Provision, limaconfig.Provision{ Mode: script.Mode, Script: script.Script, }) } return } type Arch = environment.Arch func selectPath(m config.Mount) (string, error) { if m.MountPoint != "" { return util.CleanPath(m.MountPoint) } return util.CleanPath(m.Location) } func checkOverlappingMounts(mounts []config.Mount) error { for i := 0; i < len(mounts)-1; i++ { a, err := selectPath(mounts[i]) if err != nil { return err } for j := i + 1; j < len(mounts); j++ { b, err := selectPath(mounts[j]) if err != nil { return err } if strings.HasPrefix(a, b) || strings.HasPrefix(b, a) { return fmt.Errorf("'%s' overlaps '%s'", a, b) } } } return nil } // disableHas checks if the provided feature is indeed found in the disable configuration slice. func ingressDisabled(disableFlags []string) bool { disabled := func(s string) bool { return s == "traefik" || s == "ingress" } for i, f := range disableFlags { if f == "--disable" { if len(disableFlags)-1 <= i { return false } if disabled(disableFlags[i+1]) { return true } continue } str := strings.SplitN(f, "=", 2) if len(str) < 2 || str[0] != "--disable" { continue } if disabled(str[1]) { return true } } return false } const diskLabelMaxLength = 16 // https://tldp.org/HOWTO/Partition/labels.html func diskByLabelPath(instanceId string) string { name := "lima-" + instanceId if len(name) > diskLabelMaxLength { name = name[:diskLabelMaxLength] } return "/dev/disk/by-label/" + name } ================================================ FILE: environment/vm/lima/yaml_test.go ================================================ package lima import ( "context" "fmt" "strconv" "strings" "testing" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment/vm/lima/limaconfig" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/fsutil" ) func Test_checkOverlappingMounts(t *testing.T) { type args struct { mounts []string } tests := []struct { args args wantErr bool }{ {args: args{mounts: []string{"/User", "/User/something"}}, wantErr: true}, {args: args{mounts: []string{"/User/one", "/User/two"}}, wantErr: false}, {args: args{mounts: []string{"/User/one", "/User/one_other"}}, wantErr: false}, {args: args{mounts: []string{"/User/one_other", "/User/one"}}, wantErr: false}, {args: args{mounts: []string{"/User/one", "/User/one/other"}}, wantErr: true}, {args: args{mounts: []string{"/User/one/", "/User/one"}}, wantErr: true}, {args: args{mounts: []string{"/User/one/", "/User/two", "User/one"}}, wantErr: true}, {args: args{mounts: []string{"/home/a/b/c", "/home/b/c/a", "/home/c/a/b"}}, wantErr: false}, } for i, tt := range tests { t.Run(fmt.Sprint(i), func(t *testing.T) { mounts := func(mounts []string) (mnts []config.Mount) { for _, m := range mounts { mnts = append(mnts, config.Mount{Location: m}) } return }(tt.args.mounts) if err := checkOverlappingMounts(mounts); (err != nil) != tt.wantErr { t.Errorf("checkOverlappingMounts() error = %v, wantErr %v", err, tt.wantErr) } }) } } func Test_config_Mounts(t *testing.T) { fsutil.FS = fsutil.FakeFS tests := []struct { mounts []string isDefault bool }{ {mounts: []string{"/User/user", "/tmp/another"}}, {mounts: []string{"/User/another", "/User/something", "/User/else"}}, {mounts: []string{}, isDefault: true}, {mounts: nil}, {mounts: []string{util.HomeDir()}}, } for i, tt := range tests { t.Run(fmt.Sprint(i), func(t *testing.T) { mounts := func(mounts []string) (mnts []config.Mount) { if mounts != nil { mnts = []config.Mount{} } for _, m := range mounts { mnts = append(mnts, config.Mount{Location: m}) } return }(tt.mounts) conf, err := newConf(context.Background(), config.Config{Mounts: mounts}) if err != nil { t.Error(err) return } expectedLocations := tt.mounts if tt.isDefault { expectedLocations = []string{"~"} } sameMounts := func(expectedLocations []string, mounts []limaconfig.Mount) bool { sanitize := func(s string) string { return strings.TrimSuffix(s, "/") + "/" } for i, m := range mounts { if sanitize(m.Location) != sanitize(expectedLocations[i]) { return false } } return true }(expectedLocations, conf.Mounts) if !sameMounts { foundLocations := func() (locations []string) { for _, m := range conf.Mounts { locations = append(locations, m.Location) } return }() t.Errorf("got: %+v, want: %v", foundLocations, expectedLocations) } }) } } func Test_ingressDisabled(t *testing.T) { tests := []struct { args []string want bool }{ {args: []string{"--flag=f", "--another", "flag"}, want: false}, {args: []string{"--disable=traefik", "--version=3"}, want: true}, {args: []string{}, want: false}, {args: []string{"--disable", "traefik", "--one=two"}, want: true}, } for i, tt := range tests { t.Run(strconv.Itoa(i+1), func(t *testing.T) { if got := ingressDisabled(tt.args); got != tt.want { t.Errorf("ingressDisabled() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: environment/vm.go ================================================ package environment import ( "context" "runtime" "github.com/abiosoft/colima/util" ) // VM is virtual machine. type VM interface { GuestActions Dependencies Host() HostActions Teardown(ctx context.Context) error } // VM configurations const ( // ContainerRuntimeKey is the settings key for container runtime. ContainerRuntimeKey = "runtime" ) // Arch is the CPU architecture of the VM. type Arch string const ( X8664 Arch = "x86_64" AARCH64 Arch = "aarch64" ) // HostArch returns the host CPU architecture. func HostArch() Arch { return Arch(runtime.GOARCH).Value() } // GoArch returns the GOARCH equivalent value for the architecture. func (a Arch) GoArch() string { switch a { case X8664: return "amd64" case AARCH64: return "arm64" } return runtime.GOARCH } // Value converts the underlying architecture alias value to one of X8664 or AARCH64. func (a Arch) Value() Arch { switch a { case X8664, AARCH64: return a // accept amd, amd64, x86, x64, arm, arm64 and m1 values case "amd", "amd64", "x86", "x64": return X8664 case "arm", "arm64", "m1": return AARCH64 } return Arch(runtime.GOARCH).Value() } // DefaultVMType returns the default virtual machine type based on the operation // system and availability of Qemu. func DefaultVMType() string { if util.MacOS13OrNewer() { return "vz" } return "qemu" } ================================================ FILE: flake.nix ================================================ { description = "Container runtimes on macOS (and Linux) with minimal setup"; # Last revision with go_1_23 inputs.nixpkgs.url = "github:NixOS/nixpkgs/25.05"; outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; in { packages.default = import ./colima.nix { inherit pkgs; }; devShell = import ./shell.nix { inherit pkgs; }; apps.default = { type = "app"; program = "${self.packages.${system}.default}/bin/colima"; }; } ); } ================================================ FILE: go.mod ================================================ module github.com/abiosoft/colima go 1.25.0 require ( github.com/coreos/go-semver v0.3.1 github.com/docker/go-units v0.5.0 github.com/fatih/color v1.18.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/rjeczalik/notify v0.9.3 github.com/schollz/progressbar/v3 v3.19.0 github.com/sevlyar/go-daemon v0.1.6 github.com/sirupsen/logrus v1.9.4 github.com/spf13/cobra v1.10.2 golang.org/x/term v0.41.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.9 // indirect golang.org/x/sys v0.42.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc= github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= github.com/sevlyar/go-daemon v0.1.6 h1:EUh1MDjEM4BI109Jign0EaknA2izkOyi0LV3ro3QQGs= github.com/sevlyar/go-daemon v0.1.6/go.mod h1:6dJpPatBT9eUwM5VCw9Bt6CdX9Tk6UWvhW3MebLDRKE= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: integration/Dockerfile ================================================ # sample dockerfile to test image building # without pulling from docker hub FROM scratch COPY . /files ================================================ FILE: model/docker.go ================================================ package model import ( "encoding/json" "fmt" "strings" "time" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/environment/host" "github.com/abiosoft/colima/environment/vm/lima" "github.com/abiosoft/colima/util/terminal" log "github.com/sirupsen/logrus" ) // DockerModelInfo represents the output of docker model inspect. type DockerModelInfo struct { ID string `json:"id"` Tags []string `json:"tags"` Config struct { Format string `json:"format"` Quantization string `json:"quantization"` Parameters string `json:"parameters"` Architecture string `json:"architecture"` Size string `json:"size"` } `json:"config"` } // Hash returns the model's hash (without the "sha256:" prefix). func (m *DockerModelInfo) Hash() string { if hash, ok := strings.CutPrefix(m.ID, "sha256:"); ok { return hash } return "" } // ociManifest represents the OCI manifest structure for Docker models. type ociManifest struct { Layers []struct { MediaType string `json:"mediaType"` Digest string `json:"digest"` } `json:"layers"` } // findGGUFPath finds the GGUF file path for a model inside the docker-model-runner container. // It handles both Docker registry models (bundle path) and HuggingFace models (blob path via manifest). // For models without a bundle, it creates the bundle structure by hard-linking the blob. func findGGUFPath(guest environment.VM, modelHash string) (string, error) { // Standard bundle path used by Docker Model Runner for all models bundlePath := fmt.Sprintf("/models/bundles/sha256/%s/model/model.gguf", modelHash) // Check if bundle already exists if err := guest.RunQuiet("docker", "exec", "docker-model-runner", "test", "-f", bundlePath); err == nil { return bundlePath, nil } // Bundle doesn't exist - read manifest to find the GGUF blob and create the bundle manifestPath := fmt.Sprintf("/models/manifests/sha256/%s", modelHash) output, err := guest.RunOutput("docker", "exec", "docker-model-runner", "cat", manifestPath) if err != nil { return "", fmt.Errorf("failed to read model manifest: %w", err) } var manifest ociManifest if err := json.Unmarshal([]byte(output), &manifest); err != nil { return "", fmt.Errorf("failed to parse model manifest: %w", err) } // Find the GGUF layer (mediaType contains "gguf") var blobPath string for _, layer := range manifest.Layers { if strings.Contains(layer.MediaType, "gguf") { if blobHash, ok := strings.CutPrefix(layer.Digest, "sha256:"); ok { blobPath = fmt.Sprintf("/models/blobs/sha256/%s", blobHash) break } } } if blobPath == "" { return "", fmt.Errorf("no GGUF layer found in model manifest") } // Create bundle directory and hard-link the blob (same approach as Docker Model Runner) bundleDir := fmt.Sprintf("/models/bundles/sha256/%s/model", modelHash) if err := guest.RunQuiet("docker", "exec", "docker-model-runner", "mkdir", "-p", bundleDir); err != nil { return "", fmt.Errorf("failed to create bundle directory: %w", err) } if err := guest.RunQuiet("docker", "exec", "docker-model-runner", "ln", blobPath, bundlePath); err != nil { return "", fmt.Errorf("failed to link model file: %w", err) } return bundlePath, nil } // InspectDockerModel returns information about a Docker model. func InspectDockerModel(modelName string) (*DockerModelInfo, error) { guest := lima.New(host.New()) output, err := guest.RunOutput("docker", "model", "inspect", modelName) if err != nil { return nil, fmt.Errorf("error inspecting model %q: %w", modelName, err) } var info DockerModelInfo if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &info); err != nil { return nil, fmt.Errorf("error parsing model info: %w", err) } return &info, nil } // SetupOrUpdateDocker reinstalls Docker Model Runner in the VM. func SetupOrUpdateDocker() error { guest := lima.New(host.New()) log.Println("reinstalling Docker Model Runner...") if err := guest.RunInteractive("docker", "model", "reinstall-runner"); err != nil { return fmt.Errorf("error reinstalling Docker Model Runner: %w", err) } log.Println("Docker Model Runner reinstalled") // Print installed version if version := GetDockerModelVersion(); version != "" { fmt.Println("Docker Model Runner") fmt.Printf("version: %s", version) fmt.Println() } return nil } // GetDockerModelVersion returns the Docker Model Runner version in the VM. // Returns empty string if version cannot be determined. func GetDockerModelVersion() string { guest := lima.New(host.New()) output, err := guest.RunOutput("docker", "model", "version") if err != nil { return "" } return strings.TrimSpace(output) } // EnsureDockerModel ensures a Docker model is available, pulling if necessary. // Returns the normalized model name (resolving aliases like hf.co → huggingface.co). func EnsureDockerModel(modelName string) (string, error) { guest := lima.New(host.New()) // Try to inspect the model first modelInfo, err := InspectDockerModel(modelName) if err != nil { // Model not found locally, try to pull it if pullErr := guest.RunInteractive("docker", "model", "pull", modelName); pullErr != nil { return "", fmt.Errorf("failed to pull model %q: %w", modelName, pullErr) } // Retry inspect after pull modelInfo, err = InspectDockerModel(modelName) if err != nil { return "", fmt.Errorf("failed to inspect model %q after pull: %w", modelName, err) } } // Return the first tag as the normalized name (e.g., "docker.io/ai/smollm2:latest") if len(modelInfo.Tags) > 0 { return modelInfo.Tags[0], nil } return modelName, nil } // DockerModelServeConfig holds configuration for serving a Docker model. type DockerModelServeConfig struct { ModelName string // Model name (e.g., "smollm2") Port int // Host port to expose the model on Threads int // Number of CPU threads (default: 2) GPULayers int // Number of GPU layers (default: 999 = all) } // ServeDockerModel serves a Docker model with llama-server. // It runs llama-server interactively (with visible output) and uses socat to forward the port. // The function blocks until interrupted (Ctrl-C) or llama-server exits. // Note: Call EnsureDockerModel first to ensure the model is available. func ServeDockerModel(cfg DockerModelServeConfig) error { guest := lima.New(host.New()) // Set defaults if cfg.Threads <= 0 { cfg.Threads = 2 } if cfg.GPULayers <= 0 { cfg.GPULayers = 999 } // Get the model info (model should already be available via EnsureDockerModel) modelInfo, err := InspectDockerModel(cfg.ModelName) if err != nil { return fmt.Errorf("failed to inspect model %q: %w", cfg.ModelName, err) } // Check model format - only GGUF models are supported if modelInfo.Config.Format != "gguf" { return fmt.Errorf("model %q has format %q, only GGUF models are supported\n"+ "Try a GGUF version of this model (e.g., from TheBloke on HuggingFace)", cfg.ModelName, modelInfo.Config.Format) } modelHash := modelInfo.Hash() if modelHash == "" { return fmt.Errorf("could not determine hash for model %q", cfg.ModelName) } // Ensure docker-model-runner container is running (needed to find GGUF path) if err := ensureDockerModelRunner(guest); err != nil { return err } // Find the GGUF file path (handles both Docker registry and HuggingFace models) ggufPath, err := findGGUFPath(guest, modelHash) if err != nil { return fmt.Errorf("could not find GGUF file for model %q: %w", cfg.ModelName, err) } // Get container IP containerIP, err := getDockerModelRunnerIP(guest) if err != nil { return err } // Kill any existing socat on this port stopSocat(guest, cfg.Port) // Start socat in background to forward localhost:port → container_ip:port if err := startSocat(guest, cfg.Port, containerIP); err != nil { return fmt.Errorf("failed to start port forwarder: %w", err) } // Run llama-server interactively (blocking, with visible output) // Ctrl-C will be received by the interactive process directly // Use -it for TTY, -i for non-TTY (e.g., piped or CI environments) execFlag := "-i" if terminal.IsTerminal() { execFlag = "-it" } err = guest.RunInteractive("docker", "exec", execFlag, "docker-model-runner", "/app/bin/com.docker.llama-server", "-ngl", fmt.Sprintf("%d", cfg.GPULayers), "--metrics", "--threads", fmt.Sprintf("%d", cfg.Threads), "--model", ggufPath, "--alias", cfg.ModelName, "--host", "0.0.0.0", "--port", fmt.Sprintf("%d", cfg.Port), "--jinja", ) // Cleanup socat on exit (whether normal exit or Ctrl-C) stopSocat(guest, cfg.Port) return err } // ensureDockerModelRunner ensures the docker-model-runner container is running. // Attempts to start it up to 3 times if not found. func ensureDockerModelRunner(guest environment.VM) error { for attempt := 1; attempt <= 3; attempt++ { // Check if container exists if err := guest.RunQuiet("docker", "inspect", "docker-model-runner"); err == nil { return nil } log.Infof("docker-model-runner not found, starting it (attempt %d/3)...", attempt) _ = guest.Run("docker", "model", "start-runner") time.Sleep(2 * time.Second) } return fmt.Errorf("could not start docker-model-runner after 3 attempts") } // getDockerModelRunnerIP returns the IP address of the docker-model-runner container. func getDockerModelRunnerIP(guest environment.VM) (string, error) { output, err := guest.RunOutput("docker", "inspect", "docker-model-runner", "--format", "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}") if err != nil { return "", fmt.Errorf("failed to get container IP: %w", err) } ip := strings.TrimSpace(output) if ip == "" { return "", fmt.Errorf("container IP is empty") } return ip, nil } // startSocat starts socat in the background to forward a port to the container. func startSocat(guest environment.VM, port int, containerIP string) error { cmd := fmt.Sprintf("nohup socat TCP-LISTEN:%d,fork,reuseaddr TCP:%s:%d > /dev/null 2>&1 &", port, containerIP, port) return guest.Run("sh", "-c", cmd) } // stopSocat stops the socat process for a given port. func stopSocat(guest environment.VM, port int) { cmd := fmt.Sprintf("pkill -f 'socat.*TCP-LISTEN:%d' 2>/dev/null || true", port) _ = guest.Run("sh", "-c", cmd) } // StopDockerModelServe stops a Docker model serve instance. func StopDockerModelServe(port int) error { guest := lima.New(host.New()) // Stop the socat proxy on the VM stopCmd := fmt.Sprintf("pkill -f 'socat.*TCP-LISTEN:%d' 2>/dev/null || true", port) if err := guest.Run("sh", "-c", stopCmd); err != nil { log.Debugf("error stopping socat: %v", err) } // Note: llama-server processes inside docker-model-runner are harder to clean up // since they run in the same container. For now, we just stop the socat proxy. // The llama-server process will remain running but be inaccessible. return nil } // IsDockerModelServeRunning checks if a serve instance is running on the given port. func IsDockerModelServeRunning(port int) bool { guest := lima.New(host.New()) // Check if socat is running for this port checkCmd := fmt.Sprintf("pgrep -f 'socat.*TCP-LISTEN:%d' > /dev/null 2>&1", port) err := guest.Run("sh", "-c", checkCmd) return err == nil } ================================================ FILE: model/ramalama.go ================================================ package model import ( "encoding/json" "fmt" "net/http" "strings" "time" "github.com/abiosoft/colima/environment/host" "github.com/abiosoft/colima/environment/vm/lima" "github.com/abiosoft/colima/store" log "github.com/sirupsen/logrus" ) const ramalamaReleasesURL = "https://api.github.com/repos/containers/ramalama/releases/latest" // SetupOrUpdateRamalama installs or updates ramalama. // Call CheckSetup() first to determine if setup is needed and display version info. func SetupOrUpdateRamalama() error { return ProvisionRamalama() } // GetRamalamaVersion returns the currently installed ramalama version in the VM. // Returns empty string if ramalama is not installed or version cannot be determined. func GetRamalamaVersion() string { guest := lima.New(host.New()) output, err := guest.RunOutput("sh", "-c", `export PATH="$HOME/.local/bin:$PATH"; ramalama version 2>/dev/null`) if err != nil { return "" } // Output format: "ramalama version 0.17.1" output = strings.TrimSpace(output) if version, ok := strings.CutPrefix(output, "ramalama version "); ok { return version } return "" } // getLatestRamalamaVersion fetches the latest release version from GitHub. func getLatestRamalamaVersion() (string, error) { client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Get(ramalamaReleasesURL) if err != nil { return "", fmt.Errorf("failed to fetch releases: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) } var release struct { TagName string `json:"tag_name"` } if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { return "", fmt.Errorf("failed to decode response: %w", err) } // Tag might be "v0.17.1" or "0.17.1" version := strings.TrimPrefix(release.TagName, "v") return version, nil } // ramalamaModel represents a model from ramalama ls --json output. type ramalamaModel struct { Name string `json:"name"` Modified string `json:"modified"` Size int64 `json:"size"` } // listRamalamaModels returns all locally available ramalama models. func listRamalamaModels() ([]ramalamaModel, error) { guest := lima.New(host.New()) output, err := guest.RunOutput("sh", "-c", `export PATH="$HOME/.local/bin:$PATH"; ramalama ls --json 2>/dev/null`) if err != nil { return nil, fmt.Errorf("error listing models: %w", err) } output = strings.TrimSpace(output) if output == "" || output == "[]" { return nil, nil } var models []ramalamaModel if err := json.Unmarshal([]byte(output), &models); err != nil { return nil, fmt.Errorf("error parsing model list: %w", err) } return models, nil } // ramalamaModelExists checks if a model exists locally in ramalama. func ramalamaModelExists(modelName string) bool { models, err := listRamalamaModels() if err != nil { return false } // Normalize the input model name normalizedInput := normalizeRamalamaModelName(modelName) for _, m := range models { // Model names in ramalama have format like "hf://TheBloke/..." or "ollama://library/..." normalizedStored := normalizeRamalamaModelName(m.Name) if normalizedInput == normalizedStored { return true } } return false } // normalizeRamalamaModelName normalizes a ramalama model name for comparison. func normalizeRamalamaModelName(name string) string { name = strings.ToLower(strings.TrimSpace(name)) // Normalize different URL formats to a common form // "hf.co/..." -> "hf://..." // "huggingface.co/..." -> "hf://..." name = strings.ReplaceAll(name, "hf.co/", "hf://") name = strings.ReplaceAll(name, "huggingface.co/", "hf://") return name } // EnsureRamalamaModel ensures a ramalama model is available, pulling if necessary. func EnsureRamalamaModel(modelName string) error { if ramalamaModelExists(modelName) { return nil } // Model not found locally, pull it guest := lima.New(host.New()) shellCmd := fmt.Sprintf( `export RAMALAMA_CONTAINER_ENGINE=docker PATH="$HOME/.local/bin:$PATH"; ramalama pull %s`, modelName, ) if err := guest.RunInteractive("sh", "-c", shellCmd); err != nil { return fmt.Errorf("failed to pull model %q: %w", modelName, err) } return nil } // ProvisionRamalama installs ramalama and its dependencies in the VM. func ProvisionRamalama() error { guest := lima.New(host.New()) script := `set -e export PATH="$HOME/.local/bin:$PATH" # ensure pipx is available sudo apt-get update -y && sudo apt-get install -y pipx # install ramalama via pipx; upgrade if ramalama is already installed if command -v ramalama >/dev/null 2>&1; then pipx upgrade ramalama else pipx install ramalama fi # pull ramalama container images docker pull quay.io/ramalama/ramalama docker pull quay.io/ramalama/ramalama-rag # fix ownership of persistent data dir and symlink to expected location sudo chown -R $(id -u):$(id -g) /var/lib/ramalama mkdir -p "$HOME/.local/share" ln -sfn /var/lib/ramalama "$HOME/.local/share/ramalama" ` log.Println("installing AI model runner...") if err := guest.RunInteractive("sh", "-c", script); err != nil { return fmt.Errorf("error setting up AI model runner: %w", err) } log.Println("AI model runner installed") // mark as provisioned if err := store.Set(func(s *store.Store) { s.RamalamaProvisioned = true }); err != nil { return fmt.Errorf("error saving provisioning state: %w", err) } return nil } ================================================ FILE: model/runner.go ================================================ package model import ( "encoding/json" "fmt" "strings" "github.com/abiosoft/colima/app" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/config/configmanager" "github.com/abiosoft/colima/environment/container/docker" "github.com/abiosoft/colima/environment/host" "github.com/abiosoft/colima/environment/vm/lima" "github.com/abiosoft/colima/environment/vm/lima/limaconfig" "github.com/abiosoft/colima/store" "github.com/abiosoft/colima/util" "github.com/abiosoft/colima/util/terminal" "github.com/coreos/go-semver/semver" log "github.com/sirupsen/logrus" ) // RunnerType represents the type of AI model runner. type RunnerType string const ( RunnerDocker RunnerType = "docker" RunnerRamalama RunnerType = "ramalama" ) // SetupStatus contains the result of checking if setup is needed. type SetupStatus struct { // NeedsSetup indicates whether setup/update is required. NeedsSetup bool // CurrentVersion is the currently installed version (empty if not installed). CurrentVersion string // LatestVersion is the latest available version (empty if not checked). LatestVersion string } // Runner defines the interface for AI model runners. type Runner interface { // Name returns the runner type name. Name() RunnerType // DisplayName returns a human-readable name for the runner. DisplayName() string // ValidatePrerequisites checks runner-specific requirements. ValidatePrerequisites(a app.App) error // EnsureProvisioned ensures the runner is set up (no-op for docker). EnsureProvisioned() error // BuildArgs constructs the command arguments for the runner. // Returns an error if the command is not supported. BuildArgs(args []string) ([]string, error) // EnsureModel ensures a model is available (pulls if necessary). // Returns the normalized model name. EnsureModel(model string) (string, error) // Serve starts serving a model on the given port. // This is a blocking call that runs until interrupted. // The model should already be available (call EnsureModel first). Serve(model string, port int) error // CheckSetup checks if setup/update is needed and returns version info. // This should be called before Setup() to display version info on primary screen. CheckSetup() (SetupStatus, error) // Setup installs or updates the runner. // Call CheckSetup() first to determine if setup is needed. Setup() error // GetCurrentVersion returns the currently installed version. GetCurrentVersion() string } // GetRunner returns the appropriate Runner based on type. func GetRunner(runnerType RunnerType) (Runner, error) { switch runnerType { case RunnerDocker: return &dockerRunner{}, nil case RunnerRamalama: return &ramalamaRunner{}, nil default: return nil, fmt.Errorf("unknown runner type: %s (valid options: docker, ramalama)", runnerType) } } // validateCommonPrerequisites checks prerequisites common to all runners. func validateCommonPrerequisites(a app.App) error { // VM must be running if !a.Active() { return fmt.Errorf("%s is not running", config.CurrentProfile().DisplayName) } // check runtime is docker r, err := a.Runtime() if err != nil { return err } if r != docker.Name { return fmt.Errorf("'colima model' requires docker runtime, current runtime is %s\n"+ "Start colima with: colima start --runtime docker --vm-type krunkit", r) } // check VM type is krunkit (required for GPU access) conf, err := configmanager.LoadInstance() if err != nil { return fmt.Errorf("error loading instance config: %w", err) } if conf.VMType != limaconfig.Krunkit { return fmt.Errorf("'colima model' requires krunkit VM type for GPU access, current VM type is %s\n"+ "Start colima with: colima start --runtime docker --vm-type krunkit", conf.VMType) } // check krunkit binary exists on host if err := util.AssertKrunkit(); err != nil { return err } return nil } // dockerRunner implements Runner for Docker Model Runner. type dockerRunner struct{} func (d *dockerRunner) Name() RunnerType { return RunnerDocker } func (d *dockerRunner) DisplayName() string { return "Docker Model Runner" } func (d *dockerRunner) ValidatePrerequisites(a app.App) error { return validateCommonPrerequisites(a) } func (d *dockerRunner) EnsureProvisioned() error { // Docker Model Runner requires no provisioning return nil } func (d *dockerRunner) BuildArgs(args []string) ([]string, error) { // docker model [args...] return append([]string{"docker", "model"}, args...), nil } // EnsureModel ensures a Docker model is available, pulling if necessary. // Returns the normalized model name (resolving aliases like hf.co → huggingface.co). func (d *dockerRunner) EnsureModel(modelName string) (string, error) { return EnsureDockerModel(modelName) } // Serve starts serving a Docker model using llama-server. func (d *dockerRunner) Serve(modelName string, port int) error { return ServeDockerModel(DockerModelServeConfig{ ModelName: modelName, Port: port, }) } // dockerModel represents a model from docker model list --json output. type dockerModel struct { ID string `json:"id"` Tags []string `json:"tags"` } // GetFirstModel returns the first available model from docker model list. // Returns empty string if no models are available. func GetFirstModel() (string, error) { models, err := listDockerModels() if err != nil { return "", err } if len(models) == 0 { return "", nil } // Return the first tag of the first model if len(models[0].Tags) > 0 { return models[0].Tags[0], nil } return "", nil } // listDockerModels returns all available models from docker model list. func listDockerModels() ([]dockerModel, error) { guest := lima.New(host.New()) output, err := guest.RunOutput("docker", "model", "list", "--json") if err != nil { return nil, fmt.Errorf("error listing models: %w", err) } output = strings.TrimSpace(output) if output == "" || output == "[]" { return nil, nil } var models []dockerModel if err := json.Unmarshal([]byte(output), &models); err != nil { return nil, fmt.Errorf("error parsing model list: %w", err) } return models, nil } // ResolveModelName resolves a short model name to its full tag. // Supports flexible matching: // - "smollm2" resolves to "docker.io/ai/smollm2:latest" // - "ai/smollm2" resolves to "docker.io/ai/smollm2:latest" // - "hf.co/..." resolves to "huggingface.co/..." // // Returns the original name if no match is found (for new models to be pulled). func ResolveModelName(name string) (string, error) { models, err := listDockerModels() if err != nil { return name, err } for _, m := range models { for _, tag := range m.Tags { if matchesModel(name, tag) { return tag, nil } } } // Return original name if not found (will be pulled) return name, nil } // matchesModel checks if a user-provided name matches a full model tag. func matchesModel(name, tag string) bool { // Normalize both for comparison normName := normalizeModelName(name) normTag := normalizeModelName(tag) // Exact match after normalization if normName == normTag { return true } // Check if name is a suffix of tag (e.g., "smollm2" matches "ai/smollm2") // Strip the tag version suffix for matching tagParts := strings.Split(normTag, ":") tagWithoutVersion := tagParts[0] tagVersion := "" if len(tagParts) > 1 { tagVersion = tagParts[1] } nameParts := strings.Split(normName, ":") nameWithoutVersion := nameParts[0] nameHasVersion := len(nameParts) > 1 // If input has no version, only match :latest tags if !nameHasVersion && tagVersion != "" && tagVersion != "latest" { return false } // "smollm2" should match "ai/smollm2:latest" if strings.HasSuffix(tagWithoutVersion, "/"+normName) { return true } // "ai/smollm2" should match "docker.io/ai/smollm2" or just "ai/smollm2" if strings.HasSuffix(tagWithoutVersion, "/"+nameWithoutVersion) { return true } // Direct suffix match (handles cases like "tinyllama/tinyllama-1.1b-chat-v1.0") if strings.HasSuffix(tagWithoutVersion, nameWithoutVersion) { return true } return false } // normalizeModelName normalizes a model name for comparison. func normalizeModelName(name string) string { name = strings.ToLower(strings.TrimSpace(name)) // Normalize registry prefixes name = strings.TrimPrefix(name, "docker.io/") name = strings.ReplaceAll(name, "hf.co/", "huggingface.co/") return name } func (d *dockerRunner) CheckSetup() (SetupStatus, error) { // Docker Model Runner always reinstalls; no version comparison return SetupStatus{ NeedsSetup: true, CurrentVersion: GetDockerModelVersion(), }, nil } func (d *dockerRunner) Setup() error { return SetupOrUpdateDocker() } func (d *dockerRunner) GetCurrentVersion() string { return GetDockerModelVersion() } // gpuSubcommands are ramalama subcommands that need GPU device passthrough. var gpuSubcommands = map[string]bool{ "run": true, "serve": true, "bench": true, "chat": true, "perplexity": true, } // ramalamaRunner implements Runner for Ramalama. type ramalamaRunner struct{} func (r *ramalamaRunner) Name() RunnerType { return RunnerRamalama } func (r *ramalamaRunner) DisplayName() string { return "Ramalama" } func (r *ramalamaRunner) ValidatePrerequisites(a app.App) error { return validateCommonPrerequisites(a) } func (r *ramalamaRunner) EnsureProvisioned() error { s, _ := store.Load() if s.RamalamaProvisioned { return nil } prompt := fmt.Sprintf("%s requires initial setup (this may take a few minutes depending on internet connection speed). Continue", r.DisplayName()) if !cli.Prompt(prompt) { return fmt.Errorf("setup cancelled") } separator := "────────────────────────────────────────" header := fmt.Sprintf("Colima - %s Setup\n%s", r.DisplayName(), separator) return terminal.WithAltScreen(ProvisionRamalama, header) } func (r *ramalamaRunner) BuildArgs(args []string) ([]string, error) { return r.buildRamalamaArgs(args), nil } // EnsureModel ensures a ramalama model is available, pulling if necessary. func (r *ramalamaRunner) EnsureModel(modelName string) (string, error) { if err := EnsureRamalamaModel(modelName); err != nil { return "", err } return modelName, nil } // Serve starts serving a model using ramalama. func (r *ramalamaRunner) Serve(modelName string, port int) error { guest := lima.New(host.New()) // ramalama serve with GPU support and custom port shellCmd := fmt.Sprintf( `export RAMALAMA_CONTAINER_ENGINE=docker PATH="$HOME/.local/bin:$PATH"; exec ramalama serve --device=/dev/dri -p %d %s`, port, modelName, ) return guest.RunInteractive("sh", "-c", shellCmd) } func (r *ramalamaRunner) buildRamalamaArgs(args []string) []string { shellCmd := `export RAMALAMA_CONTAINER_ENGINE=docker PATH="$HOME/.local/bin:$PATH"; exec ramalama "$@"` ramalamaArgs := []string{"sh", "-c", shellCmd, "--"} // for GPU subcommands, inject --device=/dev/dri after the subcommand name if len(args) > 0 && gpuSubcommands[args[0]] { ramalamaArgs = append(ramalamaArgs, args[0], "--device=/dev/dri") ramalamaArgs = append(ramalamaArgs, args[1:]...) } else { ramalamaArgs = append(ramalamaArgs, args...) } return ramalamaArgs } func (r *ramalamaRunner) CheckSetup() (SetupStatus, error) { s, _ := store.Load() // Fresh install - no version check needed if !s.RamalamaProvisioned { return SetupStatus{NeedsSetup: true}, nil } // Get current version currentVersion := GetRamalamaVersion() if currentVersion == "" { // Can't determine current version, proceed with update log.Debug("could not determine current ramalama version, proceeding with update") return SetupStatus{NeedsSetup: true}, nil } // Fetch latest version latestVersion, err := getLatestRamalamaVersion() if err != nil { log.Debugf("could not fetch latest ramalama version: %v", err) return SetupStatus{}, fmt.Errorf("could not check for updates: %w", err) } // Compare versions current, err := semver.NewVersion(currentVersion) if err != nil { log.Debugf("could not parse current version %q: %v", currentVersion, err) return SetupStatus{ NeedsSetup: true, CurrentVersion: currentVersion, LatestVersion: latestVersion, }, nil } latest, err := semver.NewVersion(latestVersion) if err != nil { log.Debugf("could not parse latest version %q: %v", latestVersion, err) return SetupStatus{ NeedsSetup: true, CurrentVersion: currentVersion, LatestVersion: latestVersion, }, nil } needsSetup := current.Compare(*latest) < 0 return SetupStatus{ NeedsSetup: needsSetup, CurrentVersion: currentVersion, LatestVersion: latestVersion, }, nil } func (r *ramalamaRunner) Setup() error { return SetupOrUpdateRamalama() } func (r *ramalamaRunner) GetCurrentVersion() string { return GetRamalamaVersion() } ================================================ FILE: model/runner_test.go ================================================ package model import ( "testing" ) func TestNormalizeModelName(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "lowercase conversion", input: "AI/SmollM2", expected: "ai/smollm2", }, { name: "trim whitespace", input: " ai/smollm2 ", expected: "ai/smollm2", }, { name: "strip docker.io prefix", input: "docker.io/ai/smollm2", expected: "ai/smollm2", }, { name: "convert hf.co to huggingface.co", input: "hf.co/tinyllama/model", expected: "huggingface.co/tinyllama/model", }, { name: "already normalized", input: "ai/smollm2:latest", expected: "ai/smollm2:latest", }, { name: "huggingface.co unchanged", input: "huggingface.co/tinyllama/model:latest", expected: "huggingface.co/tinyllama/model:latest", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := normalizeModelName(tt.input) if result != tt.expected { t.Errorf("normalizeModelName(%q) = %q, want %q", tt.input, result, tt.expected) } }) } } func TestMatchesModel(t *testing.T) { tests := []struct { name string input string tag string expected bool }{ // Exact matches { name: "exact match with full tag", input: "docker.io/ai/smollm2:latest", tag: "docker.io/ai/smollm2:latest", expected: true, }, { name: "exact match after normalization", input: "ai/smollm2:latest", tag: "docker.io/ai/smollm2:latest", expected: true, }, // Short name matches { name: "short name matches full tag", input: "smollm2", tag: "docker.io/ai/smollm2:latest", expected: true, }, { name: "short name with ai prefix", input: "ai/smollm2", tag: "docker.io/ai/smollm2:latest", expected: true, }, { name: "short name case insensitive", input: "SmollM2", tag: "docker.io/ai/smollm2:latest", expected: true, }, // Huggingface models { name: "hf.co prefix matches huggingface.co", input: "hf.co/tinyllama/tinyllama-1.1b-chat-v1.0", tag: "huggingface.co/tinyllama/tinyllama-1.1b-chat-v1.0:latest", expected: true, }, { name: "huggingface short name", input: "tinyllama/tinyllama-1.1b-chat-v1.0", tag: "huggingface.co/tinyllama/tinyllama-1.1b-chat-v1.0:latest", expected: true, }, { name: "huggingface model name only", input: "tinyllama-1.1b-chat-v1.0", tag: "huggingface.co/tinyllama/tinyllama-1.1b-chat-v1.0:latest", expected: true, }, // Version tag handling { name: "input without version matches tag with version", input: "ai/smollm2", tag: "docker.io/ai/smollm2:latest", expected: true, }, { name: "input with version matches tag with same version", input: "ai/smollm2:latest", tag: "docker.io/ai/smollm2:latest", expected: true, }, { name: "input with specific version matches tag with same version", input: "ai/smollm2:v1.0", tag: "docker.io/ai/smollm2:v1.0", expected: true, }, { name: "input without version does NOT match tag with specific version", input: "smollm2", tag: "docker.io/ai/smollm2:v1.0", expected: false, }, { name: "short name does NOT match tag with specific version", input: "gemma3", tag: "docker.io/ai/gemma3:4b-it-qat-q4_0", expected: false, }, { name: "input without version matches tag with latest", input: "smollm2", tag: "docker.io/ai/smollm2:latest", expected: true, }, // Non-matches { name: "different model names", input: "smollm2", tag: "docker.io/ai/gemma3:latest", expected: false, }, { name: "partial name should not match", input: "smoll", tag: "docker.io/ai/smollm2:latest", expected: false, }, { name: "different registry", input: "ollama/smollm2", tag: "docker.io/ai/smollm2:latest", expected: false, }, // Edge cases { name: "gemma3 short name", input: "gemma3", tag: "docker.io/ai/gemma3:latest", expected: true, }, { name: "ai/gemma3", input: "ai/gemma3", tag: "docker.io/ai/gemma3:latest", expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := matchesModel(tt.input, tt.tag) if result != tt.expected { t.Errorf("matchesModel(%q, %q) = %v, want %v", tt.input, tt.tag, result, tt.expected) } }) } } func TestResolveModelNameWithMockData(t *testing.T) { // Test the resolution logic by testing matchesModel with various inputs // against a set of mock tags that would come from docker model list mockTags := []string{ "docker.io/ai/smollm2:latest", "huggingface.co/tinyllama/tinyllama-1.1b-chat-v1.0:latest", "docker.io/ai/gemma3:latest", } tests := []struct { name string input string shouldMatch string // empty if no match expected }{ { name: "smollm2 resolves to full tag", input: "smollm2", shouldMatch: "docker.io/ai/smollm2:latest", }, { name: "ai/smollm2 resolves to full tag", input: "ai/smollm2", shouldMatch: "docker.io/ai/smollm2:latest", }, { name: "gemma3 resolves to full tag", input: "gemma3", shouldMatch: "docker.io/ai/gemma3:latest", }, { name: "tinyllama model resolves", input: "tinyllama/tinyllama-1.1b-chat-v1.0", shouldMatch: "huggingface.co/tinyllama/tinyllama-1.1b-chat-v1.0:latest", }, { name: "hf.co prefix resolves", input: "hf.co/tinyllama/tinyllama-1.1b-chat-v1.0", shouldMatch: "huggingface.co/tinyllama/tinyllama-1.1b-chat-v1.0:latest", }, { name: "unknown model returns no match", input: "unknown-model", shouldMatch: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var matched string for _, tag := range mockTags { if matchesModel(tt.input, tag) { matched = tag break } } if matched != tt.shouldMatch { t.Errorf("resolving %q: got %q, want %q", tt.input, matched, tt.shouldMatch) } }) } } ================================================ FILE: scripts/build_vmnet.sh ================================================ #!/usr/bin/env sh set -ex export DIR_BUILD=$PWD/_build/network export DIR_VMNET=$DIR_BUILD/socket_vmnet export EMBED_DIR=$PWD/embedded/network clone() ( if [ ! -d "$2" ]; then git clone "$1" "$2" fi ) mkdir -p "$DIR_BUILD" clone https://github.com/lima-vm/socket_vmnet.git "$DIR_VMNET" move_to_embed_dir() ( mkdir -p "$EMBED_DIR"/vmnet/bin cp "$DIR_VMNET"/socket_vmnet "$DIR_VMNET"/socket_vmnet_client "$EMBED_DIR"/vmnet/bin cd "$EMBED_DIR"/vmnet && tar cvfz "$EMBED_DIR"/vmnet_"${1}".tar.gz bin/socket_vmnet bin/socket_vmnet_client rm -rf "$EMBED_DIR"/vmnet ) build_x86_64() ( cd "$DIR_VMNET" # pinning to a commit for consistency git checkout v1.1.5 make ARCH=x86_64 move_to_embed_dir x86_64 # cleanup make clean ) build_arm64() ( cd "$DIR_VMNET" # pinning to a commit for consistency git checkout v1.1.5 make ARCH=arm64 move_to_embed_dir arm64 # cleanup make clean ) test_archives() ( TEMP_DIR=/tmp/colima-test-archives rm -rf $TEMP_DIR mkdir -p $TEMP_DIR/x86 $TEMP_DIR/arm ( cp "$EMBED_DIR"/vmnet_x86_64.tar.gz $TEMP_DIR/x86 cd $TEMP_DIR/x86 && tar xvfz vmnet_x86_64.tar.gz ) ( cp "$EMBED_DIR"/vmnet_arm64.tar.gz $TEMP_DIR/arm cd $TEMP_DIR/arm && tar xvfz vmnet_arm64.tar.gz ) assert_not_equal() ( if diff $TEMP_DIR/x86/"$1" $TEMP_DIR/arm/"$1"; then echo "$1" is same for both arch exit 1 fi ) assert_not_equal bin/socket_vmnet assert_not_equal bin/socket_vmnet_client ) build_x86_64 build_arm64 test_archives ================================================ FILE: scripts/integration.sh ================================================ #!/usr/bin/env bash set -ex alias colima="$COLIMA_BINARY" DOCKER_CONTEXT="$(docker info -f '{{json .}}' | jq -r '.ClientInfo.Context')" OTHER_ARCH="amd64" if [ "$GOARCH" == "amd64" ]; then OTHER_ARCH="arm64" fi stage() ( set +x echo echo "######################################" echo "$@" echo "######################################" echo set -x ) test_runtime() ( stage "runtime: $2, arch: $1" NAME="itest-$2" COLIMA="$COLIMA_BINARY -p $NAME" COMMAND="docker" if [ "$2" == "containerd" ]; then COMMAND="$COLIMA nerdctl --" fi # reset $COLIMA delete -f # start $COLIMA start --arch "$1" --runtime "$2" # validate $COMMAND ps && $COMMAND info # validate DNS $COLIMA ssh -- nslookup host.docker.internal # valid building image $COMMAND build integration # teardown $COLIMA delete -f ) test_kubernetes() ( stage "k8s runtime: $2, arch: $1" NAME="itest-$2-k8s" COLIMA="$COLIMA_BINARY -p $NAME" # reset $COLIMA delete -f # start $COLIMA start --arch "$1" --runtime "$2" --kubernetes # short delay sleep 5 # validate kubectl cluster-info && kubectl version && kubectl get nodes -o wide # teardown $COLIMA delete -f ) test_runtime $GOARCH docker test_runtime $GOARCH containerd test_kubernetes $GOARCH docker test_kubernetes $GOARCH containerd test_runtime $OTHER_ARCH docker test_runtime $OTHER_ARCH containerd if [ -n "$DOCKER_CONTEXT" ]; then docker context use "$DOCKER_CONTEXT" || echo # prevent error fi ================================================ FILE: shell.nix ================================================ { pkgs ? import { } }: pkgs.mkShell { # nativeBuildInputs is usually what you want -- tools you need to run nativeBuildInputs = with pkgs.buildPackages; [ go_1_23 gotools git lima qemu ]; shellHook = '' echo Nix Shell with $(go version) echo COLIMA_BIN="$PWD/$(make print-binary-name)" if [ ! -f "$COLIMA_BIN" ]; then echo "Run 'make' to build Colima." echo fi set -x set -x alias colima="$COLIMA_BIN" set +x ''; } ================================================ FILE: store/store.go ================================================ package store import ( "encoding/json" "fmt" "os" "github.com/abiosoft/colima/config" "github.com/sirupsen/logrus" ) // Store stores internal Colima configuration for an instance type Store struct { // if the runtime disk has been formatted. DiskFormatted bool `json:"disk_formatted"` // the container runtime the disk is provisioned for DiskRuntime string `json:"disk_runtime"` // if ramalama has been provisioned in the VM RamalamaProvisioned bool `json:"ramalama_provisioned"` } func storeFile() string { return config.CurrentProfile().StoreFile() } // Load loads the store from the json file. func Load() (s Store, err error) { b, err := os.ReadFile(storeFile()) if err != nil { return s, fmt.Errorf("cannot read store file: %w", err) } if err := json.Unmarshal(b, &s); err != nil { return s, fmt.Errorf("error unmarshaling store file: %w", err) } return s, nil } // save persists the store. func save(s Store) error { b, err := json.MarshalIndent(s, "", " ") if err != nil { return fmt.Errorf("error marshaling store: %w", err) } if err := os.WriteFile(storeFile(), b, 0o644); err != nil { return fmt.Errorf("error writing store file: %w", err) } return nil } // Set provides an easy way to set a value in the store. func Set(f func(*Store)) error { s, err := Load() if err != nil { logrus.Debug("error loading store: %w", err) } f(&s) if err := save(s); err != nil { return fmt.Errorf("error saving store: %w", err) } return nil } // Reset resets the values in the store to the defaults. func Reset() error { // first attempt to remove store file if err := os.Remove(storeFile()); err != nil { // if it fails // then attempt to set it to empty value return Set(func(s *Store) { *s = Store{} }) } return nil } ================================================ FILE: util/debutil/debutil.go ================================================ package debutil import ( "context" "fmt" "strings" "github.com/abiosoft/colima/cli" "github.com/abiosoft/colima/environment" ) // packages is list of deb package names. type packages []string // Upgradable returns the shell command to check if the packages are upgradable with apt. // The returned command should be passed to 'sh -c' or equivalent. func (p packages) Upgradable() string { cmd := "sudo apt list --upgradable | grep" for _, v := range p { cmd += fmt.Sprintf(" -e '^%s/'", v) } return cmd } // Install returns the shell command to install the packages with apt. // The returned command should be passed to 'sh -c' or equivalent. func (p packages) Install() string { return "sudo apt-get install -y --allow-change-held-packages " + strings.Join(p, " ") } func UpdateRuntime( ctx context.Context, guest environment.GuestActions, chain cli.CommandChain, packageNames ...string, ) (bool, error) { a := chain.Init(ctx) log := a.Logger() packages := packages(packageNames) hasUpdates := false updated := false a.Stage("refreshing package manager") a.Add(func() error { return guest.RunQuiet( "sh", "-c", "sudo apt-get update -y", ) }) a.Stage("checking for updates") a.Add(func() error { err := guest.RunQuiet( "sh", "-c", packages.Upgradable(), ) hasUpdates = err == nil return nil }) a.Add(func() (err error) { if !hasUpdates { log.Warnln("no updates available") return } log.Println("updating packages ...") err = guest.RunQuiet( "sh", "-c", packages.Install(), ) if err == nil { updated = true log.Println("done") } return }) // it is necessary to execute the chain here to get the correct value for `updated`. err := a.Exec() return updated, err } ================================================ FILE: util/downloader/curl.go ================================================ package downloader import ( "fmt" "os" "os/exec" "path" "strings" "github.com/abiosoft/colima/util/terminal" ) const ( // DownloaderNative uses Go's native HTTP client DownloaderNative = "native" // DownloaderCurl uses the curl command (honors .curlrc) DownloaderCurl = "curl" envDownloader = "COLIMA_DOWNLOADER" ) // ValidateDownloader validates the downloader value (case-insensitive). // Returns the normalized value or an error if invalid. func ValidateDownloader(v string) (string, error) { switch strings.ToLower(v) { case DownloaderNative: return DownloaderNative, nil case DownloaderCurl: return DownloaderCurl, nil default: return "", fmt.Errorf("invalid downloader %q: must be one of %s, %s", v, DownloaderNative, DownloaderCurl) } } // curlDownloader handles downloads using the curl command type curlDownloader struct{} // Download downloads a file using curl func (c *curlDownloader) Download(r Request, destPath string) error { // check if curl is available if _, err := exec.LookPath("curl"); err != nil { return fmt.Errorf("curl not found in PATH: %w", err) } args := []string{ "-fSL", // fail on HTTP errors, show errors, follow redirects "-C", "-", // resume if possible (auto-detect offset) "--progress-bar", // show progress bar "-o", destPath, // output file r.URL, } cmd := exec.Command("curl", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("curl download failed for '%s': %w", path.Base(r.URL), err) } terminal.ClearLine() return nil } ================================================ FILE: util/downloader/download.go ================================================ package downloader import ( "encoding/json" "fmt" "os" "path" "path/filepath" "strings" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/environment" "github.com/abiosoft/colima/util/osutil" "github.com/abiosoft/colima/util/shautil" ) type ( hostActions = environment.HostActions guestActions = environment.GuestActions ) // Request is download request type Request struct { URL string // request URL SHA *SHA // shasum url } // FileDownloader is the interface for downloading files type FileDownloader interface { Download(r Request, destPath string) error } // fileDownloader is the configured downloader implementation var fileDownloader FileDownloader = &nativeDownloader{} // SetDownloader sets the downloader implementation based on the provided type. // The value should be validated before calling this function. func SetDownloader(v string) { if v == DownloaderCurl { fileDownloader = &curlDownloader{} } else { fileDownloader = &nativeDownloader{} } } func init() { // check environment variable for default downloader if v := osutil.EnvVar(envDownloader).Val(); v != "" { if d, err := ValidateDownloader(v); err == nil { SetDownloader(d) } } } // DownloadToGuest downloads file at url and saves it in the destination. // // In the implementation, the file is downloaded (and cached) on the host, // then copied to the guest using limactl copy. // filename must be an absolute path and a directory on the guest that does not require root access. func DownloadToGuest(host hostActions, guest guestActions, r Request, filename string) error { // if file is on the filesystem, no need for download. A copy suffices if strings.HasPrefix(r.URL, "/") { return CopyToGuest(host, r.URL, filename) } cacheFile, err := Download(host, r) if err != nil { return err } return CopyToGuest(host, cacheFile, filename) } // CopyToGuest copies a file or directory from the host to the guest VM using limactl copy. func CopyToGuest(host hostActions, src, dest string) error { instanceName := config.CurrentProfile().ID return host.RunQuiet("limactl", "copy", "-r", src, instanceName+":"+dest) } // Download downloads file at url and returns the location of the downloaded file. func Download(host hostActions, r Request) (string, error) { d := downloader{} if !d.hasCache(r.URL) { if err := d.downloadFile(r); err != nil { return "", err } } return CacheFilename(r.URL), nil } type downloader struct{} // CacheFilename returns the computed filename for the url. func CacheFilename(url string) string { return filepath.Join(config.CacheDir(), "caches", shautil.SHA256(url).String()) } func (d downloader) cacheDownloadingFileName(url string) string { return CacheFilename(url) + ".downloading" } func (d downloader) resumeInfoPath(url string) string { return CacheFilename(url) + ".resume" } func (d downloader) downloadFile(r Request) (err error) { cacheDownloadingFilename := d.cacheDownloadingFileName(r.URL) // create cache directory cacheDir := filepath.Dir(cacheDownloadingFilename) if err := os.MkdirAll(cacheDir, 0755); err != nil { return fmt.Errorf("error preparing cache dir: %w", err) } if err := fileDownloader.Download(r, cacheDownloadingFilename); err != nil { return err } // validate download if SHA is present if r.SHA != nil { if err := r.SHA.validateDownload(r.URL, cacheDownloadingFilename); err != nil { // move file to allow subsequent re-download _ = os.Rename(cacheDownloadingFilename, cacheDownloadingFilename+".invalid") return fmt.Errorf("error validating SHA sum for '%s': %w", path.Base(r.URL), err) } } // move completed download to final location if err := os.Rename(cacheDownloadingFilename, CacheFilename(r.URL)); err != nil { return fmt.Errorf("error finalizing download: %w", err) } return nil } func (d downloader) saveResumeInfo(url, etag string, bytesWritten int64) { info := ResumeInfo{ETag: etag, BytesWritten: bytesWritten} data, _ := json.Marshal(info) _ = os.WriteFile(d.resumeInfoPath(url), data, 0644) } func (d downloader) hasCache(url string) bool { _, err := os.Stat(CacheFilename(url)) return err == nil } ================================================ FILE: util/downloader/errors.go ================================================ package downloader import ( "errors" "fmt" "net" "net/url" "path" "syscall" ) // Sentinel errors for type checking var ( ErrNetworkConnection = errors.New("network connection error") ErrHTTPStatus = errors.New("HTTP error") ErrResumeFailed = errors.New("resume failed") ErrSHAValidation = errors.New("SHA validation failed") ) // NetworkError wraps network-related errors with user-friendly messages type NetworkError struct { Op string // "connect", "resolve", "download" URL string Err error } func (e *NetworkError) Error() string { return fmt.Sprintf("%s failed for '%s': %s", e.Op, e.URL, e.friendlyMessage()) } func (e *NetworkError) Unwrap() error { return e.Err } func (e *NetworkError) friendlyMessage() string { // check for DNS resolution errors var dnsErr *net.DNSError if errors.As(e.Err, &dnsErr) { return fmt.Sprintf("DNS lookup failed for host '%s'. Check your network connection or DNS settings", dnsErr.Name) } // check for connection refused var opErr *net.OpError if errors.As(e.Err, &opErr) { if errors.Is(opErr.Err, syscall.ECONNREFUSED) { return "connection refused. The server may be down or unreachable" } if opErr.Timeout() { return "connection timed out. Check your network connection or try again later" } } // check for URL parsing errors var urlErr *url.Error if errors.As(e.Err, &urlErr) { if urlErr.Timeout() { return "request timed out. The server may be slow or overloaded" } } return e.Err.Error() } // HTTPStatusError represents HTTP error responses type HTTPStatusError struct { StatusCode int Status string URL string } func (e *HTTPStatusError) Error() string { switch e.StatusCode { case 404: return fmt.Sprintf("file not found at '%s'. The URL may be incorrect or the file may have been removed", e.URL) case 403: return fmt.Sprintf("access forbidden to '%s'. You may need authentication or the resource is restricted", e.URL) case 401: return fmt.Sprintf("authentication required for '%s'", e.URL) case 500, 502, 503, 504: return fmt.Sprintf("server error (%d) at '%s'. Try again later", e.StatusCode, e.URL) case 416: // Range Not Satisfiable return fmt.Sprintf("resume failed for '%s'. The server does not support the requested byte range", e.URL) default: return fmt.Sprintf("HTTP %d (%s) for '%s'", e.StatusCode, e.Status, e.URL) } } func (e *HTTPStatusError) Unwrap() error { return ErrHTTPStatus } // ResumeError indicates a failed resume attempt type ResumeError struct { Reason string URL string } func (e *ResumeError) Error() string { return fmt.Sprintf("cannot resume download of '%s': %s. Starting fresh download", path.Base(e.URL), e.Reason) } func (e *ResumeError) Unwrap() error { return ErrResumeFailed } // SHAValidationError indicates checksum mismatch type SHAValidationError struct { File string Expected string Actual string Size int // 256 or 512 } func (e *SHAValidationError) Error() string { return fmt.Sprintf("SHA%d checksum mismatch for '%s':\n expected: %s\n actual: %s\nThe file may be corrupted or tampered with. Delete the cached file and retry", e.Size, e.File, e.Expected, e.Actual) } func (e *SHAValidationError) Unwrap() error { return ErrSHAValidation } ================================================ FILE: util/downloader/http.go ================================================ package downloader import ( "context" "fmt" "io" "net" "net/http" "os" "strconv" "strings" "time" "github.com/abiosoft/colima/config" "github.com/schollz/progressbar/v3" "golang.org/x/term" ) // HTTPClient encapsulates HTTP download operations type HTTPClient struct { client *http.Client userAgent string } // DownloadOptions configures a download operation type DownloadOptions struct { URL string DestPath string ExpectedETag string // for resume validation ResumeFromByte int64 // byte offset to resume from ShowProgress bool } // DownloadResult contains metadata about the completed download type DownloadResult struct { FinalURL string // After following redirects ETag string // For future resume validation TotalBytes int64 WasResumed bool } // ResumeInfo stores metadata for resumable downloads type ResumeInfo struct { ETag string `json:"etag"` BytesWritten int64 `json:"bytes_written"` } // NewHTTPClient creates a configured HTTP client func NewHTTPClient() *HTTPClient { transport := &http.Transport{ // use proxy from environment (HTTP_PROXY, HTTPS_PROXY, NO_PROXY) Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, TLSHandshakeTimeout: 10 * time.Second, ResponseHeaderTimeout: 30 * time.Second, ExpectContinueTimeout: 1 * time.Second, } return &HTTPClient{ client: &http.Client{ Transport: transport, // checkRedirect is left default - Go follows up to 10 redirects // and returns the final response }, userAgent: "colima/" + config.AppVersion().Version, } } // GetFinalURL follows redirects and returns the final URL func (h *HTTPClient) GetFinalURL(ctx context.Context, rawURL string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodHead, rawURL, nil) if err != nil { return "", fmt.Errorf("invalid URL '%s': %w", rawURL, err) } req.Header.Set("User-Agent", h.userAgent) resp, err := h.client.Do(req) if err != nil { return "", &NetworkError{Op: "resolve redirect", URL: rawURL, Err: err} } defer func() { _ = resp.Body.Close() }() // check for HTTP errors if resp.StatusCode >= 400 { return "", &HTTPStatusError{ StatusCode: resp.StatusCode, Status: resp.Status, URL: rawURL, } } // resp.Request.URL contains the final URL after redirects return resp.Request.URL.String(), nil } // Download performs a file download with optional resume support func (h *HTTPClient) Download(ctx context.Context, opts DownloadOptions) (*DownloadResult, error) { result := &DownloadResult{} // open destination file for writing (or appending if resuming) var file *os.File var existingSize int64 var err error if opts.ResumeFromByte > 0 { file, err = os.OpenFile(opts.DestPath, os.O_WRONLY|os.O_APPEND, 0644) if err != nil { // can't resume, start fresh opts.ResumeFromByte = 0 opts.ExpectedETag = "" } else { existingSize = opts.ResumeFromByte } } if file == nil { file, err = os.Create(opts.DestPath) if err != nil { return nil, fmt.Errorf("cannot create file '%s': %w", opts.DestPath, err) } } defer func() { _ = file.Close() }() // build request req, err := http.NewRequestWithContext(ctx, http.MethodGet, opts.URL, nil) if err != nil { return nil, fmt.Errorf("invalid URL '%s': %w", opts.URL, err) } req.Header.Set("User-Agent", h.userAgent) // add Range header for resume if existingSize > 0 { req.Header.Set("Range", fmt.Sprintf("bytes=%d-", existingSize)) // add If-Range with ETag if available for safe resume if opts.ExpectedETag != "" { req.Header.Set("If-Range", opts.ExpectedETag) } } // execute request resp, err := h.client.Do(req) if err != nil { return nil, &NetworkError{Op: "download", URL: opts.URL, Err: err} } defer func() { _ = resp.Body.Close() }() // store final URL after redirects result.FinalURL = resp.Request.URL.String() result.ETag = resp.Header.Get("ETag") // handle response status switch resp.StatusCode { case http.StatusOK: // 200 - Full content (resume not supported or If-Range failed) if existingSize > 0 { // server sent full content, need to truncate and start over if err := file.Truncate(0); err != nil { return nil, fmt.Errorf("cannot truncate file for fresh download: %w", err) } if _, err := file.Seek(0, 0); err != nil { return nil, fmt.Errorf("cannot seek to start of file: %w", err) } existingSize = 0 } result.TotalBytes = resp.ContentLength case http.StatusPartialContent: // 206 - Resume successful result.WasResumed = true // Content-Range: bytes 21010-47021/47022 contentRange := resp.Header.Get("Content-Range") if totalSize := parseContentRangeTotal(contentRange); totalSize > 0 { result.TotalBytes = totalSize } else { result.TotalBytes = existingSize + resp.ContentLength } case http.StatusRequestedRangeNotSatisfiable: // 416 // file is likely complete or server doesn't support range return nil, &HTTPStatusError{ StatusCode: resp.StatusCode, Status: resp.Status, URL: opts.URL, } default: if resp.StatusCode >= 400 { return nil, &HTTPStatusError{ StatusCode: resp.StatusCode, Status: resp.Status, URL: opts.URL, } } } // set up progress bar var writer io.Writer = file var bar *progressbar.ProgressBar if opts.ShowProgress && isTerminal() { bar = h.createProgressBar(result.TotalBytes, existingSize) writer = io.MultiWriter(file, bar) } // stream response body to file written, err := io.Copy(writer, resp.Body) if err != nil { return result, &NetworkError{Op: "download", URL: opts.URL, Err: err} } // finish progress bar if bar != nil { _ = bar.Finish() } result.TotalBytes = existingSize + written return result, nil } // Fetch downloads content from a URL and returns it as bytes (for small files like SHA checksums) func (h *HTTPClient) Fetch(ctx context.Context, url string) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("invalid URL '%s': %w", url, err) } req.Header.Set("User-Agent", h.userAgent) resp, err := h.client.Do(req) if err != nil { return nil, &NetworkError{Op: "fetch", URL: url, Err: err} } defer func() { _ = resp.Body.Close() }() if resp.StatusCode >= 400 { return nil, &HTTPStatusError{ StatusCode: resp.StatusCode, Status: resp.Status, URL: url, } } // limit read to 1MB for safety (SHA files should be tiny) return io.ReadAll(io.LimitReader(resp.Body, 1<<20)) } // createProgressBar creates a progress bar for download visualization func (h *HTTPClient) createProgressBar(totalBytes, startOffset int64) *progressbar.ProgressBar { opts := []progressbar.Option{ progressbar.OptionSetDescription(" "), progressbar.OptionSetWriter(os.Stderr), progressbar.OptionShowBytes(true), progressbar.OptionSetWidth(30), progressbar.OptionThrottle(100 * time.Millisecond), progressbar.OptionClearOnFinish(), progressbar.OptionSetPredictTime(true), progressbar.OptionSetRenderBlankState(true), progressbar.OptionSetTheme(progressbar.Theme{ Saucer: "=", SaucerHead: ">", SaucerPadding: " ", BarStart: "[", BarEnd: "]", }), } // if total size is unknown, use a spinner if totalBytes <= 0 { opts = append(opts, progressbar.OptionSpinnerType(11)) totalBytes = -1 } bar := progressbar.NewOptions64(totalBytes, opts...) // if resuming, set initial progress if startOffset > 0 { _ = bar.Set64(startOffset) } return bar } // parseContentRangeTotal extracts total size from Content-Range header // Format: "bytes 21010-47021/47022" or "bytes 21010-47021/*" func parseContentRangeTotal(header string) int64 { if header == "" { return -1 } parts := strings.Split(header, "/") if len(parts) != 2 || parts[1] == "*" { return -1 } total, err := strconv.ParseInt(parts[1], 10, 64) if err != nil { return -1 } return total } // isTerminal returns true if stderr is a terminal func isTerminal() bool { return term.IsTerminal(int(os.Stderr.Fd())) } ================================================ FILE: util/downloader/native.go ================================================ package downloader import ( "context" "encoding/json" "fmt" "os" "path" "time" ) // nativeDownloader uses Go's native HTTP client type nativeDownloader struct{} // Download downloads a file using Go's native HTTP client func (n *nativeDownloader) Download(r Request, destPath string) error { d := downloader{} // check for existing partial download and resume info var resumeInfo ResumeInfo resumeInfoPath := d.resumeInfoPath(r.URL) if data, err := os.ReadFile(resumeInfoPath); err == nil { _ = json.Unmarshal(data, &resumeInfo) } // get existing file size for resume var existingSize int64 if stat, err := os.Stat(destPath); err == nil { existingSize = stat.Size() } // create HTTP client client := NewHTTPClient() // use a long timeout for large files (2 hours) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour) defer cancel() // get final URL (follows redirects) finalURL, err := client.GetFinalURL(ctx, r.URL) if err != nil { return fmt.Errorf("error resolving download URL '%s': %w", r.URL, err) } // download the file result, err := client.Download(ctx, DownloadOptions{ URL: finalURL, DestPath: destPath, ExpectedETag: resumeInfo.ETag, ResumeFromByte: existingSize, ShowProgress: true, }) if err != nil { // save resume info for next attempt if we have ETag if result != nil && result.ETag != "" { d.saveResumeInfo(r.URL, result.ETag, existingSize) } return fmt.Errorf("error downloading '%s': %w", path.Base(r.URL), err) } // clean up resume info on successful download _ = os.Remove(resumeInfoPath) return nil } ================================================ FILE: util/downloader/sha.go ================================================ package downloader import ( "bufio" "bytes" "context" "crypto/sha256" "crypto/sha512" "fmt" "hash" "io" "os" "path/filepath" "strings" "time" ) // SHA is the shasum of a file. type SHA struct { Digest string // shasum URL string // url to download the shasum file (if Digest is empty) Size int // one of 256 or 512 } // ValidateFile validates the SHA of the file. // The host parameter is kept for API compatibility but is not used. func (s SHA) ValidateFile(host hostActions, file string) error { return s.validateFile(file) } // validateFile performs SHA validation using pure Go crypto. func (s SHA) validateFile(file string) error { // open the file f, err := os.Open(file) if err != nil { return fmt.Errorf("cannot open file for validation: %w", err) } defer func() { _ = f.Close() }() // select hash algorithm var h hash.Hash switch s.Size { case 256: h = sha256.New() case 512: h = sha512.New() default: return fmt.Errorf("unsupported SHA size: %d (must be 256 or 512)", s.Size) } // compute hash if _, err := io.Copy(h, f); err != nil { return fmt.Errorf("error reading file for SHA validation: %w", err) } // compare computed := fmt.Sprintf("%x", h.Sum(nil)) expected := strings.TrimPrefix(s.Digest, fmt.Sprintf("sha%d:", s.Size)) expected = strings.ToLower(strings.TrimSpace(expected)) if computed != expected { return &SHAValidationError{ File: filepath.Base(file), Expected: expected, Actual: computed, Size: s.Size, } } return nil } func (s SHA) validateDownload(url string, filename string) error { if s.URL == "" && s.Digest == "" { return fmt.Errorf("error validating SHA: one of Digest or URL must be set") } // fetch digest from URL if empty if s.Digest == "" { // retrieve the filename from the download url. targetFilename := "" if url != "" { split := strings.Split(url, "/") targetFilename = split[len(split)-1] } digest, err := fetchSHAFromURL(s.URL, targetFilename) if err != nil { return err } s.Digest = digest } return s.validateFile(filename) } // fetchSHAFromURL fetches SHA checksum file and extracts digest for the target file func fetchSHAFromURL(shaURL, targetFilename string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() client := NewHTTPClient() // fetch SHA file content data, err := client.Fetch(ctx, shaURL) if err != nil { return "", fmt.Errorf("error downloading SHA file from '%s': %w", shaURL, err) } // parse SHA file to find the matching entry digest, err := parseSHAContent(data, targetFilename) if err != nil { return "", fmt.Errorf("error parsing SHA file from '%s': %w", shaURL, err) } return digest, nil } // parseSHAContent reads SHA checksum content and extracts the digest for the target filename. // Supports formats: // - GNU coreutils: " " (two spaces) // - BSD/binary mode: " *" (space + asterisk) func parseSHAContent(data []byte, targetFilename string) (string, error) { scanner := bufio.NewScanner(bytes.NewReader(data)) for scanner.Scan() { line := scanner.Text() // format: " " (two spaces) or " *" (binary mode) parts := strings.Fields(line) if len(parts) >= 2 { hash := parts[0] filename := strings.TrimPrefix(parts[len(parts)-1], "*") if filename == targetFilename || strings.HasSuffix(filename, "/"+targetFilename) { return hash, nil } } } if err := scanner.Err(); err != nil { return "", err } return "", fmt.Errorf("no SHA entry found for '%s' in checksum file", targetFilename) } ================================================ FILE: util/fsutil/fs.go ================================================ package fsutil import ( "io/fs" "os" "testing/fstest" ) // FS is the host filesystem implementation. var FS FileSystem = DefaultFS{} // MkdirAll calls FS.MakedirAll func MkdirAll(path string, perm os.FileMode) error { return FS.MkdirAll(path, perm) } // Open calls FS.Open func Open(name string) (fs.File, error) { return FS.Open(name) } // FS is abstraction for filesystem. type FileSystem interface { MkdirAll(path string, perm os.FileMode) error fs.FS } var _ FileSystem = DefaultFS{} var _ FileSystem = fakeFS{} // DefaultFS is the default OS implementation of FileSystem. type DefaultFS struct{} // Open implements FS func (DefaultFS) Open(name string) (fs.File, error) { return os.Open(name) } // MkdirAll implements FS func (DefaultFS) MkdirAll(path string, perm fs.FileMode) error { return os.MkdirAll(path, perm) } // FakeFS is a mock FS. The following can be done in a test before usage. // // osutil.FS = osutil.FakeFS var FakeFS FileSystem = fakeFS{} type fakeFS struct{} // Open implements FileSystem func (fakeFS) Open(name string) (fs.File, error) { return fstest.MapFS{name: &fstest.MapFile{ Data: []byte("fake file - " + name), }}.Open(name) } // MkdirAll implements FileSystem func (fakeFS) MkdirAll(path string, perm fs.FileMode) error { return nil } ================================================ FILE: util/macos.go ================================================ package util import ( "bytes" "encoding/json" "fmt" "os/exec" "regexp" "runtime" "strconv" "strings" "github.com/abiosoft/colima/cli" "github.com/coreos/go-semver/semver" "github.com/sirupsen/logrus" ) // MacOS returns if the current OS is macOS. func MacOS() bool { return runtime.GOOS == "darwin" } // MacOS13OrNewer returns if the current OS is macOS 13 or newer. func MacOS13OrNewerOnArm() bool { return runtime.GOARCH == "arm64" && MacOS13OrNewer() } // MacOS13OrNewer returns if the current OS is macOS 13 or newer. func MacOS13OrNewer() bool { return minMacOSVersion("13.0.0") } // MacOS15OrNewer returns if the current OS is macOS 15 or newer. func MacOS15OrNewer() bool { return minMacOSVersion("15.0.0") } // MacOSNestedVirtualizationSupported returns if the current device supports nested virtualization. func MacOSNestedVirtualizationSupported() bool { return IsMxOrNewer(3) && MacOS15OrNewer() } func minMacOSVersion(version string) bool { if !MacOS() { return false } ver, err := macOSProductVersion() if err != nil { logrus.Warnln(fmt.Errorf("error retrieving macOS version: %w", err)) return false } cver, err := semver.NewVersion(version) if err != nil { logrus.Warnln(fmt.Errorf("error parsing version: %w", err)) return false } return cver.Compare(*ver) <= 0 } // IsMxOrNewer returns true if the machine is Apple Silicon M{n} where n >= min // e.g. IsMxOrNewer(3) returns true for M3, M4, M5, ... func IsMxOrNewer(min int) bool { chip, err := chipDetector.GetChipType() if err != nil { logrus.Trace(fmt.Errorf("error getting chip type: %w", err)) return false } n, ok := parseMNumber(chip) if !ok { return false } return n >= min } // chipTypeDetector fetches the chip type string from the host. type chipTypeDetector interface { GetChipType() (string, error) } // systemProfilerChipDetector is the production implementation that calls // `system_profiler -json SPHardwareDataType`. type systemProfilerChipDetector struct{} func (d systemProfilerChipDetector) GetChipType() (string, error) { if !MacOS() { return "", fmt.Errorf("not macOS") } var resp struct { SPHardwareDataType []struct { ChipType string `json:"chip_type"` } `json:"SPHardwareDataType"` } var buf bytes.Buffer cmd := cli.Command("system_profiler", "-json", "SPHardwareDataType") cmd.Stdout = &buf if err := cmd.Run(); err != nil { return "", fmt.Errorf("error retrieving chip version: %w", err) } if err := json.NewDecoder(&buf).Decode(&resp); err != nil { return "", fmt.Errorf("error decoding system_profiler response: %w", err) } if len(resp.SPHardwareDataType) == 0 { return "", fmt.Errorf("no SPHardwareDataType in response") } return resp.SPHardwareDataType[0].ChipType, nil } // chipDetector is the instance used by IsMxOrNewer. Tests can replace // this with a fake implementation. var chipDetector chipTypeDetector = systemProfilerChipDetector{} var mRe = regexp.MustCompile(`\bM(\d+)\b`) func parseMNumber(s string) (int, bool) { if s == "" { return 0, false } matches := mRe.FindStringSubmatch(strings.ToUpper(s)) if len(matches) < 2 { return 0, false } n, err := strconv.Atoi(matches[1]) if err != nil { return 0, false } return n, true } // RosettaRunning checks if Rosetta process is running. func RosettaRunning() bool { if !MacOS() { return false } cmd := cli.Command("pgrep", "oahd") cmd.Stderr = nil cmd.Stdout = nil return cmd.Run() == nil } // macOSProductVersion returns the host's macOS version. func macOSProductVersion() (*semver.Version, error) { cmd := exec.Command("sw_vers", "-productVersion") // output is like "12.3.1\n" b, err := cmd.Output() if err != nil { return nil, fmt.Errorf("failed to execute %v: %w", cmd.Args, err) } verTrimmed := strings.TrimSpace(string(b)) // macOS 12.4 returns just "12.4\n" for strings.Count(verTrimmed, ".") < 2 { verTrimmed += ".0" } verSem, err := semver.NewVersion(verTrimmed) if err != nil { return nil, fmt.Errorf("failed to parse macOS version %q: %w", verTrimmed, err) } return verSem, nil } ================================================ FILE: util/macos_test.go ================================================ package util import ( "fmt" "testing" ) type fakeDetector struct { v string e error } func (f fakeDetector) GetChipType() (string, error) { return f.v, f.e } func TestParseMNumber(t *testing.T) { cases := []struct { in string want int ok bool }{ {"M3", 3, true}, {"APPLE M1", 1, true}, {"M10 Pro", 10, true}, {"apple m3 pro", 3, true}, {"Apple M1", 1, true}, {"No M here", 0, false}, {"", 0, false}, {"ARM64", 0, false}, } for _, c := range cases { n, ok := parseMNumber(c.in) if ok != c.ok || n != c.want { t.Fatalf("parseMNumber(%q) = (%d, %v), want (%d, %v)", c.in, n, ok, c.want, c.ok) } } } func TestIsMxOrNewer(t *testing.T) { cases := []struct { name string chip string chipErr error min int want bool }{ {"m3 satisfies min=3", "Apple M3 Pro", nil, 3, true}, {"m3 satisfies min=1", "Apple M3 Pro", nil, 1, true}, {"m3 does not satisfy min=4", "Apple M3 Pro", nil, 4, false}, {"m1 satisfies min=1", "Apple M1", nil, 1, true}, {"m1 does not satisfy min=2", "Apple M1", nil, 2, false}, {"m10 satisfies min=10", "Apple M10", nil, 10, true}, {"m10 satisfies min=3", "Apple M10", nil, 3, true}, {"chip fetch error returns false", "", fmt.Errorf("not mac"), 3, false}, {"non-apple chip returns false", "INTEL CORE I9", nil, 1, false}, {"empty chip returns false", "", nil, 1, false}, } orig := chipDetector defer func() { chipDetector = orig }() for _, c := range cases { t.Run(c.name, func(t *testing.T) { chipDetector = fakeDetector{v: c.chip, e: c.chipErr} got := IsMxOrNewer(c.min) if got != c.want { t.Fatalf("IsMxOrNewer(%d) = %v, want %v (chip=%q)", c.min, got, c.want, c.chip) } }) } } ================================================ FILE: util/osutil/os.go ================================================ package osutil import ( "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" "github.com/sirupsen/logrus" ) // EnvVar is environment variable type EnvVar string // Exists checks if the environment variable has been set. func (e EnvVar) Exists() bool { _, ok := os.LookupEnv(string(e)) return ok } // Bool returns the environment variable value as boolean. func (e EnvVar) Bool() bool { ok, _ := strconv.ParseBool(e.Val()) return ok } // Bool returns the environment variable value. func (e EnvVar) Val() string { return os.Getenv(string(e)) } // Or returns the environment variable value if set, otherwise returns val. func (e EnvVar) ValOr(val string) string { if v := os.Getenv(string(e)); v != "" { return v } return val } // WithPath appends p to the environment variable value as path list. func (e EnvVar) WithPath(p string) string { if v := e.Val(); v != "" { return v + string(os.PathListSeparator) + p } return p } const EnvColimaBinary = "COLIMA_BINARY" // Executable returns the path name for the executable that started // the current process. func Executable() string { e, err := func(s string) (string, error) { // prioritize env var in case this is a nested process if e := os.Getenv(EnvColimaBinary); e != "" { return e, nil } if filepath.IsAbs(s) { return s, nil } e, err := exec.LookPath(s) if err != nil { return "", fmt.Errorf("error looking up '%s' in PATH: %w", s, err) } abs, err := filepath.Abs(e) if err != nil { return "", fmt.Errorf("error computing absolute path of '%s': %w", e, err) } return abs, nil }(os.Args[0]) if err != nil { // this should never happen, thereby it is safe to do logrus.Traceln(fmt.Errorf("cannot detect current running executable: %w", err)) logrus.Traceln("falling back to first CLI argument") return os.Args[0] } return e } // Socket is a unix socket type Socket string // Unix returns the unix address for the socket. func (s Socket) Unix() string { return "unix://" + s.File() } // File returns the file path for the socket. func (s Socket) File() string { return strings.TrimPrefix(string(s), "unix://") } ================================================ FILE: util/qemu.go ================================================ package util import ( "fmt" "os/exec" ) // AssertQemuImg checks if qemu-img is available. func AssertQemuImg() error { cmd := "qemu-img" if _, err := exec.LookPath(cmd); err != nil { return fmt.Errorf("%s not found, run 'brew install %s' to install", cmd, "qemu") } return nil } // AssertKrunkit checks if krunkit is available. func AssertKrunkit() error { if _, err := exec.LookPath("krunkit"); err != nil { return fmt.Errorf("krunkit not found in $PATH\nInstall with: brew tap slp/krunkit && brew install krunkit") } return nil } ================================================ FILE: util/shautil/sha.go ================================================ package shautil import ( "crypto/sha1" "crypto/sha256" "fmt" ) // SHA is a sha computation type SHA interface { String() string Bytes() []byte } type s1 [20]byte func (s s1) String() string { return fmt.Sprintf("%x", s[:]) } func (s s1) Bytes() []byte { return s[:] } type s256 [32]byte func (s s256) String() string { return fmt.Sprintf("%x", s[:]) } func (s s256) Bytes() []byte { return s[:] } // SHA256Hash computes a sha256sum of a string. func SHA256(s string) SHA { return s256(sha256.Sum256([]byte(s))) } // SHA256Hash computes a sha256sum of a string. func SHA1(s string) SHA { return s1(sha1.Sum([]byte(s))) } ================================================ FILE: util/template.go ================================================ package util import ( "bytes" "fmt" "os" "text/template" ) // WriteTemplate writes template with body to file after applying values. func WriteTemplate(body string, file string, values any) error { b, err := ParseTemplate(body, values) if err != nil { return err } return os.WriteFile(file, b, 0644) } // ParseTemplate parses template with body and values and returns the resulting bytes. func ParseTemplate(body string, values any) ([]byte, error) { t, err := template.New("").Parse(body) if err != nil { return nil, fmt.Errorf("error parsing template: %w", err) } var b bytes.Buffer if err := t.Execute(&b, values); err != nil { return nil, fmt.Errorf("error executing template: %w", err) } return b.Bytes(), err } ================================================ FILE: util/terminal/output.go ================================================ package terminal import ( "bytes" "fmt" "io" "os" "strconv" "strings" "sync" "time" "github.com/fatih/color" "golang.org/x/term" ) var _ io.WriteCloser = (*verboseWriter)(nil) type verboseWriter struct { buf bytes.Buffer lines []string lineHeight int termWidth int overflow int lastUpdate time.Time sync.Mutex } // NewVerboseWriter creates a new verbose writer. // A verbose writer pipes the input received to the stdout while tailing the specified lines. // Calling `Close` when done is recommended to clear the last uncleared output. func NewVerboseWriter(lineHeight int) io.WriteCloser { return &verboseWriter{lineHeight: lineHeight} } func (v *verboseWriter) Write(p []byte) (n int, err error) { // if it's not a terminal, simply write to stdout if !isTerminal { return os.Stdout.Write(p) } v.Lock() defer v.Unlock() for i, c := range p { if c != '\n' { v.buf.WriteByte(c) continue } if err := v.refresh(); err != nil { return i + 1, err } } return len(p), nil } func (v *verboseWriter) printLineVerbose() { line := v.sanitizeLine(v.buf.String()) line = color.HiBlackString(line) _, _ = fmt.Fprintln(os.Stderr, line) } func (v *verboseWriter) refresh() error { v.clearScreen() v.addLine() return v.printScreen() } func (v *verboseWriter) addLine() { defer v.buf.Reset() // if height <=0, do not scroll if v.lineHeight <= 0 { v.printLineVerbose() return } if len(v.lines) >= v.lineHeight { v.lines = v.lines[1:] } v.lines = append(v.lines, v.buf.String()) } func (v *verboseWriter) Close() error { v.Lock() defer v.Unlock() if v.buf.Len() > 0 { if err := v.refresh(); err != nil { return err } } v.clearScreen() return nil } func (v *verboseWriter) sanitizeLine(line string) string { // remove logrus noises if strings.HasPrefix(line, "time=") && strings.Contains(line, "msg=") { line = line[strings.Index(line, "msg=")+4:] if l, err := strconv.Unquote(line); err == nil { line = l } } return "> " + line } func (v *verboseWriter) printScreen() error { if err := v.updateTerm(); err != nil { return err } v.overflow = 0 for _, line := range v.lines { line = v.sanitizeLine(line) if len(line) > v.termWidth { v.overflow += len(line) / v.termWidth if len(line)%v.termWidth == 0 { v.overflow -= 1 } } line = color.HiBlackString(line) fmt.Println(line) } return nil } func (v *verboseWriter) clearScreen() { for i := 0; i < len(v.lines)+v.overflow; i++ { ClearLine() } } func (v *verboseWriter) updateTerm() error { // no need to refresh so quickly if time.Since(v.lastUpdate) < time.Second*2 { return nil } v.lastUpdate = time.Now().UTC() w, _, err := term.GetSize(int(os.Stdout.Fd())) if err != nil { return fmt.Errorf("error getting terminal size: %w", err) } // A width of zero would result in a division by zero panic when computing overflow // in printScreen. Therefore, set it to a safe - even though probably wrong - value. // We use <= 0 here because negative values are guaranteed to lead to unexpected // results, even if they don't cause panics. if w <= 0 { w = 80 } v.termWidth = w return nil } ================================================ FILE: util/terminal/terminal.go ================================================ package terminal import ( "fmt" "os" "os/signal" "strings" "golang.org/x/term" ) var isTerminal = term.IsTerminal(int(os.Stdout.Fd())) // IsTerminal returns true if stdout is a terminal. func IsTerminal() bool { return isTerminal } // ClearLine clears the previous line of the terminal func ClearLine() { if !isTerminal { return } fmt.Print("\033[1A \033[2K \r") } // EnterAltScreen switches to the alternate screen buffer. // This preserves the main terminal content which can be restored // by calling ExitAltScreen. func EnterAltScreen() { if !isTerminal { return } // Switch to alternate screen buffer and move cursor to top-left fmt.Print("\033[?1049h\033[H") } // ExitAltScreen switches back to the main screen buffer, // restoring the previous terminal content. func ExitAltScreen() { if !isTerminal { return } fmt.Print("\033[?1049l") } // WithAltScreen runs the provided function in the alternate screen buffer. // The main terminal content is preserved and restored after the function completes. // Handles Ctrl-C to ensure the terminal is restored even on interrupt. // // If header lines are provided, they are joined with newlines and displayed as a // fixed header at the top of the screen. The command output scrolls below the header. // The number of header lines is computed automatically based on newlines and terminal width. func WithAltScreen(fn func() error, header ...string) error { hasHeader := len(header) > 0 var headerText string if hasHeader { headerText = strings.Join(header, "\n") } if !isTerminal { if hasHeader { fmt.Println(headerText) } return fn() } EnterAltScreen() // Handle Ctrl-C to ensure terminal is restored even on interrupt sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt) defer signal.Stop(sigCh) done := make(chan struct{}) go func() { select { case <-sigCh: if hasHeader { fmt.Print("\033[r") // Reset scroll region } ExitAltScreen() os.Exit(1) case <-done: return } }() if hasHeader { // Get terminal dimensions width, height, err := term.GetSize(int(os.Stdout.Fd())) if err != nil { width = 80 height = 24 } // Print the header fmt.Println(headerText) // Calculate number of lines used by the header headerLines := countLines(headerText, width) + 1 // +1 for padding // Set scroll region from headerLines+1 to bottom // This keeps the header fixed while everything below scrolls fmt.Printf("\033[%d;%dr", headerLines+1, height) // Move cursor to the first line of the scroll region fmt.Printf("\033[%d;1H", headerLines+1) } err := fn() if hasHeader { // Reset scroll region fmt.Print("\033[r") } close(done) ExitAltScreen() return err } // countLines calculates the number of terminal lines a string will occupy, // accounting for newlines and line wrapping based on terminal width. func countLines(s string, termWidth int) int { if termWidth <= 0 { termWidth = 80 } lines := 1 currentLineLen := 0 for _, ch := range s { if ch == '\n' { lines++ currentLineLen = 0 } else { currentLineLen++ if currentLineLen >= termWidth { lines++ currentLineLen = 0 } } } return lines } ================================================ FILE: util/util.go ================================================ package util import ( "fmt" "net" "os" "os/exec" "path/filepath" "strings" "github.com/google/shlex" "github.com/sirupsen/logrus" ) // HomeDir returns the user home directory. func HomeDir() string { home, err := os.UserHomeDir() if err != nil { // this should never happen logrus.Fatal(fmt.Errorf("error retrieving home directory: %w", err)) } return home } // RandomAvailablePort returns an available port on the host machine. func RandomAvailablePort() int { listener, err := net.Listen("tcp", ":0") if err != nil { logrus.Fatal(fmt.Errorf("error picking an available port: %w", err)) } if err := listener.Close(); err != nil { logrus.Fatal(fmt.Errorf("error closing temporary port listener: %w", err)) } return listener.Addr().(*net.TCPAddr).Port } // isPortAvailable checks if a specific port is available on the host. func isPortAvailable(port int) bool { listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) if err != nil { return false } if err := listener.Close(); err != nil { return false } return true } // FindAvailablePort finds the first available port starting from startPort. // It checks up to maxAttempts consecutive ports (startPort, startPort+1, ...). // Returns the available port and true if found, or 0 and false if no port is available. func FindAvailablePort(startPort, maxAttempts int) (int, bool) { for i := range maxAttempts { port := startPort + i if isPortAvailable(port) { return port, true } } return 0, false } // HostIPAddresses returns all IPv4 addresses on the host. func HostIPAddresses() []net.IP { var addresses []net.IP ints, err := net.InterfaceAddrs() if err != nil { return nil } for i := range ints { split := strings.Split(ints[i].String(), "/") addr := net.ParseIP(split[0]).To4() // ignore default loopback if addr != nil && addr.String() != "127.0.0.1" { addresses = append(addresses, addr) } } return addresses } // SubnetAvailable checks if a subnet (in CIDR notation) does not conflict // with any existing host network interface addresses. func SubnetAvailable(subnet string) bool { _, cidr, err := net.ParseCIDR(subnet) if err != nil { return false } addrs, err := net.InterfaceAddrs() if err != nil { return false } for _, addr := range addrs { ip, _, err := net.ParseCIDR(addr.String()) if err != nil { continue } if ip = ip.To4(); ip == nil { continue } if cidr.Contains(ip) { return false } } return true } // RouteExists checks if a route exists for the given subnet on macOS. func RouteExists(subnet string) bool { if !MacOS() { return false } ip, _, err := net.ParseCIDR(subnet) if err != nil { return false } out, err := exec.Command("netstat", "-rn", "-f", "inet").Output() if err != nil { return false } // macOS netstat shows /24 subnets without trailing .0 // e.g. "192.168.100" instead of "192.168.100.0" networkAddr := strings.TrimSuffix(ip.String(), ".0") for _, line := range strings.Split(string(out), "\n") { fields := strings.Fields(line) if len(fields) > 0 && (fields[0] == networkAddr || fields[0] == subnet) { return true } } return false } // ShellSplit splits cmd into arguments using. func ShellSplit(cmd string) []string { split, err := shlex.Split(cmd) if err != nil { logrus.Warnln("error splitting into args: %w", err) logrus.Warnln("falling back to whitespace split", err) split = strings.Fields(cmd) } return split } // CleanPath returns the absolute path to the mount location. // If location is an empty string, nothing is done. func CleanPath(location string) (string, error) { if location == "" { return "", nil } str := os.ExpandEnv(location) if strings.HasPrefix(str, "~") { str = strings.Replace(str, "~", HomeDir(), 1) } str = filepath.Clean(str) if !filepath.IsAbs(str) { return "", fmt.Errorf("relative paths not supported for mount '%s'", location) } return strings.TrimSuffix(str, "/") + "/", nil } ================================================ FILE: util/yamlutil/yaml.go ================================================ package yamlutil import ( "bytes" "fmt" "os" "reflect" "strconv" "strings" "github.com/abiosoft/colima/config" "github.com/abiosoft/colima/embedded" "gopkg.in/yaml.v3" ) // WriteYAML encodes struct to file as YAML. func WriteYAML(value any, file string) error { b, err := yaml.Marshal(value) if err != nil { return fmt.Errorf("error encoding YAML: %w", err) } return os.WriteFile(file, b, 0644) } // Save saves the config. func Save(c config.Config, file string) error { b, err := encodeYAML(c) if err != nil { return err } if err := os.WriteFile(file, b, 0644); err != nil { return fmt.Errorf("error writing yaml file: %w", err) } return nil } func encodeYAML(conf config.Config) ([]byte, error) { var doc yaml.Node f, err := embedded.Read("defaults/colima.yaml") if err != nil { return nil, fmt.Errorf("error reading config file: %w", err) } if err := yaml.Unmarshal(f, &doc); err != nil { return nil, fmt.Errorf("embedded default config is invalid yaml: %w", err) } if l := len(doc.Content); l != 1 { return nil, fmt.Errorf("unexpected error during yaml decode: doc has multiple children of len %d", l) } root := doc.Content[0] // get all nodes nodeVals := map[string]*yaml.Node{} if err := traverseNode("", root, nodeVals); err != nil { return nil, fmt.Errorf("error traversing yaml node: %w", err) } // get all node values structVals := map[string]any{} traverseConfig("", conf, structVals) // apply values to nodes for key, node := range nodeVals { val := structVals[key] // top level, ignore. except known maps. if node.Kind == yaml.MappingNode { switch val.(type) { case map[string]any: case map[string]string: default: continue } } // nil slices are converted to untyped nil to encode as `null` instead of `[]`. // this preserves nil vs empty slice distinction when the yaml is loaded back. if v := reflect.ValueOf(val); v.Kind() == reflect.Slice && v.IsNil() { val = nil } // lazy way, delegate node construction to the yaml library via a roundtrip. // no performance concern as only one file is being read b, err := yaml.Marshal(val) if err != nil { return nil, fmt.Errorf("unexpected error nested value encoding: %w", err) } var newNode yaml.Node if err := yaml.Unmarshal(b, &newNode); err != nil { return nil, fmt.Errorf("unexpected error during yaml node traversal: %w", err) } if l := len(newNode.Content); l != 1 { return nil, fmt.Errorf("unexpected error during yaml node traversal: doc has multiple children of len %d", l) } *node = *newNode.Content[0] } b, err := encode(root) if err != nil { return nil, fmt.Errorf("error encoding yaml file: %w", err) } return b, nil } func traverseConfig(parentKey string, s any, vals map[string]any) { typ := reflect.TypeOf(s) val := reflect.ValueOf(s) // everything else is a value, no nesting required if typ.Kind() != reflect.Struct { vals[parentKey] = val.Interface() return } // traverse the struct fields recursively for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) key := strings.TrimSuffix(field.Tag.Get("yaml"), ",omitempty") if key == "" || key == "-" { // no yaml tag is present continue } if parentKey != "" { key = parentKey + "." + key } val := val.Field(i) traverseConfig(key, val.Interface(), vals) } } func traverseNode(parentKey string, node *yaml.Node, vals map[string]*yaml.Node) error { switch node.Kind { case yaml.MappingNode: if l := len(node.Content); l%2 != 0 { return fmt.Errorf("uneven children of %d found for mapping node", l) } for i := 0; i < len(node.Content); i += 2 { if i > 1 { // fix jumbled comments if cn := node.Content[i]; cn.HeadComment != "" { if strings.Index(cn.HeadComment, "#") == 0 { cn.HeadComment = "\n" + cn.HeadComment } } } key := node.Content[i].Value val := node.Content[i+1] if parentKey != "" { key = parentKey + "." + key } vals[key] = val if err := traverseNode(key, val, vals); err != nil { return err } } case yaml.SequenceNode: for i := 0; i < len(node.Content); i++ { key := strconv.Itoa(i) val := node.Content[i] if parentKey != "" { key = parentKey + "." + key } vals[key] = val if err := traverseNode(key, val, vals); err != nil { return err } } } // yaml.ScalarNode has nothing to do return nil } func encode(v any) ([]byte, error) { var buf bytes.Buffer enc := yaml.NewEncoder(&buf) enc.SetIndent(2) err := enc.Encode(v) return buf.Bytes(), err } ================================================ FILE: util/yamlutil/yaml_test.go ================================================ package yamlutil import ( "net" "reflect" "testing" "github.com/abiosoft/colima/config" "gopkg.in/yaml.v3" ) func Test_encode_Docker(t *testing.T) { conf := config.Config{ Docker: map[string]any{"insecure-registries": []any{"127.0.0.1"}}, Network: config.Network{DNSResolvers: []net.IP{net.ParseIP("1.1.1.1")}}, Kubernetes: config.Kubernetes{K3sArgs: []string{"--disable=traefik"}}, } tests := []struct { name string args config.Config want config.Config wantErr bool }{ {name: "nested", args: conf, want: conf}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b, err := encodeYAML(tt.args) var got config.Config if err := yaml.Unmarshal(b, &got); err != nil { t.Errorf("resulting byte is not a valid yaml: %v", err) return } if (err != nil) != tt.wantErr { t.Errorf("save() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got.Docker, tt.want.Docker) { t.Errorf("save() = %+v\nwant %+v", got.Docker, tt.want.Docker) } }) } }