[
  {
    "path": ".editorconfig",
    "content": "[*.go]\nindent_style = tab"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: abiosoft\ncustom:\n  - \"https://buymeacoffee.com/abiosoft\"\npatreon: colima\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "content": "name: Bug report\ndescription: Report a bug or issue\nbody:\n  - type: textarea\n    attributes:\n      label: Description\n      description: A clear and concise description of what the issue is.\n  - type: textarea\n    attributes:\n      label: Version\n      description: Please show the output of `colima version && limactl --version && qemu-img --version`.\n  - type: checkboxes\n    attributes:\n      label: Operating System\n      description: Which Operating System/Architecture does this issue happen on? Check all that apply.\n      options:\n        - label: macOS Intel <= 13 (Ventura)\n          required: false\n        - label: macOS Intel >= 14 (Sonoma)\n          required: false\n        - label: Apple Silicon <= 13 (Ventura)\n          required: false\n        - label: Apple Silicon >= 14 (Sonoma)\n          required: false\n        - label: Linux\n          required: false\n  - type: textarea\n    attributes:\n      label: Output of `colima status`\n      description: The output of `colima status` or `colima status -p <profilename>` tells us what vm-type and mount type, etc.\n      value:\n  - type: textarea\n    attributes:\n      label: Reproduction Steps\n      description: Kindly walk us through the steps to reproduce this behaviour.\n      value: |\n        1.\n        2.\n        3.\n  - type: textarea\n    attributes:\n      label: Expected behaviour\n      description: A clear and concise description of what you expected to happen.\n  - type: textarea\n    attributes:\n      label: Additional context\n      description: Add any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yaml",
    "content": "name: Feature request\ndescription: Request a missing feature\nbody:\n  - type: textarea\n    attributes:\n      label: Description\n"
  },
  {
    "path": ".github/dependabot.yaml",
    "content": "version: 2\nupdates:\n- package-ecosystem: \"gomod\"\n  directory: \"/\"\n  schedule:\n    interval: \"daily\"\n  open-pull-requests-limit: 10\n- package-ecosystem: \"github-actions\"\n  directory: \"/\"\n  schedule:\n    interval: \"weekly\""
  },
  {
    "path": ".github/workflows/go.yml",
    "content": "name: Go\n\non:\n  push:\n    tags: [\"v*\"]\n    paths-ignore:\n      - \"**/*.md\"\n      - \"**/*.nix\"\n      - \"**/*.lock\"\n  pull_request:\n    branches: [main]\n    paths-ignore:\n      - \"**/*.md\"\n      - \"**/*.nix\"\n      - \"**/*.lock\"\n\npermissions: write-all\n\njobs:\n  build-linux:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: \"1.26.1\"\n\n      - name: Build\n        run: go build -v ./...\n\n      - name: Test\n        run: go test -v ./...\n\n  build-macos:\n    runs-on: macos-15-intel\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: \"1.26.1\"\n\n      - name: Build\n        run: go build -v ./...\n\n      - name: Test\n        run: go test -v ./...\n\n  binaries-linux:\n    needs: \"build-linux\"\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: \"1.26.1\"\n\n      - name: install gcc-aarch64-linux-gnu\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y gcc-aarch64-linux-gnu\n\n      - name: generate binaries\n        run: |\n          OS=Linux ARCH=x86_64 make\n          OS=Linux ARCH=aarch64 make\n\n      - name: upload artifacts\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: artifacts-linux\n          path: _output/binaries/\n\n  binaries-macos:\n    needs: \"build-macos\"\n    runs-on: macos-15-intel\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: \"1.26.1\"\n\n      - name: generate binaries\n        run: |\n          CGO_ENABLED=1 OS=Darwin ARCH=x86_64 make\n          CGO_ENABLED=1 OS=Darwin ARCH=arm64 make\n\n      - name: upload artifacts\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: artifacts-macos\n          path: _output/binaries/\n\n\n  release:\n    needs: [\"binaries-linux\", \"binaries-macos\"]\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0\n        with:\n          name: artifacts-linux\n          path: _output/binaries/\n      - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0\n        with:\n          name: artifacts-macos\n          path: _output/binaries/\n      - name: create release\n        if: github.event_name != 'pull_request'\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: >\n          tag=\"${GITHUB_REF##*/}\"\n\n          gh release create \"${tag}\" --draft --title \"${tag}\"\n          _output/binaries/colima-Darwin-x86_64\n          _output/binaries/colima-Darwin-x86_64.sha256sum\n          _output/binaries/colima-Darwin-arm64\n          _output/binaries/colima-Darwin-arm64.sha256sum\n          _output/binaries/colima-Linux-x86_64\n          _output/binaries/colima-Linux-x86_64.sha256sum\n          _output/binaries/colima-Linux-aarch64\n          _output/binaries/colima-Linux-aarch64.sha256sum\n"
  },
  {
    "path": ".github/workflows/golang-ci.yml",
    "content": "name: golangci-lint\non:\n  push:\n    tags: [v*]\n    branches: [main]\n    paths-ignore:\n      - \"**/*.md\"\n      - \"**/*.nix\"\n      - \"**/*.lock\"\n  pull_request:\n    paths-ignore:\n      - \"**/*.md\"\n      - \"**/*.nix\"\n      - \"**/*.lock\"\njobs:\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      - name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: \"1.26.1\"\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0\n        with:\n          version: v2.11.3\n          args: --timeout 3m0s\n"
  },
  {
    "path": ".github/workflows/integration.yml",
    "content": "name: Integration\n\non:\n  push:\n    tags: [\"v*\"]\n    branches: [main]\n    paths-ignore:\n      - \"**/*.md\"\n      - \"**/*.nix\"\n      - \"**/*.lock\"\n  pull_request:\n    branches: [main]\n    paths-ignore:\n      - \"**/*.md\"\n      - \"**/*.nix\"\n      - \"**/*.lock\"\n  workflow_dispatch:\n    inputs:\n      debug_enabled:\n        description: 'Debug with tmate set \"debug_enabled\"'\n        required: false\n        default: \"false\"\n\njobs:\n  kubernetes-docker:\n    runs-on: macos-15-intel\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: \"1.26.1\"\n\n      - name: Install CLI deps\n        run: brew install kubectl docker coreutils lima\n\n      - name: Build and Install\n        run: make && sudo make install\n\n      - name: tmate debugging session\n        uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23\n        with:\n          limit-access-to-actor: true\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n        if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}\n\n      - name: Start Colima\n        run: colima start --runtime docker --kubernetes\n\n      - name: Delay\n        run: sleep 20\n\n      - name: Validate Kubernetes\n        run: kubectl cluster-info && kubectl version && kubectl get nodes -o wide\n\n      - name: Teardown\n        run: colima delete -f\n\n  kubernetes-containerd:\n    runs-on: macos-15-intel\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: \"1.26.1\"\n\n      - name: Install CLI deps\n        run: brew install kubectl docker coreutils lima\n\n      - name: Build and Install\n        run: make && sudo make install\n\n      - name: tmate debugging session\n        uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23\n        with:\n          limit-access-to-actor: true\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n        if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}\n\n      - name: Start\n        run: colima start --runtime containerd --kubernetes\n\n      - name: Delay\n        run: sleep 20\n\n      - name: Validate Kubernetes\n        run: kubectl cluster-info && kubectl version && kubectl get nodes -o wide\n\n      - name: Teardown\n        run: colima delete -f\n\n  docker:\n    runs-on: macos-15-intel\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: \"1.26.1\"\n\n      - name: Install CLI deps\n        run: brew install kubectl docker coreutils lima\n\n      - name: Build and Install\n        run: make && sudo make install\n\n      - name: tmate debugging session\n        uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23\n        with:\n          limit-access-to-actor: true\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n        if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}\n\n      - name: Start Colima\n        run: colima start --runtime docker\n\n      - name: Delay\n        run: sleep 10\n\n      - name: Validate Docker\n        run: docker ps && docker info\n\n      - name: Validate DNS\n        run: colima ssh -- sh -c \"sudo apt-get update -y -qq && sudo apt-get install -qq dnsutils && nslookup host.docker.internal\"\n\n      - name: Build Image\n        run: docker build integration\n\n      - name: Run Image arm64\n        run: docker run --rm --platform=linux/arm64 ghcr.io/linuxcontainers/alpine:latest uname -a\n\n      - name: Run Image amd64\n        run: docker run --rm --platform=linux/amd64 ghcr.io/linuxcontainers/alpine:latest uname -a\n\n      - name: Stop\n        run: colima stop\n\n      - name: Temp Delete\n        run: colima delete -f\n\n      - name: Restart\n        run: colima start --runtime docker\n\n      - name: Assert runtime disk arm64\n        run: docker run --pull=never --rm --platform=linux/arm64 ghcr.io/linuxcontainers/alpine:latest uname -a\n\n      - name: Assert runtime disk amd64\n        run: docker run --pull=never --rm --platform=linux/amd64 ghcr.io/linuxcontainers/alpine:latest uname -a\n\n      - name: Teardown\n        run: colima delete --data -f\n\n  containerd:\n    runs-on: macos-15-intel\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: \"1.26.1\"\n\n      - name: Install CLI deps\n        run: brew install kubectl docker coreutils lima\n\n      - name: Build and Install\n        run: make && sudo make install\n\n      - name: tmate debugging session\n        uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23\n        with:\n          limit-access-to-actor: true\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n        if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}\n\n      - name: Start Colima\n        run: colima start --runtime containerd\n\n      - name: Delay\n        run: sleep 10\n\n      - name: Validate Containerd\n        run: colima nerdctl ps && colima nerdctl info\n\n      - name: Validate DNS\n        run: colima ssh -- sh -c \"sudo apt-get update -y -qq && sudo apt-get install -qq dnsutils && nslookup host.docker.internal\"\n\n      - name: Build Image\n        run: colima nerdctl -- build integration\n\n      - name: Run Image arm64\n        run: colima nerdctl -- run --rm --platform=linux/arm64 ghcr.io/linuxcontainers/alpine:latest uname -a\n\n      - name: Run Image amd64\n        run: colima nerdctl -- run --rm --platform=linux/amd64 ghcr.io/linuxcontainers/alpine:latest uname -a\n\n      - name: Stop\n        run: colima stop\n\n      - name: Temp Delete\n        run: colima delete -f\n\n      - name: Restart\n        run: colima start --runtime containerd\n\n      - name: Assert runtime disk arm64\n        run: colima nerdctl -- run --pull=never --rm --platform=linux/arm64 ghcr.io/linuxcontainers/alpine:latest uname -a\n\n      - name: Assert runtime disk amd64\n        run: colima nerdctl -- run --pull=never --rm --platform=linux/amd64 ghcr.io/linuxcontainers/alpine:latest uname -a\n\n      - name: Teardown\n        run: colima delete --data -f\n\n  incus:\n    runs-on: macos-15-intel\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - name: Set up Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n        with:\n          go-version: \"1.26.1\"\n\n      - name: Install CLI deps\n        run: brew install kubectl docker coreutils lima incus\n\n      - name: Build and Install\n        run: make && sudo make install\n\n      - name: tmate debugging session\n        uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3.23\n        with:\n          limit-access-to-actor: true\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n        if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }}\n\n      - name: Start Colima\n        run: colima start --runtime incus\n\n      - name: Delay\n        run: sleep 10\n\n      - name: Validate Incus\n        run: incus version && incus list\n\n      - name: Launch Instance\n        run: incus launch images:alpine/edge test-instance\n\n      - name: Delay for instance\n        run: sleep 5\n\n      - name: Validate Instance\n        run: incus exec test-instance -- cat /etc/os-release\n\n      - name: Validate DNS\n        run: colima ssh -- sh -c \"sudo apt-get update -y -qq && sudo apt-get install -qq dnsutils && nslookup host.docker.internal\"\n\n      - name: Stop\n        run: colima stop\n\n      - name: Temp Delete\n        run: colima delete -f\n\n      - name: Restart\n        run: colima start --runtime incus\n\n      - name: Delay for restart\n        run: sleep 10\n\n      - name: Assert instance restored\n        run: incus exec test-instance -- cat /etc/os-release\n\n      - name: Teardown\n        run: colima delete --data -f\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\n.fleet/\n.vscode/\n_output/\n_build/\nbin/\nresult\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nlinters:\n  enable:\n    - gocritic\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Abiola Ibrahim\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "\nOS ?= $(shell uname)\nARCH ?= $(shell uname -m)\n\nGOOS ?= $(shell echo \"$(OS)\" | tr '[:upper:]' '[:lower:]')\nGOARCH_x86_64 = amd64\nGOARCH_aarch64 = arm64\nGOARCH_arm64 = arm64\nGOARCH ?= $(shell echo \"$(GOARCH_$(ARCH))\")\n\nVERSION := $(shell git describe --tags --always)\nREVISION := $(shell git rev-parse HEAD)\nPACKAGE := github.com/abiosoft/colima/config\nVERSION_VARIABLES := -X $(PACKAGE).appVersion=$(VERSION) -X $(PACKAGE).revision=$(REVISION)\n\nOUTPUT_DIR := _output/binaries\nOUTPUT_BIN := colima-$(OS)-$(ARCH)\nINSTALL_DIR := /usr/local/bin\nBIN_NAME := colima\n\nLDFLAGS := $(VERSION_VARIABLES)\n\n.PHONY: all\nall: build\n\n.PHONY: clean\nclean:\n\trm -rf _output _build\n\n.PHONY: gopath\ngopath:\n\tgo get -v ./cmd/colima\n\n.PHONY: fmt\nfmt:\n\tgo fmt ./...\n\tgoimports -w .\n\n.PHONY: build\nbuild:\n\tGOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags=\"$(LDFLAGS)\" -o $(OUTPUT_DIR)/$(OUTPUT_BIN) ./cmd/colima\nifeq ($(GOOS),darwin)\n\tcodesign -s - $(OUTPUT_DIR)/$(OUTPUT_BIN)\nendif\n\tcd $(OUTPUT_DIR) && openssl sha256 -r -out $(OUTPUT_BIN).sha256sum $(OUTPUT_BIN)\n\n.PHONY: test\ntest:\n\tgo test -v -ldflags=\"$(LD_FLAGS)\" ./...\n\n.PHONY: vmnet\nvmnet:\n\tsh scripts/build_vmnet.sh\n\n.PHONY: install\ninstall:\n\tmkdir -p $(INSTALL_DIR)\n\trm -f $(INSTALL_DIR)/$(BIN_NAME)\n\tcp $(OUTPUT_DIR)/colima-$(OS)-$(ARCH) $(INSTALL_DIR)/$(BIN_NAME)\n\tchmod +x $(INSTALL_DIR)/$(BIN_NAME)\n\n.PHONY: lint\nlint: ## Assumes that golangci-lint is installed and in the path.  To install: https://golangci-lint.run/usage/install/\n\tgolangci-lint --timeout 3m run\n\n.PHONY: print-binary-name\nprint-binary-name:\n\t@echo $(OUTPUT_DIR)/$(OUTPUT_BIN)\n\n.PHONY: nix-derivation-shell\nnix-derivation-shell:\n\t$(eval DERIVATION=$(shell nix-build))\n\techo $(DERIVATION) | grep ^/nix\n\tnix-shell -p $(DERIVATION)\n\n.PHONY: integration\nintegration: build\n\tGOARCH=$(GOARCH) COLIMA_BINARY=$(OUTPUT_DIR)/$(OUTPUT_BIN) scripts/integration.sh\n\n.PHONY: images-sha\nimages-sha:\n\tbash embedded/images/images_sha.sh\n"
  },
  {
    "path": "README.md",
    "content": "![colima-logo](colima.png)\n\n## Colima - container runtimes on macOS (and Linux) with minimal setup.\n\n[![Go](https://github.com/abiosoft/colima/actions/workflows/go.yml/badge.svg)](https://github.com/abiosoft/colima/actions/workflows/go.yml)\n[![Integration](https://github.com/abiosoft/colima/actions/workflows/integration.yml/badge.svg)](https://github.com/abiosoft/colima/actions/workflows/integration.yml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/abiosoft/colima)](https://goreportcard.com/report/github.com/abiosoft/colima)\n\n![Demonstration](colima.gif)\n\n**Website & Documentation:** [colima.run](https://colima.run) | [colima.run/docs](https://colima.run/docs/)\n\n## Features\n\nSupport for Intel and Apple Silicon macOS, and Linux\n\n- Simple CLI interface with sensible defaults\n- Automatic Port Forwarding\n- Volume mounts\n- Multiple instances\n- Support for multiple container runtimes\n  - [Docker](https://docker.com) (with optional Kubernetes)\n  - [Containerd](https://containerd.io) (with optional Kubernetes)\n  - [Incus](https://linuxcontainers.org/incus) (containers and virtual machines)\n- GPU accelerated containers for AI workloads\n\n## Getting Started\n\n### Installation\n\nColima is available on Homebrew, MacPorts, Nix and [Mise](http://github.com/jdx/mise). Check [here](docs/INSTALL.md) for other installation options.\n\n```sh\n# Homebrew\nbrew install colima\n\n# MacPorts\nsudo port install colima\n\n# Nix\nnix-env -iA nixpkgs.colima\n\n# Mise\nmise use -g colima@latest\n\n```\n\nOr stay on the bleeding edge (only Homebrew)\n\n```\nbrew install --HEAD colima\n```\n\n## Usage\n\nStart Colima with defaults\n\n```\ncolima start\n```\n\nFor more usage options\n\n```\ncolima --help\ncolima start --help\n```\n\nOr use a config file\n\n```\ncolima start --edit\n```\n\n## Runtimes\n\nOn initial startup, Colima initiates with a user specified runtime that defaults to Docker.\n\n### Docker\n\nDocker client is required for Docker runtime. Installable with `brew install docker`.\n\n```\ncolima start\ndocker run hello-world\ndocker ps\n```\n\nYou can use the `docker` client on macOS after `colima start` with no additional setup.\n\n### Containerd\n\n`colima start --runtime containerd` starts and setup Containerd. You can use `colima nerdctl` to interact with\nContainerd using [nerdctl](https://github.com/containerd/nerdctl).\n\n```\ncolima start --runtime containerd\nnerdctl run hello-world\nnerdctl ps\n```\n\nIt is recommended to run `colima nerdctl install` to install `nerdctl` alias script in $PATH.\n\n### Kubernetes\n\nkubectl is required for Kubernetes. Installable with `brew install kubectl`.\n\nTo enable Kubernetes, start Colima with `--kubernetes` flag.\n\n```\ncolima start --kubernetes\nkubectl run caddy --image=caddy\nkubectl get pods\n```\n\n#### Interacting with Image Registry\n\nFor Docker runtime, images built or pulled with Docker are accessible to Kubernetes.\n\nFor Containerd runtime, images built or pulled in the `k8s.io` namespace are accessible to Kubernetes.\n\n### Incus\n\n<small>**Requires v0.7.0**</small>\n\n\nIncus client is required for Incus runtime. Installable with brew `brew install incus`.\n\n`colima start --runtime incus` starts and setup Incus.\n\n```\ncolima start --runtime incus\nincus launch images:alpine/edge\nincus list\n```\n\nYou can use the `incus` client on macOS after `colima start` with no additional setup.\n\n**Note:** Running virtual machines on Incus is only supported on m3 or newer Apple Silicon devices.\n\n### AI Models (GPU Accelerated)\n\n<small>**Requires v0.10.0, Apple Silicon and macOS 13+**</small>\n\nColima supports GPU accelerated containers for AI workloads using the `krunkit` VM type.\n\n**Note:** To use krunkit with colima, ensure it is installed.\n\n```\nbrew tap slp/krunkit\nbrew install krunkit\n```\n\nSetup and use a model.\n\n```\ncolima start --runtime docker --vm-type krunkit\ncolima model run gemma3\n```\n\nColima supports two model runner backends:\n\n- **Docker Model Runner** (default) — supports [Docker AI Registry](https://hub.docker.com/u/ai) and [HuggingFace](https://huggingface.co).\n- **Ramalama** — supports [HuggingFace](https://huggingface.co) and [Ollama](https://ollama.com) registries.\n\nThe default registry is the Docker AI Registry. Models can be run by name without a prefix:\n\n```sh\ncolima model run gemma3\ncolima model run llama3.2\n# HuggingFace (Docker Model Runner)\ncolima model run hf.co/microsoft/Phi-3-mini-4k-instruct-gguf\n# Ollama (requires ramalama runner)\ncolima model run ollama://gemma3 --runner ramalama\n```\n\nSee the [AI Workloads documentation](https://colima.run/docs/ai/) for more details.\n\n### Customizing the VM\n\nThe default VM created by Colima has 2 CPUs, 2GiB memory and 100GiB storage.\n\nThe VM can be customized either by passing additional flags to `colima start`.\ne.g. `--cpu`, `--memory`, `--disk`, `--runtime`.\nOr by editing the config file with `colima start --edit`.\n\n**NOTE**: Disk size can be increased after the VM is created.\n\n#### Customization Examples\n\n- create VM with 1CPU, 2GiB memory and 10GiB storage.\n\n  ```\n  colima start --cpu 1 --memory 2 --disk 10\n  ```\n\n- modify an existing VM to 4CPUs and 8GiB memory.\n\n  ```\n  colima stop\n  colima start --cpu 4 --memory 8\n  ```\n\n- create VM with Rosetta 2 emulation. Requires v0.5.3 and macOS >= 13 (Ventura) on Apple Silicon.\n\n  ```\n  colima start --vm-type=vz --vz-rosetta\n  ```\n\n## Project Goal\n\nTo provide container runtimes on macOS with minimal setup.\n\n## What is with the name?\n\nColima means Containers on [Lima](https://github.com/lima-vm/lima).\n\nSince Lima is aka Linux Machines. By transitivity, Colima can also mean Containers on Linux Machines.\n\n## And the Logo?\n\nThe logo was contributed by [Daniel Hodvogner](https://github.com/dhodvogner). Check [this issue](https://github.com/abiosoft/colima/issues/781) for more.\n\n## Troubleshooting and FAQs\n\nCheck [here](docs/FAQ.md) for Frequently Asked Questions, or visit the [online FAQ](https://colima.run/docs/faq/) for a searchable version.\n\n## How to Contribute?\n\nCheck [here](docs/CONTRIBUTE.md) for the instructions on contributing to the project.\n\n## Community\n- [GitHub Discussions](https://github.com/abiosoft/colima/discussions)\n- [GitHub Issues](https://github.com/abiosoft/colima/issues)\n- [Announcements](https://colima.run/announcements/)\n- `#colima` channel in the CNCF Slack\n  - New account: <https://slack.cncf.io/>\n  - Login: <https://cloud-native.slack.com/>\n\n## License\n\nMIT\n\n\n## Sponsoring the Project\n\nIf you (or your company) are benefiting from the project and would like to support the contributors, kindly sponsor.\n\n- [Github Sponsors](https://github.com/sponsors/abiosoft)\n- [Buy me a coffee](https://www.buymeacoffee.com/abiosoft)\n- [Patreon](https://patreon.com/colima)\n\n---\n\n[<img src=\"https://uploads-ssl.webflow.com/5ac3c046c82724970fc60918/5c019d917bba312af7553b49_MacStadium-developerlogo.png\" style=\"max-height: 150px\"/>](https://macstadium.com)\n\n\n"
  },
  {
    "path": "SECURITY.md",
    "content": "Thanks for helping make Colima safe for everyone.\n\n## Security\n\nWe take the security of Colima seriously.\n\nWe will ensure that your finding gets passed along to the appropriate maintainers for remediation. \n\n## Reporting Security Issues\n\nIf you believe you have found a security vulnerability in this repository, please report it to us through coordinated disclosure.\n\n**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**\n\nInstead, please send an email to git[@]abiosoft.com.\n\nPlease include as much of the information listed below as you can to help us better understand and resolve the issue:\n\n  * The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting)\n  * Full paths of source file(s) related to the manifestation of the issue\n  * The location of the affected source code (tag/branch/commit or direct URL)\n  * Any special configuration required to reproduce the issue\n  * Step-by-step instructions to reproduce the issue\n  * Proof-of-concept or exploit code (if possible)\n  * Impact of the issue, including how an attacker might exploit the issue\n\nThis information will help us triage your report more quickly.\n"
  },
  {
    "path": "app/app.go",
    "content": "package app\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/config/configmanager\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/environment/container/containerd\"\n\t\"github.com/abiosoft/colima/environment/container/docker\"\n\t\"github.com/abiosoft/colima/environment/container/incus\"\n\t\"github.com/abiosoft/colima/environment/container/kubernetes\"\n\t\"github.com/abiosoft/colima/environment/host\"\n\t\"github.com/abiosoft/colima/environment/vm/lima\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n\t\"github.com/abiosoft/colima/store\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/docker/go-units\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\ntype App interface {\n\tActive() bool\n\tStart(config.Config) error\n\tStop(force bool) error\n\tDelete(data, force bool) error\n\tSSH(args ...string) error\n\tStatus(extended bool, json bool) error\n\tVersion() error\n\tRuntime() (string, error)\n\tUpdate() error\n\tKubernetes() (environment.Container, error)\n}\n\nvar _ App = (*colimaApp)(nil)\n\n// New creates a new app.\nfunc New() (App, error) {\n\tguest := lima.New(host.New())\n\tif err := host.IsInstalled(guest); err != nil {\n\t\treturn nil, fmt.Errorf(\"dependency check failed for VM: %w\", err)\n\t}\n\n\treturn &colimaApp{\n\t\tguest: guest,\n\t}, nil\n}\n\ntype colimaApp struct {\n\tguest environment.VM\n}\n\nfunc (c colimaApp) startWithRuntime(conf config.Config) ([]environment.Container, error) {\n\tkubernetesEnabled := conf.Kubernetes.Enabled\n\n\t// Kubernetes can only be enabled for docker and containerd\n\tswitch conf.Runtime {\n\tcase docker.Name, containerd.Name:\n\tdefault:\n\t\tkubernetesEnabled = false\n\t}\n\n\tvar containers []environment.Container\n\n\t{\n\t\truntime := conf.Runtime\n\t\tif kubernetesEnabled {\n\t\t\truntime += \"+k3s\"\n\t\t}\n\t\tlog.Println(\"runtime:\", runtime)\n\t}\n\n\t// runtime\n\t{\n\t\tenv, err := c.containerEnvironment(conf.Runtime)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcontainers = append(containers, env)\n\t}\n\n\t// kubernetes should come after required runtime\n\tif kubernetesEnabled {\n\t\tenv, err := c.containerEnvironment(kubernetes.Name)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcontainers = append(containers, env)\n\t}\n\n\treturn containers, nil\n}\n\nfunc (c colimaApp) Start(conf config.Config) error {\n\tctx := context.WithValue(context.Background(), config.CtxKey(), conf)\n\n\tlog.Println(\"starting\", config.CurrentProfile().DisplayName)\n\t// print the full path of current profile being used\n\tlog.Tracef(\"starting with config file: %s\\n\", config.CurrentProfile().File())\n\n\tvar containers []environment.Container\n\tif !environment.IsNoneRuntime(conf.Runtime) {\n\t\tcs, err := c.startWithRuntime(conf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcontainers = cs\n\t}\n\n\t// the order for start is:\n\t//   vm start -> container runtime provision -> container runtime start\n\n\t// start vm\n\tif err := c.guest.Start(ctx, conf); err != nil {\n\t\treturn fmt.Errorf(\"error starting vm: %w\", err)\n\t}\n\n\t// run after-boot provision scripts\n\tc.runProvisionScripts(conf, config.ProvisionModeAfterBoot)\n\n\t// provision and start container runtimes\n\tfor _, cont := range containers {\n\t\tlog := log.WithField(\"context\", cont.Name())\n\t\tlog.Println(\"provisioning ...\")\n\t\tif err := cont.Provision(ctx); err != nil {\n\t\t\treturn fmt.Errorf(\"error provisioning %s: %w\", cont.Name(), err)\n\t\t}\n\t\tlog.Println(\"starting ...\")\n\t\tif err := cont.Start(ctx); err != nil {\n\t\t\treturn fmt.Errorf(\"error starting %s: %w\", cont.Name(), err)\n\t\t}\n\t}\n\n\t// run ready provision scripts\n\tc.runProvisionScripts(conf, config.ProvisionModeReady)\n\n\t// persist the current runtime\n\tif err := c.setRuntime(conf.Runtime); err != nil {\n\t\tlog.Error(fmt.Errorf(\"error persisting runtime settings: %w\", err))\n\t}\n\n\t// persist the kubernetes config\n\tif err := c.setKubernetes(conf.Kubernetes); err != nil {\n\t\tlog.Error(fmt.Errorf(\"error persisting kubernetes settings: %w\", err))\n\t}\n\n\tlog.Println(\"done\")\n\n\tif err := generateSSHConfig(conf.SSHConfig); err != nil {\n\t\tlog.Trace(\"error generating ssh_config: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (c colimaApp) runProvisionScripts(conf config.Config, mode string) {\n\tvar failed bool\n\tfor _, s := range conf.Provision {\n\t\tif s.Mode != mode {\n\t\t\tcontinue\n\t\t}\n\t\tif err := c.guest.Run(\"sh\", \"-c\", s.Script); err != nil {\n\t\t\tfailed = true\n\t\t}\n\t}\n\tif failed {\n\t\tlog.Warnln(fmt.Errorf(\"error running %s provision script(s)\", mode))\n\t}\n}\n\nfunc (c colimaApp) Stop(force bool) error {\n\tctx := context.Background()\n\tlog.Println(\"stopping\", config.CurrentProfile().DisplayName)\n\n\t// the order for stop is:\n\t//   container stop -> vm stop\n\n\t// stop container runtimes\n\tif c.guest.Running(ctx) {\n\t\tcontainers, err := c.currentContainerEnvironments(ctx)\n\t\tif err != nil {\n\t\t\tlog.Warnln(fmt.Errorf(\"error retrieving runtimes: %w\", err))\n\t\t}\n\n\t\t// stop happens in reverse of start\n\t\tfor i := len(containers) - 1; i >= 0; i-- {\n\t\t\tcont := containers[i]\n\n\t\t\tlog := log.WithField(\"context\", cont.Name())\n\t\t\tlog.Println(\"stopping ...\")\n\n\t\t\tif err := cont.Stop(ctx, force); err != nil {\n\t\t\t\t// failure to stop a container runtime is not fatal\n\t\t\t\t// it is only meant for graceful shutdown.\n\t\t\t\t// the VM will shut down anyways.\n\t\t\t\tlog.Warnln(fmt.Errorf(\"error stopping %s: %w\", cont.Name(), err))\n\t\t\t}\n\t\t}\n\t}\n\n\t// stop vm\n\t// no need to check running status, it may be in a state that requires stopping.\n\tif err := c.guest.Stop(ctx, force); err != nil {\n\t\treturn fmt.Errorf(\"error stopping vm: %w\", err)\n\t}\n\n\tlog.Println(\"done\")\n\n\tif err := generateSSHConfig(false); err != nil {\n\t\tlog.Trace(\"error generating ssh_config: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (c colimaApp) Delete(data, force bool) error {\n\tconfirmContainerDestruction := func() bool {\n\t\treturn cli.Prompt(\"\\033[31m\\033[1mthis will delete ALL container data. Are you sure you want to continue\")\n\t}\n\n\ts, _ := store.Load()\n\tdiskInUse := s.DiskFormatted\n\n\tif !force {\n\t\ty := cli.Prompt(\"are you sure you want to delete \" + config.CurrentProfile().DisplayName + \" and all settings\")\n\t\tif !y {\n\t\t\treturn nil\n\t\t}\n\n\t\t// runtime disk not in use or data deletion is requested,\n\t\t// deletion deletes all data, warn accordingly.\n\t\tif !diskInUse || data {\n\t\t\tif y := confirmContainerDestruction(); !y {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\n\tctx := context.Background()\n\tlog.Println(\"deleting\", config.CurrentProfile().DisplayName)\n\n\t// the order for teardown is:\n\t//   container teardown -> vm teardown\n\n\t// vm teardown would've sufficed but container provision\n\t// may have created configurations on the host.\n\t// it is thereby necessary to teardown containers as well.\n\n\t// teardown container runtimes\n\tif c.guest.Running(ctx) {\n\t\tcontainers, err := c.currentContainerEnvironments(ctx)\n\t\tif err != nil {\n\t\t\tlog.Warnln(fmt.Errorf(\"error retrieving runtimes: %w\", err))\n\t\t}\n\t\tfor _, cont := range containers {\n\t\t\tlog := log.WithField(\"context\", cont.Name())\n\t\t\tlog.Println(\"deleting ...\")\n\n\t\t\tif err := cont.Teardown(ctx); err != nil {\n\t\t\t\t// failure here is not fatal\n\t\t\t\tlog.Warnln(fmt.Errorf(\"error during teardown of %s: %w\", cont.Name(), err))\n\t\t\t}\n\t\t}\n\t}\n\n\t// teardown vm\n\tif err := c.guest.Teardown(ctx); err != nil {\n\t\treturn fmt.Errorf(\"error during teardown of vm: %w\", err)\n\t}\n\n\t// delete configs\n\tif err := configmanager.Teardown(); err != nil {\n\t\treturn fmt.Errorf(\"error deleting configs: %w\", err)\n\t}\n\n\t// delete runtime disk if disk in use and data deletion is requested\n\tif diskInUse && data {\n\t\tlog.Println(\"deleting container data\")\n\t\tif err := limautil.DeleteDisk(); err != nil {\n\t\t\treturn fmt.Errorf(\"error deleting container data: %w\", err)\n\t\t}\n\n\t\tif err := store.Reset(); err != nil {\n\t\t\tlog.Trace(\"error resetting store: %w\", err)\n\t\t}\n\t}\n\n\tlog.Println(\"done\")\n\n\tif err := generateSSHConfig(false); err != nil {\n\t\tlog.Trace(\"error generating ssh_config: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (c colimaApp) SSH(args ...string) error {\n\tctx := context.Background()\n\tif !c.guest.Running(ctx) {\n\t\treturn fmt.Errorf(\"%s not running\", config.CurrentProfile().DisplayName)\n\t}\n\n\tworkDir, err := os.Getwd()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error retrieving current working directory: %w\", err)\n\t}\n\t// peek the current directory to see if it is mounted to prevent `cd` errors\n\t// with limactl ssh\n\tif err := func() error {\n\t\tconf, err := configmanager.LoadInstance()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tpwd, err := util.CleanPath(workDir)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor _, m := range conf.MountsOrDefault() {\n\t\t\tlocation := m.MountPoint\n\t\t\tif location == \"\" {\n\t\t\t\tlocation = m.Location\n\t\t\t}\n\t\t\tlocation, err := util.CleanPath(location)\n\t\t\tif err != nil {\n\t\t\t\tlog.Trace(err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif strings.HasPrefix(pwd, location) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\treturn fmt.Errorf(\"not a mounted directory: %s\", workDir)\n\t}(); err != nil {\n\t\t// the errors returned here is not critical and thereby silenced.\n\t\t// the goal is to prevent unnecessary warning message from Lima.\n\t\tlog.Trace(fmt.Errorf(\"error checking if PWD is mounted: %w\", err))\n\t\tworkDir = \"\"\n\t}\n\n\tguest := lima.New(host.New())\n\treturn guest.SSH(workDir, args...)\n}\n\ntype statusInfo struct {\n\tDisplayName      string `json:\"display_name\"`\n\tDriver           string `json:\"driver\"`\n\tArch             string `json:\"arch\"`\n\tRuntime          string `json:\"runtime\"`\n\tMountType        string `json:\"mount_type\"`\n\tIPAddress        string `json:\"ip_address,omitempty\"`\n\tDockerSocket     string `json:\"docker_socket,omitempty\"`\n\tContainerdSocket string `json:\"containerd_socket,omitempty\"`\n\tBuildkitdSocket  string `json:\"buildkitd_socket,omitempty\"`\n\tIncusSocket      string `json:\"incus_socket,omitempty\"`\n\tKubernetes       bool   `json:\"kubernetes\"`\n\tCPU              int    `json:\"cpu\"`\n\tMemory           int64  `json:\"memory\"`\n\tDisk             int64  `json:\"disk\"`\n}\n\nfunc (c colimaApp) getStatus() (status statusInfo, err error) {\n\tctx := context.Background()\n\tif !c.guest.Running(ctx) {\n\t\treturn status, fmt.Errorf(\"%s is not running\", config.CurrentProfile().DisplayName)\n\t}\n\n\tcurrentRuntime, err := c.currentRuntime(ctx)\n\tif err != nil {\n\t\treturn status, err\n\t}\n\n\tstatus.DisplayName = config.CurrentProfile().DisplayName\n\tstatus.Driver = \"QEMU\"\n\tconf, _ := configmanager.LoadInstance()\n\tif !conf.Empty() {\n\t\tstatus.Driver = conf.DriverLabel()\n\t}\n\tstatus.Arch = string(c.guest.Arch())\n\tstatus.Runtime = currentRuntime\n\tstatus.MountType = conf.MountType\n\tipAddress := limautil.IPAddress(config.CurrentProfile().ID)\n\tif ipAddress != \"127.0.0.1\" {\n\t\tstatus.IPAddress = ipAddress\n\t}\n\tif currentRuntime == docker.Name {\n\t\tstatus.DockerSocket = \"unix://\" + docker.HostSocketFile()\n\t\tstatus.ContainerdSocket = \"unix://\" + containerd.HostSocketFiles().Containerd\n\t}\n\tif currentRuntime == containerd.Name {\n\t\tstatus.ContainerdSocket = \"unix://\" + containerd.HostSocketFiles().Containerd\n\t\tstatus.BuildkitdSocket = \"unix://\" + containerd.HostSocketFiles().Buildkitd\n\t}\n\tif currentRuntime == incus.Name {\n\t\tstatus.IncusSocket = \"unix://\" + incus.HostSocketFile()\n\t}\n\tif k, err := c.Kubernetes(); err == nil && k.Running(ctx) {\n\t\tstatus.Kubernetes = true\n\t}\n\tif inst, err := limautil.Instance(); err == nil {\n\t\tstatus.CPU = inst.CPU\n\t\tstatus.Memory = inst.Memory\n\t\tstatus.Disk = inst.Disk\n\t}\n\treturn status, nil\n}\n\nfunc (c colimaApp) Status(extended bool, jsonOutput bool) error {\n\tstatus, err := c.getStatus()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif jsonOutput {\n\t\tif err := json.NewEncoder(os.Stdout).Encode(status); err != nil {\n\t\t\treturn fmt.Errorf(\"error encoding status as json: %w\", err)\n\t\t}\n\t} else {\n\t\tlog.Println(config.CurrentProfile().DisplayName, \"is running using\", status.Driver)\n\t\tlog.Println(\"arch:\", status.Arch)\n\t\tlog.Println(\"runtime:\", status.Runtime)\n\t\tif status.MountType != \"\" {\n\t\t\tlog.Println(\"mountType:\", status.MountType)\n\t\t}\n\n\t\t// ip address\n\t\tif status.IPAddress != \"\" {\n\t\t\tlog.Println(\"address:\", status.IPAddress)\n\t\t}\n\n\t\t// docker socket\n\t\tif status.DockerSocket != \"\" {\n\t\t\tlog.Println(\"docker socket:\", status.DockerSocket)\n\t\t}\n\t\tif status.ContainerdSocket != \"\" {\n\t\t\tlog.Println(\"containerd socket:\", status.ContainerdSocket)\n\t\t}\n\t\tif status.BuildkitdSocket != \"\" {\n\t\t\tlog.Println(\"buildkitd socket:\", status.BuildkitdSocket)\n\t\t}\n\t\tif status.IncusSocket != \"\" {\n\t\t\tlog.Println(\"incus socket:\", status.IncusSocket)\n\t\t}\n\n\t\t// kubernetes\n\t\tif status.Kubernetes {\n\t\t\tlog.Println(\"kubernetes: enabled\")\n\t\t}\n\n\t\t// additional details\n\t\tif extended {\n\t\t\tif status.CPU > 0 {\n\t\t\t\tlog.Println(\"cpu:\", status.CPU)\n\t\t\t}\n\t\t\tif status.Memory > 0 {\n\t\t\t\tlog.Println(\"mem:\", units.BytesSize(float64(status.Memory)))\n\t\t\t}\n\t\t\tif status.Disk > 0 {\n\t\t\t\tlog.Println(\"disk:\", units.BytesSize(float64(status.Disk)))\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c colimaApp) Version() error {\n\tctx := context.Background()\n\tif !c.guest.Running(ctx) {\n\t\treturn nil\n\t}\n\n\tcontainerRuntimes, err := c.currentContainerEnvironments(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar kube environment.Container\n\tfor _, cont := range containerRuntimes {\n\t\tif cont.Name() == kubernetes.Name {\n\t\t\tkube = cont\n\t\t\tcontinue\n\t\t}\n\n\t\tfmt.Println()\n\t\tfmt.Println(\"runtime:\", cont.Name())\n\t\tfmt.Println(\"arch:\", c.guest.Arch())\n\t\tfmt.Println(cont.Version(ctx))\n\t}\n\n\tif kube != nil && kube.Version(ctx) != \"\" {\n\t\tfmt.Println()\n\t\tfmt.Println(kubernetes.Name)\n\t\tfmt.Println(kube.Version(ctx))\n\t}\n\n\treturn nil\n}\n\nfunc (c colimaApp) currentRuntime(ctx context.Context) (string, error) {\n\tif !c.guest.Running(ctx) {\n\t\treturn \"\", fmt.Errorf(\"%s is not running\", config.CurrentProfile().DisplayName)\n\t}\n\n\tr := c.guest.Get(environment.ContainerRuntimeKey)\n\tif r == \"\" {\n\t\treturn \"\", fmt.Errorf(\"error retrieving current runtime: empty value\")\n\t}\n\n\treturn r, nil\n}\n\nfunc (c colimaApp) setRuntime(runtime string) error {\n\terr := store.Set(func(s *store.Store) {\n\t\t// update runtime if runtime disk is in use\n\t\tif s.DiskFormatted {\n\t\t\ts.DiskRuntime = runtime\n\t\t}\n\t})\n\n\tif err != nil {\n\t\tlog.Traceln(fmt.Errorf(\"error persisting store: %w\", err))\n\t}\n\n\treturn c.guest.Set(environment.ContainerRuntimeKey, runtime)\n}\n\nfunc (c colimaApp) setKubernetes(conf config.Kubernetes) error {\n\tb, err := json.Marshal(conf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.guest.Set(kubernetes.ConfigKey, string(b))\n}\n\nfunc (c colimaApp) currentContainerEnvironments(ctx context.Context) ([]environment.Container, error) {\n\tvar containers []environment.Container\n\t// runtime\n\t{\n\t\truntime, err := c.currentRuntime(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif environment.IsNoneRuntime(runtime) {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tenv, err := c.containerEnvironment(runtime)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcontainers = append(containers, env)\n\t}\n\n\t// detect and add kubernetes\n\tif k, err := c.containerEnvironment(kubernetes.Name); err == nil && k.Running(ctx) {\n\t\tcontainers = append(containers, k)\n\t}\n\n\treturn containers, nil\n}\n\nfunc (c colimaApp) containerEnvironment(runtime string) (environment.Container, error) {\n\tenv, err := environment.NewContainer(runtime, c.guest.Host(), c.guest)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error initiating container runtime: %w\", err)\n\t}\n\tif err := host.IsInstalled(env); err != nil {\n\t\treturn nil, fmt.Errorf(\"dependency check failed for %s: %w\", runtime, err)\n\t}\n\n\treturn env, nil\n}\n\nfunc (c colimaApp) Runtime() (string, error) {\n\treturn c.currentRuntime(context.Background())\n}\n\nfunc (c colimaApp) Kubernetes() (environment.Container, error) {\n\treturn c.containerEnvironment(kubernetes.Name)\n}\n\nfunc (c colimaApp) Active() bool {\n\treturn c.guest.Running(context.Background())\n}\n\nfunc (c *colimaApp) Update() error {\n\tctx := context.Background()\n\tif !c.guest.Running(ctx) {\n\t\treturn fmt.Errorf(\"runtime cannot be updated, %s is not running\", config.CurrentProfile().DisplayName)\n\t}\n\n\truntime, err := c.currentRuntime(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcontainer, err := c.containerEnvironment(runtime)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\toldVersion := container.Version(ctx)\n\n\tupdated, err := container.Update(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif updated {\n\t\tfmt.Println()\n\t\tfmt.Println(\"Previous\")\n\t\tfmt.Println(oldVersion)\n\t\tfmt.Println()\n\t\tfmt.Println(\"Current\")\n\t\tfmt.Println(container.Version(ctx))\n\t}\n\n\treturn nil\n}\n\nfunc generateSSHConfig(modifySSHConfig bool) error {\n\tinstances, err := limautil.Instances()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error retrieving instances: %w\", err)\n\t}\n\tvar buf bytes.Buffer\n\n\tfor _, i := range instances {\n\t\tif !i.Running() {\n\t\t\tcontinue\n\t\t}\n\n\t\tprofile := config.ProfileFromName(i.Name)\n\t\tresp, err := limautil.ShowSSH(profile.ID)\n\t\tif err != nil {\n\t\t\tlog.Trace(fmt.Errorf(\"error retrieving SSH config for '%s': %w\", i.Name, err))\n\t\t\tcontinue\n\t\t}\n\n\t\tfmt.Fprintln(&buf, resp.Output)\n\t}\n\n\tsshFileColima := config.SSHConfigFile()\n\tif err := os.WriteFile(sshFileColima, buf.Bytes(), 0644); err != nil {\n\t\treturn fmt.Errorf(\"error writing ssh_config file: %w\", err)\n\t}\n\n\tif !modifySSHConfig {\n\t\t// ~/.ssh/config modification disabled\n\t\treturn nil\n\t}\n\n\tincludeLine := \"Include \" + sshFileColima\n\n\tsshFileSystem := filepath.Join(util.HomeDir(), \".ssh\", \"config\")\n\n\t// include the SSH config file if not included\n\t// if ssh file missing, the only content will be the include\n\tif _, err := os.Stat(sshFileSystem); err != nil {\n\t\tif err := os.MkdirAll(filepath.Dir(sshFileSystem), 0700); err != nil {\n\t\t\treturn fmt.Errorf(\"error creating ssh directory: %w\", err)\n\t\t}\n\n\t\tif err := os.WriteFile(sshFileSystem, []byte(includeLine), 0644); err != nil {\n\t\t\treturn fmt.Errorf(\"error modifying %s: %w\", sshFileSystem, err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tsshContent, err := os.ReadFile(sshFileSystem)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading ssh config: %w\", err)\n\t}\n\n\tscanner := bufio.NewScanner(bytes.NewReader(sshContent))\n\tfor scanner.Scan() {\n\t\twords := strings.Fields(scanner.Text())\n\n\t\t// empty line\n\t\tif len(words) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\t// comment\n\t\tif strings.HasPrefix(words[0], \"#\") {\n\t\t\tcontinue\n\t\t}\n\n\t\t// not an include line\n\t\tif len(words) < 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif words[0] == \"Include\" {\n\t\t\tsshConfig := words[1]\n\t\t\tsshConfig = strings.Replace(sshConfig, \"~/\", \"$HOME/\", 1)\n\t\t\tsshConfig = os.ExpandEnv(sshConfig)\n\t\t\tif sshConfig == sshFileColima {\n\t\t\t\t// already present\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n\n\t// not found, prepend file\n\tif err := os.WriteFile(sshFileSystem, []byte(includeLine+\"\\n\\n\"+string(sshContent)), 0644); err != nil {\n\t\treturn fmt.Errorf(\"error modifying %s: %w\", sshFileSystem, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cli/chain.go",
    "content": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"time\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// CtxKeyQuiet is the context key to mute the chain.\nvar CtxKeyQuiet = struct{ key string }{key: \"quiet\"}\n\n// errNonFatal is a non fatal error\ntype errNonFatal struct {\n\terr error\n}\n\n// Error implements error\nfunc (e errNonFatal) Error() string { return e.err.Error() }\n\n// ErrNonFatal creates a non-fatal error for a command chain.\n// A warning would be printed instead of terminating the chain.\nfunc ErrNonFatal(err error) error {\n\treturn errNonFatal{err}\n}\n\n// New creates a new runner instance.\nfunc New(name string) CommandChain {\n\treturn &namedCommandChain{\n\t\tname: name,\n\t}\n}\n\ntype cFunc struct {\n\tf func() error\n\ts string\n}\n\n// CommandChain is a chain of commands.\n// commands are executed in order.\ntype CommandChain interface {\n\t// Init initiates a new runner using the current instance.\n\tInit(ctx context.Context) *ActiveCommandChain\n\t// Logger returns the instance logger.\n\tLogger(ctx context.Context) *log.Entry\n}\n\nvar _ CommandChain = (*namedCommandChain)(nil)\n\ntype namedCommandChain struct {\n\tname string\n\tlog  *log.Entry\n}\n\nfunc (n *namedCommandChain) Logger(ctx context.Context) *log.Entry {\n\tif quiet, _ := ctx.Value(CtxKeyQuiet).(bool); quiet {\n\t\tl := log.New()\n\t\tl.SetOutput(io.Discard)\n\t\treturn l.WithContext(ctx)\n\t}\n\tif n.log == nil {\n\t\tn.log = log.WithField(\"context\", n.name).WithContext(ctx)\n\t}\n\treturn n.log\n}\n\nfunc (n *namedCommandChain) Init(ctx context.Context) *ActiveCommandChain {\n\treturn &ActiveCommandChain{\n\t\tlog: n.Logger(ctx),\n\t}\n}\n\n// ActiveCommandChain is an active command chain.\ntype ActiveCommandChain struct {\n\tfuncs     []cFunc\n\tlastStage string\n\tlog       *log.Entry\n\n\texecuting bool\n}\n\n// Logger returns the logger for the command chain.\nfunc (a *ActiveCommandChain) Logger() *log.Entry { return a.log }\n\n// Add adds a new function to the runner.\nfunc (a *ActiveCommandChain) Add(f func() error) {\n\ta.funcs = append(a.funcs, cFunc{f: f})\n}\n\n// Stage sets the current stage of the runner.\nfunc (a *ActiveCommandChain) Stage(s string) {\n\tif a.executing {\n\t\ta.log.Println(s, \"...\")\n\t\treturn\n\t}\n\ta.funcs = append(a.funcs, cFunc{s: s})\n}\n\n// Stagef is like stage with string format.\nfunc (a *ActiveCommandChain) Stagef(format string, s ...any) {\n\tf := fmt.Sprintf(format, s...)\n\ta.Stage(f)\n}\n\n// Exec executes the command chain.\n// The first errored function terminates the chain and the\n// error is returned. Otherwise, returns nil.\nfunc (a *ActiveCommandChain) Exec() error {\n\ta.executing = true\n\tdefer func() { a.executing = false }()\n\n\tfor _, f := range a.funcs {\n\t\tif f.f == nil {\n\t\t\tif f.s != \"\" {\n\t\t\t\ta.log.Println(f.s, \"...\")\n\t\t\t\ta.lastStage = f.s\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// success\n\t\terr := f.f()\n\t\tif err == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// warning\n\t\tif _, ok := err.(errNonFatal); ok {\n\t\t\tif a.lastStage == \"\" {\n\t\t\t\ta.log.Warnln(err)\n\t\t\t} else {\n\t\t\t\ta.log.Warnln(fmt.Errorf(\"error at '%s': %w\", a.lastStage, err))\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// error\n\t\tif a.lastStage == \"\" {\n\t\t\treturn err\n\t\t}\n\t\treturn fmt.Errorf(\"error at '%s': %w\", a.lastStage, err)\n\t}\n\treturn nil\n}\n\n// Retry retries `f` up to `count` times at interval.\n// If after `count` attempts there is an error, the command chain is terminated with the final error.\n// retryCount starts from 1.\nfunc (a *ActiveCommandChain) Retry(stage string, interval time.Duration, count int, f func(retryCount int) error) {\n\ta.Add(func() (err error) {\n\t\tvar i int\n\t\tfor err = f(i + 1); i < count && err != nil; i, err = i+1, f(i+1) {\n\t\t\tif stage != \"\" {\n\t\t\t\ta.log.Println(stage, \"...\")\n\t\t\t}\n\t\t\ttime.Sleep(interval)\n\t\t}\n\t\treturn err\n\t})\n}\n"
  },
  {
    "path": "cli/command.go",
    "content": "package cli\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar runner commandRunner = &defaultCommandRunner{}\n\n// Settings is global cli settings\nvar Settings = struct {\n\t// Verbose toggles verbose output for commands.\n\tVerbose bool\n}{}\n\n// Command creates a new command.\nfunc Command(command string, args ...string) *exec.Cmd { return runner.Command(command, args...) }\n\n// CommandInteractive creates a new interactive command.\nfunc CommandInteractive(command string, args ...string) *exec.Cmd {\n\treturn runner.CommandInteractive(command, args...)\n}\n\ntype commandRunner interface {\n\tCommand(command string, args ...string) *exec.Cmd\n\tCommandInteractive(command string, args ...string) *exec.Cmd\n}\n\nvar _ commandRunner = (*defaultCommandRunner)(nil)\n\ntype defaultCommandRunner struct{}\n\nfunc (d defaultCommandRunner) Command(command string, args ...string) *exec.Cmd {\n\tcmd := exec.Command(command, args...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tlog.Trace(\"cmd \", quotedArgs(cmd.Args))\n\n\treturn cmd\n}\n\nfunc (d defaultCommandRunner) CommandInteractive(command string, args ...string) *exec.Cmd {\n\tcmd := exec.Command(command, args...)\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tlog.Trace(\"cmd int \", quotedArgs(cmd.Args))\n\n\treturn cmd\n}\n\nfunc quotedArgs(args []string) string {\n\tvar q []string\n\tfor _, s := range args {\n\t\tq = append(q, strconv.Quote(s))\n\t}\n\treturn fmt.Sprintf(\"%v\", q)\n}\n\n// Prompt prompts for input with a question. It returns true only if answer is y or Y.\nfunc Prompt(question string) bool {\n\tfmt.Print(question)\n\tfmt.Print(\"? [y/N] \")\n\tfmt.Print(\"\\033[0m\") // reset all formatting modes (if any) used by the question string\n\n\tvar answer string\n\t_, _ = fmt.Scanln(&answer)\n\n\tif answer == \"\" {\n\t\treturn false\n\t}\n\n\treturn answer[0] == 'Y' || answer[0] == 'y'\n}\n"
  },
  {
    "path": "cmd/clone.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\n// stopCmd represents the stop command\nvar cloneCmd = &cobra.Command{\n\tUse:   \"clone <profile> <new-profile>\",\n\tShort: \"clone Colima profile\",\n\tLong:  `Clone the Colima profile.`,\n\tArgs:  cobra.ExactArgs(2),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tfrom := config.ProfileFromName(args[0])\n\t\tto := config.ProfileFromName(args[1])\n\n\t\tlogrus.Infof(\"preparing to clone %s...\", from.DisplayName)\n\t\t{\n\t\t\t// verify source profile exists\n\t\t\tif stat, err := os.Stat(from.LimaInstanceDir()); err != nil || !stat.IsDir() {\n\t\t\t\treturn fmt.Errorf(\"colima profile '%s' does not exist\", from.ShortName)\n\t\t\t}\n\n\t\t\t// verify destination profile does not exists\n\t\t\tif stat, err := os.Stat(to.LimaInstanceDir()); err == nil && stat.IsDir() {\n\t\t\t\treturn fmt.Errorf(\"colima profile '%s' already exists, delete with `colima delete %s` and try again\", to.ShortName, to.ShortName)\n\t\t\t}\n\n\t\t\t// copy source to destination\n\t\t\tlogrus.Info(\"cloning virtual machine...\")\n\t\t\tif err := cli.Command(\"mkdir\", \"-p\", to.LimaInstanceDir()).Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error preparing to copy VM: %w\", err)\n\t\t\t}\n\n\t\t\tif err := cli.Command(\"cp\",\n\t\t\t\tfilepath.Join(from.LimaInstanceDir(), \"basedisk\"),\n\t\t\t\tfilepath.Join(from.LimaInstanceDir(), \"diffdisk\"),\n\t\t\t\tfilepath.Join(from.LimaInstanceDir(), \"cidata.iso\"),\n\t\t\t\tfilepath.Join(from.LimaInstanceDir(), \"lima.yaml\"),\n\t\t\t\tto.LimaInstanceDir(),\n\t\t\t).Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error copying VM: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t{\n\t\t\tlogrus.Info(\"copying config...\")\n\t\t\t// verify source config exists\n\t\t\tif _, err := os.Stat(from.LimaInstanceDir()); err != nil {\n\t\t\t\treturn fmt.Errorf(\"config missing for colima profile '%s': %w\", from.ShortName, err)\n\t\t\t}\n\n\t\t\t// ensure destination config directory\n\t\t\tif err := cli.Command(\"mkdir\", \"-p\", filepath.Dir(to.LimaInstanceDir())).Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"cannot copy config to new profile '%s': %w\", to.ShortName, err)\n\t\t\t}\n\n\t\t\tif err := cli.Command(\"cp\", from.LimaInstanceDir(), to.LimaInstanceDir()).Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error copying VM config: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\tlogrus.Info(\"clone successful\")\n\t\tlogrus.Infof(\"run `colima start %s` to start the newly cloned profile\", to.ShortName)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(cloneCmd)\n\tcloneCmd.Hidden = true\n\n}\n"
  },
  {
    "path": "cmd/colima/main.go",
    "content": "package main\n\nimport (\n\t_ \"github.com/abiosoft/colima/cmd\"        // for other commands\n\t_ \"github.com/abiosoft/colima/cmd/daemon\" // for vmnet daemon\n\t_ \"github.com/abiosoft/colima/embedded\"   // for embedded assets\n\n\t\"github.com/abiosoft/colima/cmd/root\"\n)\n\nfunc main() {\n\troot.Execute()\n}\n"
  },
  {
    "path": "cmd/completion.go",
    "content": "package cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/spf13/cobra\"\n)\n\n// completionCmd represents the completion command\nfunc completionCmd() *cobra.Command {\n\tcmd := &cobra.Command{\n\t\tUse:   \"completion [bash|zsh|fish|powershell]\",\n\t\tShort: \"Generate completion script\",\n\t\tLong: `To load completions:\nBash:\n\n  $ source <(colima completion bash)\n\n  # To load completions for each session, execute once:\n  # Linux:\n  $ colima completion bash > /etc/bash_completion.d/colima\n  # macOS:\n  $ colima completion bash > /usr/local/etc/bash_completion.d/colima\n\nZsh:\n\n  # If shell completion is not already enabled in your environment,\n  # you will need to enable it.  You can execute the following once:\n\n  $ echo \"autoload -U compinit; compinit\" >> ~/.zshrc\n\n  # To load completions for each session, execute once:\n  $ colima completion zsh > \"${fpath[1]}/_colima\"\n\n  # You will need to start a new shell for this setup to take effect.\n\nfish:\n\n  $ colima completion fish | source\n\n  # To load completions for each session, execute once:\n  $ colima completion fish > ~/.config/fish/completions/colima.fish\n\nPowerShell:\n\n  PS> colima completion powershell | Out-String | Invoke-Expression\n\n  # To load completions for every new session, run:\n  PS> colima completion powershell > colima.ps1\n  # and source this file from your PowerShell profile.\n`,\n\t\tDisableFlagsInUseLine: true,\n\t\tValidArgs:             []string{\"bash\", \"zsh\", \"fish\", \"powershell\"},\n\t\tArgs:                  cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),\n\t\tRun: func(cmd *cobra.Command, args []string) {\n\t\t\tswitch args[0] {\n\t\t\tcase \"bash\":\n\t\t\t\t_ = cmd.Root().GenBashCompletion(os.Stdout)\n\t\t\tcase \"zsh\":\n\t\t\t\t_ = cmd.Root().GenZshCompletion(os.Stdout)\n\t\t\tcase \"fish\":\n\t\t\t\t_ = cmd.Root().GenFishCompletion(os.Stdout, true)\n\t\t\tcase \"powershell\":\n\t\t\t\t_ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)\n\t\t\t}\n\t\t},\n\t}\n\treturn cmd\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(completionCmd())\n}\n"
  },
  {
    "path": "cmd/daemon/cmd.go",
    "content": "package daemon\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/daemon/process\"\n\t\"github.com/abiosoft/colima/daemon/process/inotify\"\n\t\"github.com/abiosoft/colima/daemon/process/vmnet\"\n\t\"github.com/abiosoft/colima/environment/host\"\n\t\"github.com/abiosoft/colima/environment/vm/lima\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar daemonCmd = &cobra.Command{\n\tUse:    \"daemon\",\n\tShort:  \"daemon\",\n\tLong:   `runner for background daemons.`,\n\tHidden: true,\n}\n\nvar startCmd = &cobra.Command{\n\tUse:   \"start [profile]\",\n\tShort: \"start daemon\",\n\tLong:  `start the daemon`,\n\tArgs:  cobra.ExactArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tconfig.SetProfile(args[0])\n\t\tctx := cmd.Context()\n\n\t\tvar processes []process.Process\n\t\tif daemonArgs.vmnet.enabled {\n\t\t\tprocesses = append(processes, vmnet.New(daemonArgs.vmnet.mode, daemonArgs.vmnet.netInterface))\n\t\t}\n\t\tif daemonArgs.inotify.enabled {\n\t\t\tprocesses = append(processes, inotify.New())\n\t\t\tguest := lima.New(host.New())\n\t\t\targs := inotify.Args{\n\t\t\t\tGuestActions: guest,\n\t\t\t\tRuntime:      daemonArgs.inotify.runtime,\n\t\t\t\tDirs:         daemonArgs.inotify.dirs,\n\t\t\t}\n\t\t\tctx = context.WithValue(ctx, inotify.CtxKeyArgs(), args)\n\t\t}\n\n\t\treturn start(ctx, processes)\n\t},\n}\n\nvar stopCmd = &cobra.Command{\n\tUse:   \"stop [profile]\",\n\tShort: \"stop daemon\",\n\tLong:  `stop the daemon`,\n\tArgs:  cobra.ExactArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tconfig.SetProfile(args[0])\n\n\t\t// wait for 60 seconds\n\t\ttimeout := time.Second * 60\n\t\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\t\tdefer cancel()\n\n\t\treturn stop(ctx)\n\t},\n}\n\nvar statusCmd = &cobra.Command{\n\tUse:   \"status\",\n\tShort: \"status of the daemon\",\n\tLong:  `status of the daemon`,\n\tArgs:  cobra.ExactArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tconfig.SetProfile(args[0])\n\n\t\treturn status()\n\t},\n}\n\nvar daemonArgs struct {\n\tvmnet struct {\n\t\tenabled      bool\n\t\tmode         string\n\t\tnetInterface string\n\t}\n\tinotify struct {\n\t\tenabled bool\n\t\tdirs    []string\n\t\truntime string\n\t}\n\n\tverbose bool\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(daemonCmd)\n\n\tdaemonCmd.AddCommand(startCmd)\n\tdaemonCmd.AddCommand(stopCmd)\n\tdaemonCmd.AddCommand(statusCmd)\n\n\tstartCmd.Flags().BoolVar(&daemonArgs.vmnet.enabled, \"vmnet\", false, \"start vmnet\")\n\tstartCmd.Flags().StringVar(&daemonArgs.vmnet.mode, \"vmnet-mode\", \"shared\", \"vmnet mode (shared, bridged)\")\n\tstartCmd.Flags().StringVar(&daemonArgs.vmnet.netInterface, \"vmnet-interface\", \"en0\", \"vmnet interface for bridged mode\")\n\tstartCmd.Flags().BoolVar(&daemonArgs.inotify.enabled, \"inotify\", false, \"start inotify\")\n\tstartCmd.Flags().StringSliceVar(&daemonArgs.inotify.dirs, \"inotify-dir\", nil, \"set inotify directories\")\n\tstartCmd.Flags().StringVar(&daemonArgs.inotify.runtime, \"inotify-runtime\", \"docker\", \"set runtime\")\n}\n"
  },
  {
    "path": "cmd/daemon/daemon.go",
    "content": "package daemon\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/daemon/process\"\n\t\"github.com/abiosoft/colima/util/fsutil\"\n\tgodaemon \"github.com/sevlyar/go-daemon\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nvar dir = process.Dir\n\n// daemonize creates the daemon and returns if this is a child process\nfunc daemonize() (ctx *godaemon.Context, child bool, err error) {\n\tdir := dir()\n\tif err := fsutil.MkdirAll(dir, 0755); err != nil {\n\t\treturn nil, false, fmt.Errorf(\"cannot make dir: %w\", err)\n\t}\n\n\tinfo := Info()\n\n\tctx = &godaemon.Context{\n\t\tPidFileName: info.PidFile,\n\t\tPidFilePerm: 0644,\n\t\tLogFileName: info.LogFile,\n\t\tLogFilePerm: 0644,\n\t}\n\n\td, err := ctx.Reborn()\n\tif err != nil {\n\t\treturn ctx, false, fmt.Errorf(\"error starting daemon: %w\", err)\n\t}\n\tif d != nil {\n\t\treturn ctx, false, nil\n\t}\n\n\tlogrus.Info(\"- - - - - - - - - - - - - - -\")\n\tlogrus.Info(\"daemon started by colima\")\n\tlogrus.Infof(\"Run `/usr/bin/pkill -F %s` to kill the daemon\", info.PidFile)\n\n\treturn ctx, true, nil\n}\n\nfunc start(ctx context.Context, processes []process.Process) error {\n\tif status() == nil {\n\t\tlogrus.Info(\"daemon already running, startup ignored\")\n\t\treturn nil\n\t}\n\n\t{\n\t\tctx, child, err := daemonize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif ctx != nil {\n\t\t\tdefer func() {\n\t\t\t\t_ = ctx.Release()\n\t\t\t}()\n\t\t}\n\n\t\tif !child {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)\n\tdefer stop()\n\n\treturn RunProcesses(ctx, processes...)\n}\n\nfunc stop(ctx context.Context) error {\n\tif status() != nil {\n\t\t// not running\n\t\treturn nil\n\t}\n\n\tinfo := Info()\n\n\tif err := cli.CommandInteractive(\"/usr/bin/pkill\", \"-F\", info.PidFile).Run(); err != nil {\n\t\treturn fmt.Errorf(\"error sending sigterm to daemon: %w\", err)\n\t}\n\n\tlogrus.Info(\"waiting for process to terminate\")\n\n\tfor {\n\t\talive := status() == nil\n\t\tif !alive {\n\t\t\treturn nil\n\t\t}\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t\ttime.Sleep(time.Second * 1)\n\t\t}\n\t}\n\n}\n\nfunc status() error {\n\tinfo := Info()\n\tif _, err := os.Stat(info.PidFile); err != nil {\n\t\treturn fmt.Errorf(\"pid file not found: %w\", err)\n\t}\n\n\t// check if process is actually running\n\tp, err := os.ReadFile(info.PidFile)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading pid file: %w\", err)\n\t}\n\tpid, _ := strconv.Atoi(string(p))\n\tif pid == 0 {\n\t\treturn fmt.Errorf(\"invalid pid: %v\", string(p))\n\t}\n\n\tprocess, err := os.FindProcess(pid)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"process not found: %v\", err)\n\t}\n\n\tif err := process.Signal(syscall.Signal(0)); err != nil {\n\t\treturn fmt.Errorf(\"process signal(0) returned error: %w\", err)\n\t}\n\n\treturn nil\n}\n\nconst (\n\tpidFileName = \"daemon.pid\"\n\tlogFileName = \"daemon.log\"\n)\n\nfunc Info() struct {\n\tPidFile string\n\tLogFile string\n} {\n\tdir := dir()\n\treturn struct {\n\t\tPidFile string\n\t\tLogFile string\n\t}{\n\t\tPidFile: filepath.Join(dir, pidFileName),\n\t\tLogFile: filepath.Join(dir, logFileName),\n\t}\n}\n\n// Run runs the daemon with background processes.\n// NOTE: this must be called from the program entrypoint with minimal intermediary logic\n// due to the creation of the daemon.\nfunc RunProcesses(ctx context.Context, processes ...process.Process) error {\n\tctx, stop := context.WithCancel(ctx)\n\tdefer stop()\n\n\tvar wg sync.WaitGroup\n\twg.Add(len(processes))\n\n\tfor _, bg := range processes {\n\t\tgo func(bg process.Process) {\n\t\t\terr := bg.Start(ctx)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Error(fmt.Errorf(\"error starting %s: %w\", bg.Name(), err))\n\t\t\t\tstop()\n\t\t\t}\n\t\t\twg.Done()\n\t\t}(bg)\n\t}\n\n\t<-ctx.Done()\n\tlogrus.Info(\"terminate signal received\")\n\n\twg.Wait()\n\n\treturn ctx.Err()\n}\n"
  },
  {
    "path": "cmd/daemon/daemon_test.go",
    "content": "package daemon\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/exec\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/daemon/process\"\n)\n\nvar testDir string\n\nfunc setDir(t *testing.T) {\n\tif testDir == \"\" {\n\t\ttestDir = t.TempDir()\n\t}\n\tdir = func() string { return testDir }\n}\n\nfunc getProcesses() []process.Process {\n\tvar addresses = []string{\n\t\t\"localhost\",\n\t\t\"127.0.0.1\",\n\t}\n\n\tvar processes []process.Process\n\tfor _, add := range addresses {\n\t\tprocesses = append(processes, &pinger{address: add})\n\t}\n\n\treturn processes\n}\n\nfunc TestStart(t *testing.T) {\n\tsetDir(t)\n\tinfo := Info()\n\n\tprocesses := getProcesses()\n\n\tt.Log(\"pidfile\", info.PidFile)\n\n\ttimeout := time.Second * 5\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\tdefer cancel()\n\n\t// start the processes\n\tif err := start(ctx, processes); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Log(\"start successful\")\n\n\t{\n\tloop:\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tt.Skipf(\"daemon not supported: %v\", ctx.Err())\n\t\t\tdefault:\n\t\t\t\tif p, err := os.ReadFile(info.PidFile); err == nil && len(p) > 0 {\n\t\t\t\t\tbreak loop\n\t\t\t\t} else if err != nil {\n\t\t\t\t\tt.Logf(\"encountered err: %v\", err)\n\t\t\t\t}\n\t\t\t\ttime.Sleep(1 * time.Second)\n\t\t\t}\n\t\t}\n\t}\n\n\t// verify the processes are running\n\tif err := status(); err != nil {\n\t\tt.Error(err)\n\t\treturn\n\t}\n\n\t// stop the processes\n\tif err := stop(ctx); err != nil {\n\t\tt.Error(err)\n\t}\n\n\t// verify the processes are no longer running\n\tif err := status(); err == nil {\n\t\tt.Errorf(\"process with pidFile %s is still running\", info.PidFile)\n\t\treturn\n\t}\n\n}\n\nfunc TestRunProcesses(t *testing.T) {\n\tprocesses := getProcesses()\n\n\ttimeout := time.Second * 5\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\n\t// start the processes\n\tdone := make(chan error, 1)\n\tgo func() {\n\t\tdone <- RunProcesses(ctx, processes...)\n\t}()\n\n\tcancel()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\tif err := ctx.Err(); err != context.Canceled {\n\t\t\tt.Error(err)\n\t\t}\n\tcase err := <-done:\n\t\tt.Error(err)\n\t}\n\n}\n\nvar _ process.Process = (*pinger)(nil)\n\ntype pinger struct {\n\taddress string\n}\n\nfunc (p pinger) Alive(ctx context.Context) error {\n\treturn nil\n}\n\n// Name implements BgProcess\nfunc (pinger) Name() string { return \"pinger\" }\n\n// Start implements BgProcess\nfunc (p *pinger) Start(ctx context.Context) error {\n\treturn p.run(ctx, \"ping\", \"-c10\", p.address)\n}\n\n// Start implements BgProcess\nfunc (p *pinger) Dependencies() ([]process.Dependency, bool) { return nil, false }\n\nfunc (p *pinger) run(ctx context.Context, command string, args ...string) error {\n\tcmd := exec.CommandContext(ctx, command, args...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\treturn cmd.Run()\n}\n"
  },
  {
    "path": "cmd/delete.go",
    "content": "package cmd\n\nimport (\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar deleteCmdArgs struct {\n\tforce bool\n\tdata  bool\n}\n\n// deleteCmd represents the delete command\nvar deleteCmd = &cobra.Command{\n\tUse:   \"delete [profile]\",\n\tShort: \"delete and teardown Colima\",\n\tLong: `Delete and teardown Colima and all settings.\n\nUse with caution. This deletes everything and a startup afterwards is like the\ninitial startup of Colima.`,\n\tArgs: cobra.MaximumNArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\treturn newApp().Delete(deleteCmdArgs.data, deleteCmdArgs.force)\n\t},\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(deleteCmd)\n\n\tdeleteCmd.Flags().BoolVarP(&deleteCmdArgs.force, \"force\", \"f\", false, \"do not prompt for yes/no\")\n\tdeleteCmd.Flags().BoolVarP(&deleteCmdArgs.data, \"data\", \"d\", false, \"delete container runtime data\")\n}\n"
  },
  {
    "path": "cmd/kubernetes.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment/container/kubernetes\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// kubernetesCmd represents the kubernetes command\nvar kubernetesCmd = &cobra.Command{\n\tUse:     \"kubernetes\",\n\tAliases: []string{\"kube\", \"k8s\", \"k3s\", \"k\"},\n\tShort:   \"manage Kubernetes cluster\",\n\tLong:    `Manage the Kubernetes cluster`,\n\tPersistentPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\t// cobra overrides PersistentPreRunE when redeclared.\n\t\t// re-run rootCmd's.\n\t\tif err := root.Cmd().PersistentPreRunE(cmd, args); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !newApp().Active() {\n\t\t\treturn fmt.Errorf(\"%s is not running\", config.CurrentProfile().DisplayName)\n\t\t}\n\t\treturn nil\n\t},\n}\n\n// kubernetesStartCmd represents the kubernetes start command\nvar kubernetesStartCmd = &cobra.Command{\n\tUse:   \"start\",\n\tShort: \"start the Kubernetes cluster\",\n\tLong:  `Start the Kubernetes cluster.`,\n\tArgs:  cobra.NoArgs,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tapp := newApp()\n\t\tk, err := app.Kubernetes()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := k.Provision(context.Background()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn k.Start(context.Background())\n\t},\n}\n\n// kubernetesStopCmd represents the kubernetes stop command\nvar kubernetesStopCmd = &cobra.Command{\n\tUse:   \"stop\",\n\tShort: \"stop the Kubernetes cluster\",\n\tLong:  `Stop the Kubernetes cluster.`,\n\tArgs:  cobra.NoArgs,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tctx := cmd.Context()\n\t\tapp := newApp()\n\t\tk, err := app.Kubernetes()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !k.Running(ctx) {\n\t\t\treturn fmt.Errorf(\"%s is not enabled\", kubernetes.Name)\n\t\t}\n\n\t\treturn k.Stop(ctx, false)\n\t},\n}\n\n// kubernetesDeleteCmd represents the kubernetes delete command\nvar kubernetesDeleteCmd = &cobra.Command{\n\tUse:   \"delete\",\n\tShort: \"delete the Kubernetes cluster\",\n\tLong:  `Delete the Kubernetes cluster.`,\n\tArgs:  cobra.NoArgs,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tctx := cmd.Context()\n\t\tapp := newApp()\n\t\tk, err := app.Kubernetes()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !k.Running(ctx) {\n\t\t\treturn fmt.Errorf(\"%s is not enabled\", kubernetes.Name)\n\t\t}\n\n\t\treturn k.Teardown(ctx)\n\t},\n}\n\n// kubernetesResetCmd represents the kubernetes reset command\nvar kubernetesResetCmd = &cobra.Command{\n\tUse:   \"reset\",\n\tShort: \"reset the Kubernetes cluster\",\n\tLong: `Reset the Kubernetes cluster.\n\nThis resets the Kubernetes cluster and all Kubernetes objects\nwill be deleted.\n\nThe Kubernetes images are cached making the startup (after reset) much faster.`,\n\tArgs: cobra.NoArgs,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tapp := newApp()\n\t\tk, err := app.Kubernetes()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := k.Teardown(context.Background()); err != nil {\n\t\t\treturn fmt.Errorf(\"error deleting %s: %w\", kubernetes.Name, err)\n\t\t}\n\n\t\tctx := context.Background()\n\t\tif err := k.Provision(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := k.Start(ctx); err != nil {\n\t\t\treturn fmt.Errorf(\"error starting %s: %w\", kubernetes.Name, err)\n\t\t}\n\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(kubernetesCmd)\n\tkubernetesCmd.AddCommand(kubernetesStartCmd)\n\tkubernetesCmd.AddCommand(kubernetesStopCmd)\n\tkubernetesCmd.AddCommand(kubernetesDeleteCmd)\n\tkubernetesCmd.AddCommand(kubernetesResetCmd)\n}\n"
  },
  {
    "path": "cmd/list.go",
    "content": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"text/tabwriter\"\n\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n\t\"github.com/docker/go-units\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar listCmdArgs struct {\n\tjson bool\n}\n\n// listCmd represents the version command\nvar listCmd = &cobra.Command{\n\tUse:     \"list\",\n\tAliases: []string{\"ls\"},\n\tShort:   \"list instances\",\n\tLong: `List all created instances.\n\nA new instance can be created during 'colima start' by specifying the '--profile' flag.`,\n\tArgs: cobra.MaximumNArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tprofile := config.CurrentProfile()\n\t\tprofileArgs := []string{}\n\n\t\tif profile.Changed {\n\t\t\tprofileArgs = append(profileArgs, profile.ID)\n\t\t}\n\n\t\tinstances, err := limautil.Instances(profileArgs...)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif listCmdArgs.json {\n\t\t\tencoder := json.NewEncoder(cmd.OutOrStdout())\n\t\t\t// print instance per line to conform with Lima's output\n\t\t\tfor _, instance := range instances {\n\t\t\t\t// dir should be hidden from the output\n\t\t\t\tinstance.Dir = \"\"\n\t\t\t\tif err := encoder.Encode(instance); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\tw := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0)\n\t\t_, _ = fmt.Fprintln(w, \"PROFILE\\tSTATUS\\tARCH\\tCPUS\\tMEMORY\\tDISK\\tRUNTIME\\tADDRESS\")\n\n\t\tif len(instances) == 0 {\n\t\t\tlogrus.Warn(\"No instance found. Run `colima start` to create an instance.\")\n\t\t}\n\n\t\tfor _, inst := range instances {\n\t\t\t_, _ = fmt.Fprintf(w, \"%s\\t%s\\t%s\\t%d\\t%s\\t%s\\t%s\\t%s\\n\",\n\t\t\t\tinst.Name,\n\t\t\t\tinst.Status,\n\t\t\t\tinst.Arch,\n\t\t\t\tinst.CPU,\n\t\t\t\tunits.BytesSize(float64(inst.Memory)),\n\t\t\t\tunits.BytesSize(float64(inst.Disk)),\n\t\t\t\tinst.Runtime,\n\t\t\t\tinst.IPAddress,\n\t\t\t)\n\t\t}\n\n\t\treturn w.Flush()\n\t},\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(listCmd)\n\n\tlistCmd.Flags().BoolVarP(&listCmdArgs.json, \"json\", \"j\", false, \"print json output\")\n}\n"
  },
  {
    "path": "cmd/model.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/abiosoft/colima/config/configmanager\"\n\t\"github.com/abiosoft/colima/model\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/terminal\"\n\t\"github.com/spf13/cobra\"\n)\n\n// modelCmdArgs holds command-line flags for the model command.\nvar modelCmdArgs struct {\n\tRunner    string\n\tServePort int\n}\n\n// modelCmd represents the model command\nvar modelCmd = &cobra.Command{\n\tUse:   \"model\",\n\tShort: \"manage AI models (requires docker runtime and krunkit VM type)\",\n\tLong: `Manage AI models inside the VM.\nThis requires docker runtime and krunkit VM type for GPU access.\n\nUse --runner to select the model runner:\n  - docker: Docker Model Runner (default)\n  - ramalama: Ramalama\n\nAll arguments are passed to the selected AI model runner.\nSpecifying '--' will pass arguments to the underlying tool.\n\nExamples:\n  colima model list\n  colima model pull ai/smollm2\n  colima model run ai/smollm2\n  colima model serve\n  colima model serve ai/smollm2 --port 8080\n\nMultiple registries are supported.\n`,\n\tPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\trunner, err := getModelRunner()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn runner.ValidatePrerequisites(newApp())\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif len(args) == 0 {\n\t\t\treturn cmd.Help()\n\t\t}\n\n\t\trunner, err := getModelRunner()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ta := newApp()\n\n\t\tif err := runner.EnsureProvisioned(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\trunnerArgs, err := runner.BuildArgs(args)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn a.SSH(runnerArgs...)\n\t},\n}\n\n// modelSetupCmd reinstalls the model runner in the VM.\nvar modelSetupCmd = &cobra.Command{\n\tUse:     \"setup\",\n\tShort:   \"install or update AI model runner in the VM\",\n\tLong:    `Install or update AI model runner and its dependencies in the VM.`,\n\tAliases: []string{\"update\"},\n\tPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\trunner, err := getModelRunner()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn runner.ValidatePrerequisites(newApp())\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\trunner, err := getModelRunner()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Check if setup is needed (on primary screen)\n\t\tstatus, err := runner.CheckSetup()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Print version info on primary screen\n\t\tfmt.Println(runner.DisplayName())\n\t\tif status.CurrentVersion != \"\" {\n\t\t\tfmt.Printf(\"current: %s\\n\", status.CurrentVersion)\n\t\t}\n\t\tif status.LatestVersion != \"\" {\n\t\t\tfmt.Printf(\"latest:  %s\\n\", status.LatestVersion)\n\t\t}\n\n\t\tif !status.NeedsSetup {\n\t\t\tfmt.Println()\n\t\t\tfmt.Println(\"Already up to date\")\n\t\t\treturn nil\n\t\t}\n\n\t\t// Build header for alternate screen\n\t\tseparator := \"────────────────────────────────────────\"\n\t\theader := fmt.Sprintf(\"Colima - %s Setup\\n%s\", runner.DisplayName(), separator)\n\n\t\t// Run setup in alternate screen\n\t\tif err := terminal.WithAltScreen(func() error {\n\t\t\treturn runner.Setup()\n\t\t}, header); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Print new version on primary screen after update\n\t\tif newVersion := runner.GetCurrentVersion(); newVersion != \"\" {\n\t\t\tfmt.Printf(\"updated: %s\\n\", newVersion)\n\t\t}\n\n\t\treturn nil\n\t},\n}\n\n// modelServeCmd serves a model API.\nvar modelServeCmd = &cobra.Command{\n\tUse:   \"serve [model]\",\n\tShort: \"serve a model API\",\n\tLong: `Serve a model API.\n\nThis starts a model server providing:\n  - OpenAI-compatible API at http://localhost:<port>/v1\n  - Web UI for chat at http://localhost:<port>\n\nPress Ctrl-C to stop the server.\n`,\n\tArgs: cobra.MaximumNArgs(1),\n\tPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\trunner, err := getModelRunner()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn runner.ValidatePrerequisites(newApp())\n\t},\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\trunner, err := getModelRunner()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Determine the model to serve\n\t\tvar modelName string\n\t\tif len(args) > 0 {\n\t\t\tmodelName = args[0]\n\t\t} else if runner.Name() == model.RunnerDocker {\n\t\t\t// For docker runner, get the first available model\n\t\t\tfirstModel, err := model.GetFirstModel()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif firstModel == \"\" {\n\t\t\t\treturn fmt.Errorf(\"no models available\\nPull a model first: colima model pull ai/smollm2\")\n\t\t\t}\n\t\t\tmodelName = firstModel\n\t\t} else {\n\t\t\treturn fmt.Errorf(\"model name is required for ramalama runner\\nUsage: colima model serve <model>\")\n\t\t}\n\n\t\tif err := runner.EnsureProvisioned(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Ensure the model is available (pull if necessary) - this happens outside alternate screen\n\t\tnormalizedModel, err := runner.EnsureModel(modelName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Determine the port to use\n\t\tport := modelCmdArgs.ServePort\n\t\tportExplicitlySet := cmd.Flags().Changed(\"port\")\n\n\t\t// If port was not explicitly set, find an available port starting from the default\n\t\tconst maxPortAttempts = 20\n\t\tif !portExplicitlySet {\n\t\t\tavailablePort, found := util.FindAvailablePort(port, maxPortAttempts)\n\t\t\tif !found {\n\t\t\t\treturn fmt.Errorf(\"no available port found in range %d-%d\", port, port+maxPortAttempts-1)\n\t\t\t}\n\t\t\tif availablePort != port {\n\t\t\t\tfmt.Printf(\"Port %d is in use, using port %d instead\\n\", port, availablePort)\n\t\t\t}\n\t\t\tport = availablePort\n\t\t} else {\n\t\t\t// User explicitly set the port, check if it's available\n\t\t\tif _, found := util.FindAvailablePort(port, 1); !found {\n\t\t\t\treturn fmt.Errorf(\"port %d is already in use\", port)\n\t\t\t}\n\t\t}\n\n\t\t// Build header for alternate screen\n\t\tseparator := \"────────────────────────────────────────\"\n\t\theader := fmt.Sprintf(\"Colima - Model Server (Ctrl-C to stop)\\nWeb UI & API at http://localhost:%d\\n%s\", port, separator)\n\n\t\t// Run in alternate screen with header\n\t\treturn terminal.WithAltScreen(func() error {\n\t\t\treturn runner.Serve(normalizedModel, port)\n\t\t}, header)\n\t},\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(modelCmd)\n\tmodelCmd.AddCommand(modelSetupCmd)\n\tmodelCmd.AddCommand(modelServeCmd)\n\n\t// Add --runner flag with default from config or ramalama\n\tmodelCmd.PersistentFlags().StringVar(&modelCmdArgs.Runner, \"runner\", \"\", \"AI model runner (docker, ramalama)\")\n\n\t// Add --port flag for serve command\n\tmodelServeCmd.Flags().IntVar(&modelCmdArgs.ServePort, \"port\", 8080, \"port for the web UI\")\n}\n\n// getModelRunner returns the appropriate runner based on flag or config.\nfunc getModelRunner() (model.Runner, error) {\n\trunnerType := modelCmdArgs.Runner\n\n\t// If not specified via flag, check instance config\n\tif runnerType == \"\" {\n\t\tif conf, err := configmanager.LoadInstance(); err == nil && conf.ModelRunner != \"\" {\n\t\t\trunnerType = conf.ModelRunner\n\t\t}\n\t}\n\n\t// Default to docker\n\tif runnerType == \"\" {\n\t\trunnerType = string(model.RunnerDocker)\n\t}\n\n\treturn model.GetRunner(model.RunnerType(runnerType))\n}\n"
  },
  {
    "path": "cmd/nerdctl.go",
    "content": "package cmd\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment/container/containerd\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/fsutil\"\n\t\"github.com/abiosoft/colima/util/osutil\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar nerdctlCmdArgs struct {\n\tforce           bool\n\tpath            string\n\tusrBinWriteable bool\n\tisColimaScript  bool\n}\n\n// nerdctlCmd represents the nerdctl command\nvar nerdctlCmd = &cobra.Command{\n\tUse:     \"nerdctl\",\n\tAliases: []string{\"nerd\", \"n\"},\n\tShort:   \"run nerdctl (requires containerd runtime)\",\n\tLong: `Run nerdctl to interact with containerd.\nThis requires containerd runtime.\n\nIt is recommended to specify '--' to differentiate from Colima flags.\n`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tapp := newApp()\n\t\tr, err := app.Runtime()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif r != containerd.Name {\n\t\t\treturn fmt.Errorf(\"nerdctl only supports %s runtime\", containerd.Name)\n\t\t}\n\n\t\t// collect CONTAINERD_* and NERDCTL_* environment variables from the host\n\t\tvar envVars []string\n\t\tfor _, env := range os.Environ() {\n\t\t\tif strings.HasPrefix(env, \"CONTAINERD_\") || strings.HasPrefix(env, \"NERDCTL_\") {\n\t\t\t\tenvVars = append(envVars, env)\n\t\t\t}\n\t\t}\n\n\t\tvar nerdctlArgs []string\n\t\tif len(envVars) > 0 {\n\t\t\t// use 'sudo env VAR=value ... nerdctl' to pass environment variables\n\t\t\tnerdctlArgs = append([]string{\"sudo\", \"env\"}, envVars...)\n\t\t\tnerdctlArgs = append(nerdctlArgs, \"nerdctl\")\n\t\t} else {\n\t\t\tnerdctlArgs = []string{\"sudo\", \"nerdctl\"}\n\t\t}\n\t\tnerdctlArgs = append(nerdctlArgs, args...)\n\n\t\treturn app.SSH(nerdctlArgs...)\n\t},\n}\n\n// nerdctlLinkFunc represents the nerdctl command\nvar nerdctlLinkFunc = func() *cobra.Command {\n\treturn &cobra.Command{\n\t\tUse:   \"install\",\n\t\tShort: \"install nerdctl alias script on the host\",\n\t\tLong:  `Install nerdctl alias script on the host. The script will be installed at ` + nerdctlDefaultInstallPath + `.`,\n\t\tArgs:  cobra.NoArgs,\n\t\tPreRun: func(cmd *cobra.Command, args []string) {\n\t\t\t// check if /usr/local/bin is writeable and no need for sudo\n\n\t\t\t// if the path is user-specified, ignore.\n\t\t\tif nerdctlCmdArgs.path != nerdctlDefaultInstallPath {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// attempt writing to the /usr/local/bin\n\t\t\ttmpFile := filepath.Join(filepath.Dir(nerdctlDefaultInstallPath), \"colima.tmp\")\n\t\t\tif err := os.WriteFile(tmpFile, []byte(\"tmp\"), 0777); err == nil {\n\t\t\t\tnerdctlCmdArgs.usrBinWriteable = true\n\t\t\t\t_ = os.Remove(tmpFile)\n\t\t\t}\n\n\t\t\t// check if the current file (if exists) is generated by colima\n\t\t\t// in such case no need for confirmation before overwrite\n\t\t\t// TODO: this is too basic, should be better\n\t\t\tif b, err := os.ReadFile(nerdctlCmdArgs.path); err == nil {\n\t\t\t\tif strings.Contains(string(b), \"colima nerdctl \") {\n\t\t\t\t\tnerdctlCmdArgs.isColimaScript = true\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t\texists := false\n\t\t\tif _, err := os.Stat(nerdctlCmdArgs.path); err == nil && !nerdctlCmdArgs.force && !nerdctlCmdArgs.isColimaScript {\n\t\t\t\treturn fmt.Errorf(\"%s exists, use --force to replace\", nerdctlCmdArgs.path)\n\t\t\t} else if err == nil {\n\t\t\t\texists = true\n\t\t\t}\n\n\t\t\tvar values = struct {\n\t\t\t\tColimaApp string\n\t\t\t\tProfile   string\n\t\t\t}{\n\t\t\t\tColimaApp: osutil.Executable(),\n\t\t\t\tProfile:   config.CurrentProfile().ShortName,\n\t\t\t}\n\t\t\tbuf, err := util.ParseTemplate(nerdctlScript, values)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error applying nerdctl script template: %w\", err)\n\t\t\t}\n\n\t\t\t// /usr/local/bin writeable i.e. sudo not needed\n\t\t\t// or user-specified install path, we assume user specified path is writeable\n\t\t\tif nerdctlCmdArgs.usrBinWriteable || nerdctlCmdArgs.path != nerdctlDefaultInstallPath {\n\t\t\t\tif exists {\n\t\t\t\t\tif err := os.Rename(nerdctlCmdArgs.path, nerdctlCmdArgs.path+\".moved\"); err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"error backing up existing file: %w\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif err := fsutil.MkdirAll(\"/usr/local/bin\", 0755); err != nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn os.WriteFile(nerdctlCmdArgs.path, buf, 0755)\n\t\t\t}\n\n\t\t\t// sudo is needed for the default path\n\t\t\tlog.Println(\"/usr/local/bin not writable, sudo password required to install nerdctl binary\")\n\t\t\tif exists && !nerdctlCmdArgs.isColimaScript {\n\t\t\t\tc := cli.CommandInteractive(\"sudo\", \"mv\", nerdctlCmdArgs.path, nerdctlCmdArgs.path+\".moved\")\n\t\t\t\tif err := c.Run(); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"error backing up existing file: %w\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t\t// prepare dir\n\t\t\t{\n\t\t\t\tc := cli.CommandInteractive(\"sudo\", \"mkdir\", \"-p\", \"/usr/local/bin\")\n\t\t\t\tif err := c.Run(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\t// install script\n\t\t\t{\n\t\t\t\tc := cli.CommandInteractive(\"sudo\", \"sh\", \"-c\", \"cat > \"+nerdctlCmdArgs.path)\n\t\t\t\tc.Stdin = bytes.NewReader(buf)\n\t\t\t\tif err := c.Run(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\t// ensure it is executable\n\t\t\tif err := cli.Command(\"sudo\", \"chmod\", \"+x\", nerdctlCmdArgs.path).Run(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t}\n}\n\nconst nerdctlDefaultInstallPath = \"/usr/local/bin/nerdctl\"\n\nconst nerdctlScript = `#!/usr/bin/env sh\n\n{{.ColimaApp}} nerdctl --profile {{.Profile}} -- \"$@\"\n`\n\nfunc init() {\n\troot.Cmd().AddCommand(nerdctlCmd)\n\n\tnerdctlLink := nerdctlLinkFunc()\n\tnerdctlCmd.AddCommand(nerdctlLink)\n\tnerdctlLink.Flags().BoolVarP(&nerdctlCmdArgs.force, \"force\", \"f\", false, \"replace \"+nerdctlDefaultInstallPath+\" (if exists)\")\n\tnerdctlLink.Flags().StringVar(&nerdctlCmdArgs.path, \"path\", nerdctlDefaultInstallPath, \"path to install nerdctl binary\")\n}\n"
  },
  {
    "path": "cmd/prune.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar pruneCmdArgs struct {\n\tforce bool\n\tall   bool\n}\n\n// pruneCmd represents the prune command\nvar pruneCmd = &cobra.Command{\n\tUse:   \"prune\",\n\tShort: \"prune cached downloaded assets\",\n\tLong:  `Prune cached downloaded assets`,\n\tArgs:  cobra.MaximumNArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcolimaCacheDir := config.CacheDir()\n\t\tlimaCacheDir := filepath.Join(filepath.Dir(colimaCacheDir), \"lima\")\n\t\tif !pruneCmdArgs.force {\n\t\t\tmsg := \"'\" + colimaCacheDir + \"' will be emptied, are you sure\"\n\t\t\tif pruneCmdArgs.all {\n\t\t\t\tmsg = \"'\" + colimaCacheDir + \"' and '\" + limaCacheDir + \"' will be emptied, are you sure\"\n\t\t\t}\n\t\t\tif y := cli.Prompt(msg); !y {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tlogrus.Info(\"Pruning \", strconv.Quote(config.CacheDir()))\n\t\tif err := os.RemoveAll(config.CacheDir()); err != nil {\n\t\t\treturn fmt.Errorf(\"error during prune: %w\", err)\n\t\t}\n\n\t\tif pruneCmdArgs.all {\n\t\t\tcmd := limautil.Limactl(\"prune\")\n\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error during Lima prune: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(pruneCmd)\n\n\tpruneCmd.Flags().BoolVarP(&pruneCmdArgs.force, \"force\", \"f\", false, \"do not prompt for yes/no\")\n\tpruneCmd.Flags().BoolVarP(&pruneCmdArgs.all, \"all\", \"a\", false, \"include Lima assets\")\n}\n"
  },
  {
    "path": "cmd/restart.go",
    "content": "package cmd\n\nimport (\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/abiosoft/colima/config/configmanager\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar restartCmdArgs struct {\n\tforce bool\n}\n\n// restartCmd represents the restart command\nvar restartCmd = &cobra.Command{\n\tUse:   \"restart [profile]\",\n\tShort: \"restart Colima\",\n\tLong: `Stop and then starts Colima.\n\nThe state of the VM is persisted at stop. A start afterwards\nshould return it back to its previous state.`,\n\tArgs: cobra.MaximumNArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t// validate if the instance was previously created\n\t\tif _, err := limautil.Instance(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tapp := newApp()\n\n\t\tif err := app.Stop(restartCmdArgs.force); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// delay a bit before starting\n\t\ttime.Sleep(time.Second * 3)\n\n\t\tconfig, err := configmanager.Load()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn app.Start(config)\n\t},\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(restartCmd)\n\n\trestartCmd.Flags().BoolVarP(&restartCmdArgs.force, \"force\", \"f\", false, \"during restart, do stop without graceful shutdown\")\n}\n"
  },
  {
    "path": "cmd/root/root.go",
    "content": "package root\n\nimport (\n\t\"log\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar versionInfo = config.AppVersion()\n\n// rootCmd represents the base command when called without any subcommands\nvar rootCmd = &cobra.Command{\n\tUse:     \"colima\",\n\tShort:   \"container runtimes on macOS with minimal setup\",\n\tLong:    `Colima provides container runtimes on macOS with minimal setup.`,\n\tVersion: versionInfo.Version,\n\tPersistentPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\t// use profile from environment variable if set\n\t\tprofile := config.EnvProfile()\n\n\t\tswitch cmd.Name() {\n\n\t\t// special case handling for commands directly interacting with the VM\n\t\t// start, stop, restart, delete, status, version, update, ssh-config\n\t\tcase \"start\",\n\t\t\t\"stop\",\n\t\t\t\"restart\",\n\t\t\t\"delete\",\n\t\t\t\"status\",\n\t\t\t\"list\",\n\t\t\t\"version\",\n\t\t\t\"update\",\n\t\t\t\"ssh-config\":\n\n\t\t\t// if an arg is passed, assume it to be the profile (provided --profile is unset)\n\t\t\t// i.e. colima start docker == colima start --profile=docker\n\t\t\t// takes precedence over the environment variable\n\t\t\tif len(args) > 0 && !cmd.Flag(\"profile\").Changed {\n\t\t\t\tprofile = args[0]\n\t\t\t}\n\t\t}\n\n\t\t// if profile is set via flag, use it\n\t\t// takes precedence over the environment variable and arg\n\t\tif cmd.Flag(\"profile\").Changed {\n\t\t\tprofile = rootCmdArgs.Profile\n\t\t}\n\n\t\tif profile != \"\" {\n\t\t\tconfig.SetProfile(profile)\n\t\t}\n\n\t\tinitLog()\n\n\t\tcmd.SilenceUsage = true\n\t\tcmd.SilenceErrors = true\n\t\treturn nil\n\t},\n}\n\n// Cmd returns the root command.\nfunc Cmd() *cobra.Command {\n\treturn rootCmd\n}\n\n// rootCmdArgs holds all flags configured in root Cmd\nvar rootCmdArgs struct {\n\tProfile     string\n\tVerbose     bool\n\tVeryVerbose bool\n}\n\n// Execute adds all child commands to the root command and sets flags appropriately.\n// This is called by main.main(). It only needs to happen once to the rootCmd.\nfunc Execute() {\n\tif err := rootCmd.Execute(); err != nil {\n\t\tlogrus.Fatal(err)\n\t}\n}\n\nfunc init() {\n\trootCmd.PersistentFlags().BoolVarP(&rootCmdArgs.Verbose, \"verbose\", \"v\", rootCmdArgs.Verbose, \"enable verbose log\")\n\trootCmd.PersistentFlags().BoolVar(&rootCmdArgs.VeryVerbose, \"very-verbose\", rootCmdArgs.VeryVerbose, \"enable more verbose log\")\n\trootCmd.PersistentFlags().StringVarP(&rootCmdArgs.Profile, \"profile\", \"p\", \"default\", \"profile name, for multiple instances\")\n}\n\nfunc initLog() {\n\tif rootCmdArgs.Verbose {\n\t\tcli.Settings.Verbose = true\n\t\tlogrus.SetLevel(logrus.DebugLevel)\n\t}\n\tif rootCmdArgs.VeryVerbose {\n\t\tcli.Settings.Verbose = true\n\t\tlogrus.SetLevel(logrus.TraceLevel)\n\t}\n\n\t// general log output\n\tlog.SetOutput(logrus.StandardLogger().Writer())\n\tlog.SetFlags(0)\n}\n"
  },
  {
    "path": "cmd/ssh-config.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n\t\"github.com/spf13/cobra\"\n)\n\n// statusCmd represents the status command\nvar sshConfigCmd = &cobra.Command{\n\tUse:   \"ssh-config [profile]\",\n\tShort: \"show SSH connection config\",\n\tLong:  `Show configuration of the SSH connection to the VM.`,\n\tArgs:  cobra.MaximumNArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tresp, err := limautil.ShowSSH(config.CurrentProfile().ID)\n\t\tif err == nil {\n\t\t\tfmt.Println(resp.Output)\n\t\t}\n\t\treturn err\n\t},\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(sshConfigCmd)\n}\n"
  },
  {
    "path": "cmd/ssh.go",
    "content": "package cmd\n\nimport (\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/spf13/cobra\"\n)\n\n// sshCmd represents the ssh command\nvar sshCmd = &cobra.Command{\n\tUse:     \"ssh\",\n\tAliases: []string{\"exec\", \"x\"},\n\tShort:   \"SSH into the VM\",\n\tLong: `SSH into the VM.\n\nAppending additional command runs the command instead.\ne.g. 'colima ssh -- htop' will run htop.\n\nIt is recommended to specify '--' to differentiate from colima flags.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\treturn newApp().SSH(args...)\n\t},\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(sshCmd)\n}\n"
  },
  {
    "path": "cmd/start.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/app\"\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/config/configmanager\"\n\t\"github.com/abiosoft/colima/core\"\n\t\"github.com/abiosoft/colima/embedded\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/environment/container/docker\"\n\t\"github.com/abiosoft/colima/environment/container/incus\"\n\t\"github.com/abiosoft/colima/environment/container/kubernetes\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/downloader\"\n\t\"github.com/abiosoft/colima/util/osutil\"\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n)\n\n// startCmd represents the start command\nvar startCmd = &cobra.Command{\n\tUse:   \"start [profile]\",\n\tShort: \"start Colima\",\n\tLong: `Start Colima with the specified container runtime and optional kubernetes.\n\nColima can also be configured with a YAML file.\nRun 'colima template' to set the default configurations or 'colima start --edit' to customize before startup.\n`,\n\tExample: \"  colima start\\n\" +\n\t\t\"  colima start --edit\\n\" +\n\t\t\"  colima start --foreground\\n\" +\n\t\t\"  colima start --runtime containerd\\n\" +\n\t\t\"  colima start --kubernetes\\n\" +\n\t\t\"  colima start --runtime containerd --kubernetes\\n\" +\n\t\t\"  colima start --cpu 4 --memory 8 --disk 100\\n\" +\n\t\t\"  colima start --arch aarch64\\n\" +\n\t\t\"  colima start --dns 1.1.1.1 --dns 8.8.8.8\\n\" +\n\t\t\"  colima start --dns-host example.com=1.2.3.4\\n\" +\n\t\t\"  colima start --gateway-address 192.168.6.2\\n\" +\n\t\t\"  colima start --kubernetes --k3s-arg='\\\"--disable=coredns,servicelb,traefik,local-storage,metrics-server\\\"'\",\n\tArgs: cobra.MaximumNArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tapp := newApp()\n\t\tconf := startCmdArgs.Config\n\n\t\tif !startCmdArgs.Flags.Edit {\n\t\t\tif app.Active() {\n\t\t\t\tlog.Warnln(\"already running, ignoring\")\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn start(app, conf)\n\t\t}\n\n\t\t// edit flag is specified\n\t\tconf, err := editConfigFile()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// validate config\n\t\tif err := configmanager.ValidateConfig(conf); err != nil {\n\t\t\treturn fmt.Errorf(\"error in config file: %w\", err)\n\t\t}\n\n\t\tif app.Active() {\n\t\t\tif !cli.Prompt(\"colima is currently running, restart to apply changes\") {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif err := app.Stop(false); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error stopping :%w\", err)\n\t\t\t}\n\t\t\t// pause before startup to prevent race condition\n\t\t\ttime.Sleep(time.Second * 3)\n\t\t}\n\n\t\treturn start(app, conf)\n\t},\n\tPreRunE: func(cmd *cobra.Command, args []string) error {\n\t\t// validate Lima version\n\t\tif err := core.LimaVersionSupported(); err != nil {\n\t\t\treturn fmt.Errorf(\"lima compatibility error: %w\", err)\n\t\t}\n\n\t\t// combine args and current config file(if any)\n\t\tprepareConfig(cmd)\n\n\t\t// validate config\n\t\tif err := configmanager.ValidateConfig(startCmdArgs.Config); err != nil {\n\t\t\treturn fmt.Errorf(\"error in config: %w\", err)\n\t\t}\n\n\t\t// persist in preparation for application start\n\t\tif startCmdArgs.Flags.SaveConfig {\n\t\t\tif err := configmanager.Save(startCmdArgs.Config); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error preparing config file: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// validate and set downloader if flag is specified (takes precedence over env var)\n\t\tif cmd.Flag(\"downloader\").Changed {\n\t\t\tnormalized, err := downloader.ValidateDownloader(startCmdArgs.Flags.Downloader)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdownloader.SetDownloader(normalized)\n\t\t}\n\n\t\treturn nil\n\t},\n}\n\nconst (\n\tdefaultCPU               = 2\n\tdefaultMemory            = 2\n\tdefaultDisk              = 100\n\tdefaultRootDisk          = 20\n\tdefaultKubernetesVersion = kubernetes.DefaultVersion\n\n\tdefaultMountTypeQEMU = \"sshfs\"\n\tdefaultMountTypeVZ   = \"virtiofs\"\n)\n\nvar (\n\tdefaultVMType  = \"qemu\"\n\tdefaultK3sArgs = []string{\"--disable=traefik\"}\n\tenvSaveConfig  = osutil.EnvVar(\"COLIMA_SAVE_CONFIG\")\n)\n\nvar startCmdArgs struct {\n\tconfig.Config\n\n\tFlags struct {\n\t\tMounts                  []string\n\t\tLegacyKubernetes        bool // for backward compatibility\n\t\tLegacyKubernetesDisable []string\n\t\tEdit                    bool\n\t\tEditor                  string\n\t\tActivateRuntime         bool\n\t\tBinfmt                  bool\n\t\tDNSHosts                []string\n\t\tForeground              bool\n\t\tSaveConfig              bool\n\t\tLegacyCPU               int // for backward compatibility\n\t\tTemplate                bool\n\t\tDownloader              string // downloader to use (native, curl)\n\t}\n}\n\nfunc init() {\n\truntimes := strings.Join(environment.ContainerRuntimes(), \", \")\n\tdefaultArch := string(environment.HostArch())\n\tdefaultVMType = environment.DefaultVMType()\n\n\tdefaultMountType := defaultMountTypeQEMU\n\tif defaultVMType == \"vz\" {\n\t\tdefaultMountType = defaultMountTypeVZ\n\t}\n\n\tmounts := strings.Join([]string{defaultMountTypeQEMU, \"9p\", \"virtiofs\"}, \", \")\n\n\tvmTypes := []string{\"qemu\", \"vz\"}\n\tif util.MacOS13OrNewerOnArm() {\n\t\tvmTypes = append(vmTypes, \"krunkit\")\n\t}\n\ttypes := strings.Join(vmTypes, \", \")\n\n\tsaveConfigDefault := true\n\tif envSaveConfig.Exists() {\n\t\tsaveConfigDefault = envSaveConfig.Bool()\n\t}\n\n\troot.Cmd().AddCommand(startCmd)\n\tstartCmd.Flags().StringVarP(&startCmdArgs.Runtime, \"runtime\", \"r\", docker.Name, \"container runtime (\"+runtimes+\")\")\n\tstartCmd.Flags().BoolVar(&startCmdArgs.Flags.ActivateRuntime, \"activate\", true, \"set as active Docker/Kubernetes/Incus context on startup\")\n\tstartCmd.Flags().IntVarP(&startCmdArgs.CPU, \"cpus\", \"c\", defaultCPU, \"number of CPUs\")\n\tstartCmd.Flags().StringVar(&startCmdArgs.CPUType, \"cpu-type\", \"\", \"the CPU type, options can be checked with 'qemu-system-\"+defaultArch+\" -cpu help'\")\n\tstartCmd.Flags().Float32VarP(&startCmdArgs.Memory, \"memory\", \"m\", defaultMemory, \"memory in GiB\")\n\tstartCmd.Flags().IntVarP(&startCmdArgs.Disk, \"disk\", \"d\", defaultDisk, \"disk size in GiB\")\n\tstartCmd.Flags().IntVar(&startCmdArgs.RootDisk, \"root-disk\", defaultRootDisk, \"disk size in GiB for the root filesystem\")\n\tstartCmd.Flags().StringVarP(&startCmdArgs.Arch, \"arch\", \"a\", defaultArch, \"architecture (aarch64, x86_64)\")\n\tstartCmd.Flags().BoolVarP(&startCmdArgs.Flags.Foreground, \"foreground\", \"f\", false, \"Keep colima in the foreground\")\n\tstartCmd.Flags().StringVar(&startCmdArgs.Hostname, \"hostname\", \"\", \"custom hostname for the virtual machine\")\n\tstartCmd.Flags().StringVarP(&startCmdArgs.DiskImage, \"disk-image\", \"i\", \"\", \"file path to a custom disk image\")\n\tstartCmd.Flags().BoolVar(&startCmdArgs.Flags.Template, \"template\", true, \"use the template file for initial configuration\")\n\n\t// port forwarder\n\tstartCmd.Flags().StringVar(&startCmdArgs.PortForwarder, \"port-forwarder\", \"ssh\", \"port forwarder to use (ssh, grpc, none)\")\n\n\t// retain cpu flag for backward compatibility\n\tstartCmd.Flags().IntVar(&startCmdArgs.Flags.LegacyCPU, \"cpu\", defaultCPU, \"number of CPUs\")\n\tstartCmd.Flag(\"cpu\").Hidden = true\n\n\t// host IP addresses\n\tstartCmd.Flags().BoolVar(&startCmdArgs.Network.HostAddresses, \"network-host-addresses\", false, \"support port forwarding to specific host IP addresses\")\n\n\tbinfmtDesc := \"use binfmt for foreign architecture emulation\"\n\n\tif util.MacOS() {\n\t\t// network address\n\t\tstartCmd.Flags().BoolVar(&startCmdArgs.Network.Address, \"network-address\", false, \"assign reachable IP address to the VM\")\n\t\tstartCmd.Flags().StringVar(&startCmdArgs.Network.Mode, \"network-mode\", \"shared\", \"network mode (shared, bridged)\")\n\t\tstartCmd.Flags().StringVar(&startCmdArgs.Network.BridgeInterface, \"network-interface\", \"en0\", \"host network interface to use for bridged mode\")\n\t\tstartCmd.Flags().BoolVar(&startCmdArgs.Network.PreferredRoute, \"network-preferred-route\", false, \"use the assigned IP address as the preferred route for the VM (implies --network-address)\")\n\n\t\t// vm type\n\t\tif util.MacOS13OrNewer() {\n\t\t\tstartCmd.Flags().StringVarP(&startCmdArgs.VMType, \"vm-type\", \"t\", defaultVMType, \"virtual machine type (\"+types+\")\")\n\t\t\tif util.MacOS13OrNewerOnArm() {\n\t\t\t\tstartCmd.Flags().BoolVar(&startCmdArgs.VZRosetta, \"vz-rosetta\", false, \"enable Rosetta for amd64 emulation\")\n\t\t\t\tstartCmd.Flags().StringVar(&startCmdArgs.ModelRunner, \"model-runner\", \"docker\", \"AI model runner (docker, ramalama)\")\n\t\t\t\tbinfmtDesc += \" (no-op if Rosetta is enabled)\"\n\t\t\t}\n\t\t}\n\n\t\t// nested virtualization\n\t\tif util.MacOSNestedVirtualizationSupported() {\n\t\t\tstartCmd.Flags().BoolVarP(&startCmdArgs.NestedVirtualization, \"nested-virtualization\", \"z\", false, \"enable nested virtualization\")\n\t\t}\n\t}\n\n\t// Gateway Address\n\tstartCmd.Flags().IPVar(&startCmdArgs.Network.GatewayAddress, \"gateway-address\", net.ParseIP(\"192.168.5.2\"), \"gateway address\")\n\n\t// binfmt\n\tstartCmd.Flags().BoolVar(&startCmdArgs.Flags.Binfmt, \"binfmt\", true, binfmtDesc)\n\n\t// config\n\tstartCmd.Flags().BoolVarP(&startCmdArgs.Flags.Edit, \"edit\", \"e\", false, \"edit the configuration file before starting\")\n\tstartCmd.Flags().StringVar(&startCmdArgs.Flags.Editor, \"editor\", \"\", `editor to use for edit e.g. vim, nano, code (default \"$EDITOR\" env var)`)\n\tstartCmd.Flags().BoolVar(&startCmdArgs.Flags.SaveConfig, \"save-config\", saveConfigDefault, \"persist and overwrite config file with (newly) specified flags\")\n\n\t// mounts\n\tstartCmd.Flags().StringSliceVarP(&startCmdArgs.Flags.Mounts, \"mount\", \"V\", nil, \"directories to mount, suffix ':w' for writable, disable with 'none'\")\n\tstartCmd.Flags().StringVar(&startCmdArgs.MountType, \"mount-type\", defaultMountType, \"volume driver for the mount (\"+mounts+\")\")\n\tstartCmd.Flags().BoolVar(&startCmdArgs.MountINotify, \"mount-inotify\", true, \"propagate inotify file events to the VM\")\n\n\t// ssh\n\tstartCmd.Flags().BoolVarP(&startCmdArgs.ForwardAgent, \"ssh-agent\", \"s\", false, \"forward SSH agent to the VM\")\n\tstartCmd.Flags().BoolVar(&startCmdArgs.SSHConfig, \"ssh-config\", true, \"generate SSH config in ~/.ssh/config\")\n\tstartCmd.Flags().IntVar(&startCmdArgs.SSHPort, \"ssh-port\", 0, \"SSH server port\")\n\n\t// k8s\n\tstartCmd.Flags().BoolVarP(&startCmdArgs.Kubernetes.Enabled, \"kubernetes\", \"k\", false, \"start with Kubernetes\")\n\tstartCmd.Flags().BoolVar(&startCmdArgs.Flags.LegacyKubernetes, \"with-kubernetes\", false, \"start with Kubernetes\")\n\tstartCmd.Flags().StringVar(&startCmdArgs.Kubernetes.Version, \"kubernetes-version\", defaultKubernetesVersion, \"must match a k3s version https://github.com/k3s-io/k3s/releases\")\n\tstartCmd.Flags().StringSliceVar(&startCmdArgs.Flags.LegacyKubernetesDisable, \"kubernetes-disable\", nil, \"components to disable for k3s e.g. traefik,servicelb\")\n\tstartCmd.Flags().StringSliceVar(&startCmdArgs.Kubernetes.K3sArgs, \"k3s-arg\", defaultK3sArgs, \"additional args to pass to k3s\")\n\tstartCmd.Flags().IntVar(&startCmdArgs.Kubernetes.Port, \"k3s-listen-port\", 0, \"k3s server listen port\")\n\tstartCmd.Flag(\"with-kubernetes\").Hidden = true\n\tstartCmd.Flag(\"kubernetes-disable\").Hidden = true\n\n\t// env\n\tstartCmd.Flags().StringToStringVar(&startCmdArgs.Env, \"env\", nil, \"environment variables for the VM\")\n\n\t// dns\n\tstartCmd.Flags().IPSliceVarP(&startCmdArgs.Network.DNSResolvers, \"dns\", \"n\", nil, \"DNS resolvers for the VM\")\n\tstartCmd.Flags().StringSliceVar(&startCmdArgs.Flags.DNSHosts, \"dns-host\", nil, \"custom DNS names to provide to resolver\")\n\n\t// download options\n\tstartCmd.Flags().StringVar(&startCmdArgs.Flags.Downloader, \"downloader\", downloader.DownloaderNative, \"downloader to use (native, curl)\")\n}\n\nfunc dnsHostsFromFlag(hosts []string) map[string]string {\n\tmapping := make(map[string]string)\n\n\tfor _, h := range hosts {\n\t\tstr := strings.SplitN(h, \"=\", 2)\n\t\tif len(str) != 2 {\n\t\t\tlog.Warnf(\"unable to parse custom dns host: %v, skipping\\n\", h)\n\t\t\tcontinue\n\t\t}\n\t\tsrc := str[0]\n\t\ttarget := str[1]\n\n\t\tmapping[src] = target\n\t}\n\treturn mapping\n}\n\n// mountsFromFlag converts mounts from cli flag format to config file format\nfunc mountsFromFlag(mounts []string) []config.Mount {\n\tmnts := make([]config.Mount, len(mounts))\n\tfor i, mount := range mounts {\n\n\t\t// if one of the parameters is none, treat as none.\n\t\tif strings.ToLower(mount) == \"none\" {\n\t\t\treturn nil\n\t\t}\n\n\t\tstr := strings.SplitN(mount, \":\", 3)\n\t\tmnt := config.Mount{Location: str[0]}\n\n\t\tif len(str) > 1 {\n\t\t\tif filepath.IsAbs(str[1]) {\n\t\t\t\tmnt.MountPoint = str[1]\n\t\t\t} else if str[1] == \"w\" {\n\t\t\t\tmnt.Writable = true\n\t\t\t}\n\t\t}\n\t\tif len(str) > 2 && str[2] == \"w\" {\n\t\t\tmnt.Writable = true\n\t\t}\n\n\t\tmnts[i] = mnt\n\t}\n\treturn mnts\n}\n\nfunc setFlagDefaults(cmd *cobra.Command) {\n\tif startCmdArgs.VMType == \"\" {\n\t\tstartCmdArgs.VMType = defaultVMType\n\t}\n\n\tif util.MacOS13OrNewer() {\n\t\t// changing to vz implies changing mount type to virtiofs\n\t\tif cmd.Flag(\"vm-type\").Changed && startCmdArgs.VMType == \"vz\" && !cmd.Flag(\"mount-type\").Changed {\n\t\t\tstartCmdArgs.MountType = \"virtiofs\"\n\t\t\tcmd.Flag(\"mount-type\").Changed = true\n\t\t}\n\t}\n\n\t// mount type\n\t{\n\t\t// convert mount type for qemu\n\t\tif startCmdArgs.VMType != \"vz\" && startCmdArgs.VMType != \"krunkit\" && startCmdArgs.MountType == defaultMountTypeVZ {\n\t\t\tstartCmdArgs.MountType = defaultMountTypeQEMU\n\t\t\tif cmd.Flag(\"mount-type\").Changed {\n\t\t\t\tlog.Warnf(\"%s is only available for 'vz' vmType, using %s\", defaultMountTypeVZ, defaultMountTypeQEMU)\n\t\t\t}\n\t\t}\n\t\t// convert mount type for vz\n\t\tif startCmdArgs.VMType == \"vz\" && startCmdArgs.MountType == \"9p\" {\n\t\t\tstartCmdArgs.MountType = \"virtiofs\"\n\t\t\tif cmd.Flag(\"mount-type\").Changed {\n\t\t\t\tlog.Warnf(\"9p is only available for 'qemu' vmType, using %s\", defaultMountTypeVZ)\n\t\t\t}\n\t\t}\n\t}\n\n\t// always enable nested virtualization for incus, if supported and not explicitly disabled.\n\tif util.MacOSNestedVirtualizationSupported() {\n\t\tif !cmd.Flag(\"nested-virtualization\").Changed {\n\t\t\tif startCmdArgs.Runtime == incus.Name && (startCmdArgs.VMType == \"vz\" || startCmdArgs.VMType == \"krunkit\") {\n\t\t\t\tstartCmdArgs.NestedVirtualization = true\n\t\t\t}\n\t\t}\n\t}\n\n\t// always enable network address for incus, if supported and not explicitly disabled\n\tif util.MacOS13OrNewer() {\n\t\tif !cmd.Flag(\"network-address\").Changed {\n\t\t\tif startCmdArgs.Runtime == incus.Name && startCmdArgs.VMType == \"vz\" {\n\t\t\t\tstartCmdArgs.Network.Address = true\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc setConfigDefaults(conf *config.Config) {\n\t// handle macOS virtualization.framework transition\n\tif conf.VMType == \"\" {\n\t\tconf.VMType = defaultVMType\n\t\t// if on macOS with no qemu, use vz\n\t\tif err := util.AssertQemuImg(); err != nil && util.MacOS13OrNewer() {\n\t\t\tconf.VMType = \"vz\"\n\t\t}\n\t}\n\n\tif conf.MountType == \"\" {\n\t\tconf.MountType = defaultMountTypeQEMU\n\t\tif util.MacOS13OrNewer() && conf.VMType == \"vz\" {\n\t\t\tconf.MountType = defaultMountTypeVZ\n\t\t}\n\t}\n\n\tif conf.Hostname == \"\" {\n\t\tconf.Hostname = config.CurrentProfile().ID\n\t}\n\n\tif conf.PortForwarder == \"\" {\n\t\tconf.PortForwarder = \"ssh\"\n\t}\n}\n\nfunc setFixedConfigs(conf *config.Config) {\n\tfixedConf, err := configmanager.LoadFrom(config.CurrentProfile().StateFile())\n\tif err != nil {\n\t\treturn\n\t}\n\n\twarnIfNotEqual := func(name, newVal, fixedVal string) {\n\t\tif newVal != fixedVal {\n\t\t\tlog.Warnln(fmt.Errorf(\"'%s' cannot be updated after initial setup, discarded\", name))\n\t\t}\n\t}\n\n\t// override the fixed configs\n\t// arch, vmType, mountType, runtime are fixed and cannot be changed\n\tif fixedConf.Arch != \"\" {\n\t\twarnIfNotEqual(\"architecture\", conf.Arch, fixedConf.Arch)\n\t\tconf.Arch = fixedConf.Arch\n\t}\n\tif fixedConf.VMType != \"\" {\n\t\twarnIfNotEqual(\"virtual machine type\", conf.VMType, fixedConf.VMType)\n\t\tconf.VMType = fixedConf.VMType\n\t}\n\tif fixedConf.Runtime != \"\" {\n\t\twarnIfNotEqual(\"runtime\", conf.Runtime, fixedConf.Runtime)\n\t\tconf.Runtime = fixedConf.Runtime\n\t}\n\tif fixedConf.MountType != \"\" {\n\t\twarnIfNotEqual(\"volume mount type\", conf.MountType, fixedConf.MountType)\n\t\tconf.MountType = fixedConf.MountType\n\t}\n\tif fixedConf.Network.Address && !conf.Network.Address {\n\t\tlog.Warnln(\"network address cannot be disabled once enabled\")\n\t\tconf.Network.Address = true\n\t}\n\tif fixedConf.Network.Mode != \"\" {\n\t\twarnIfNotEqual(\"network mode\", conf.Network.Mode, fixedConf.Network.Mode)\n\t\tconf.Network.Mode = fixedConf.Network.Mode\n\t}\n}\n\nfunc prepareConfig(cmd *cobra.Command) {\n\tcurrent, err := configmanager.Load()\n\tif err != nil {\n\t\t// not fatal, will proceed with defaults\n\t\tlog.Warnln(fmt.Errorf(\"config load failed: %w\", err))\n\t\tlog.Warnln(\"reverting to default settings\")\n\t}\n\n\t// handle legacy kubernetes flag\n\tif cmd.Flag(\"with-kubernetes\").Changed {\n\t\tstartCmdArgs.Kubernetes.Enabled = startCmdArgs.Flags.LegacyKubernetes\n\t\tcmd.Flag(\"kubernetes\").Changed = true\n\t}\n\n\t// handle legacy cpu flag\n\tif cmd.Flag(\"cpu\").Changed && !cmd.Flag(\"cpus\").Changed {\n\t\tstartCmdArgs.CPU = startCmdArgs.Flags.LegacyCPU\n\t\tcmd.Flag(\"cpus\").Changed = true\n\t}\n\n\t// convert cli to config file format\n\tstartCmdArgs.Mounts = mountsFromFlag(startCmdArgs.Flags.Mounts)\n\tstartCmdArgs.Network.DNSHosts = dnsHostsFromFlag(startCmdArgs.Flags.DNSHosts)\n\tstartCmdArgs.ActivateRuntime = &startCmdArgs.Flags.ActivateRuntime\n\tstartCmdArgs.Binfmt = &startCmdArgs.Flags.Binfmt\n\n\t// handle legacy kubernetes-disable\n\tfor _, disable := range startCmdArgs.Flags.LegacyKubernetesDisable {\n\t\tstartCmdArgs.Kubernetes.K3sArgs = append(startCmdArgs.Kubernetes.K3sArgs, \"--disable=\"+disable)\n\t}\n\n\t// set relevant missing default values\n\tsetFlagDefaults(cmd)\n\n\t// if there is no existing settings\n\tif current.Empty() {\n\t\ttemplateUsed := false\n\n\t\t// attempt template if enabled\n\t\tif startCmdArgs.Flags.Template {\n\t\t\ttemplate, err := configmanager.LoadFrom(templateFile())\n\t\t\tif err == nil {\n\t\t\t\tcurrent = template\n\t\t\t\ttemplateUsed = true\n\t\t\t}\n\t\t}\n\n\t\tif !templateUsed {\n\t\t\t// use default config if there is no template or template is disabled\n\t\t\treturn\n\t\t}\n\t}\n\n\t// set missing defaults in the current config\n\tsetConfigDefaults(&current)\n\n\t// docker can only be set in config file\n\tstartCmdArgs.Docker = current.Docker\n\t// provision scripts can only be set in config file\n\tstartCmdArgs.Provision = current.Provision\n\n\t// use current settings for unchanged configs\n\t// otherwise may be reverted to their default values.\n\tif !cmd.Flag(\"arch\").Changed {\n\t\tstartCmdArgs.Arch = current.Arch\n\t}\n\tif !cmd.Flag(\"disk\").Changed {\n\t\tstartCmdArgs.Disk = current.Disk\n\t}\n\tif !cmd.Flag(\"root-disk\").Changed {\n\t\tif current.RootDisk > 0 {\n\t\t\tstartCmdArgs.RootDisk = current.RootDisk\n\t\t}\n\t}\n\tif !cmd.Flag(\"kubernetes\").Changed {\n\t\tstartCmdArgs.Kubernetes.Enabled = current.Kubernetes.Enabled\n\t}\n\tif !cmd.Flag(\"kubernetes-version\").Changed && current.Kubernetes.Version != \"\" {\n\t\tstartCmdArgs.Kubernetes.Version = current.Kubernetes.Version\n\t}\n\tif !cmd.Flag(\"k3s-arg\").Changed && current.Kubernetes.K3sArgs != nil {\n\t\tstartCmdArgs.Kubernetes.K3sArgs = current.Kubernetes.K3sArgs\n\t}\n\tif !cmd.Flag(\"k3s-listen-port\").Changed && current.Kubernetes.Port > 0 {\n\t\tstartCmdArgs.Kubernetes.Port = current.Kubernetes.Port\n\t}\n\tif !cmd.Flag(\"runtime\").Changed {\n\t\tstartCmdArgs.Runtime = current.Runtime\n\t}\n\tif util.MacOS13OrNewerOnArm() {\n\t\tif !cmd.Flag(\"model-runner\").Changed {\n\t\t\tstartCmdArgs.ModelRunner = current.ModelRunner\n\t\t}\n\t}\n\tif !cmd.Flag(\"cpus\").Changed {\n\t\tstartCmdArgs.CPU = current.CPU\n\t}\n\tif !cmd.Flag(\"cpu-type\").Changed {\n\t\tstartCmdArgs.CPUType = current.CPUType\n\t}\n\tif !cmd.Flag(\"memory\").Changed {\n\t\tstartCmdArgs.Memory = current.Memory\n\t}\n\tif !cmd.Flag(\"mount\").Changed {\n\t\tstartCmdArgs.Mounts = current.Mounts\n\t}\n\tif !cmd.Flag(\"mount-type\").Changed {\n\t\tstartCmdArgs.MountType = current.MountType\n\t}\n\tif !cmd.Flag(\"mount-inotify\").Changed {\n\t\tstartCmdArgs.MountINotify = current.MountINotify\n\t}\n\tif !cmd.Flag(\"ssh-agent\").Changed {\n\t\tstartCmdArgs.ForwardAgent = current.ForwardAgent\n\t}\n\tif !cmd.Flag(\"ssh-config\").Changed {\n\t\tstartCmdArgs.SSHConfig = current.SSHConfig\n\t}\n\tif !cmd.Flag(\"ssh-port\").Changed {\n\t\tstartCmdArgs.SSHPort = current.SSHPort\n\t}\n\tif !cmd.Flag(\"port-forwarder\").Changed {\n\t\tstartCmdArgs.PortForwarder = current.PortForwarder\n\t}\n\tif !cmd.Flag(\"dns\").Changed {\n\t\tstartCmdArgs.Network.DNSResolvers = current.Network.DNSResolvers\n\t}\n\tif !cmd.Flag(\"dns-host\").Changed {\n\t\tstartCmdArgs.Network.DNSHosts = current.Network.DNSHosts\n\t}\n\tif !cmd.Flag(\"gateway-address\").Changed {\n\t\tstartCmdArgs.Network.GatewayAddress = current.Network.GatewayAddress\n\t}\n\tif !cmd.Flag(\"env\").Changed {\n\t\tstartCmdArgs.Env = current.Env\n\t}\n\tif !cmd.Flag(\"hostname\").Changed {\n\t\tstartCmdArgs.Hostname = current.Hostname\n\t}\n\tif !cmd.Flag(\"activate\").Changed {\n\t\tif current.ActivateRuntime != nil { // backward compatibility for `activate`\n\t\t\tstartCmdArgs.ActivateRuntime = current.ActivateRuntime\n\t\t}\n\t}\n\tif !cmd.Flag(\"binfmt\").Changed {\n\t\tif current.Binfmt != nil {\n\t\t\tstartCmdArgs.Binfmt = current.Binfmt\n\t\t}\n\t}\n\tif !cmd.Flag(\"network-host-addresses\").Changed {\n\t\tstartCmdArgs.Network.HostAddresses = current.Network.HostAddresses\n\t}\n\tif util.MacOS() {\n\t\tif !cmd.Flag(\"network-address\").Changed {\n\t\t\tstartCmdArgs.Network.Address = current.Network.Address\n\t\t}\n\t\tif !cmd.Flag(\"network-mode\").Changed {\n\t\t\tstartCmdArgs.Network.Mode = current.Network.Mode\n\t\t}\n\t\tif !cmd.Flag(\"network-interface\").Changed {\n\t\t\tstartCmdArgs.Network.BridgeInterface = current.Network.BridgeInterface\n\t\t}\n\t\tif !cmd.Flag(\"network-preferred-route\").Changed {\n\t\t\tstartCmdArgs.Network.PreferredRoute = current.Network.PreferredRoute\n\t\t}\n\t\tif util.MacOS13OrNewer() {\n\t\t\tif !cmd.Flag(\"vm-type\").Changed {\n\t\t\t\tstartCmdArgs.VMType = current.VMType\n\t\t\t}\n\t\t}\n\t\tif util.MacOS13OrNewerOnArm() {\n\t\t\tif !cmd.Flag(\"vz-rosetta\").Changed {\n\t\t\t\tstartCmdArgs.VZRosetta = current.VZRosetta\n\t\t\t}\n\t\t}\n\t\tif util.MacOSNestedVirtualizationSupported() {\n\t\t\tif !cmd.Flag(\"nested-virtualization\").Changed {\n\t\t\t\tstartCmdArgs.NestedVirtualization = current.NestedVirtualization\n\t\t\t}\n\t\t}\n\t}\n\n\tsetFixedConfigs(&startCmdArgs.Config)\n}\n\n// editConfigFile launches an editor to edit the config file.\nfunc editConfigFile() (config.Config, error) {\n\tvar c config.Config\n\n\t// preserve the current file in case the user terminates\n\tcurrentFile, err := os.ReadFile(config.CurrentProfile().File())\n\tif err != nil {\n\t\treturn c, fmt.Errorf(\"error reading config file: %w\", err)\n\t}\n\n\t// prepend the config file with termination instruction\n\tabort, err := embedded.ReadString(\"defaults/abort.yaml\")\n\tif err != nil {\n\t\tlog.Warnln(fmt.Errorf(\"unable to read embedded file: %w\", err))\n\t}\n\n\ttmpFile, err := waitForUserEdit(startCmdArgs.Flags.Editor, []byte(abort+\"\\n\"+string(currentFile)))\n\tif err != nil {\n\t\treturn c, fmt.Errorf(\"error editing config file: %w\", err)\n\t}\n\n\t// if file is empty, abort\n\tif tmpFile == \"\" {\n\t\treturn c, fmt.Errorf(\"empty file, startup aborted\")\n\t}\n\n\tdefer func() {\n\t\t_ = os.Remove(tmpFile)\n\t}()\n\tif startCmdArgs.Flags.SaveConfig {\n\t\tif err := configmanager.SaveFromFile(tmpFile); err != nil {\n\t\t\treturn c, err\n\t\t}\n\t}\n\treturn configmanager.LoadFrom(tmpFile)\n}\n\nfunc start(app app.App, conf config.Config) error {\n\tif err := app.Start(conf); err != nil {\n\t\treturn err\n\t}\n\tif startCmdArgs.Flags.Foreground {\n\t\treturn awaitForInterruption(app)\n\t}\n\treturn nil\n}\n\nfunc awaitForInterruption(app app.App) error {\n\tc := make(chan os.Signal, 1)\n\tsignal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)\n\n\tlog.Println(\"keeping Colima in the foreground, press ctrl+c to exit...\")\n\n\tsig := <-c\n\tlog.Infof(\"interrupted by: %v\", sig)\n\n\tif err := app.Stop(false); err != nil {\n\t\tlog.Errorf(\"error stopping: %v\", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/start_test.go",
    "content": "package cmd\n\nimport (\n\t\"reflect\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/abiosoft/colima/config\"\n)\n\nfunc Test_mountsFromFlag(t *testing.T) {\n\ttests := []struct {\n\t\tmounts []string\n\t\twant   []config.Mount\n\t}{\n\t\t{\n\t\t\tmounts: []string{\n\t\t\t\t\"~:w\",\n\t\t\t},\n\t\t\twant: []config.Mount{\n\t\t\t\t{Location: \"~\", Writable: true},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmounts: []string{\n\t\t\t\t\"~\",\n\t\t\t},\n\t\t\twant: []config.Mount{\n\t\t\t\t{Location: \"~\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmounts: []string{\n\t\t\t\t\"/home/users\", \"/home/another:w\", \"/tmp\",\n\t\t\t},\n\t\t\twant: []config.Mount{\n\t\t\t\t{Location: \"/home/users\"},\n\t\t\t\t{Location: \"/home/another\", Writable: true},\n\t\t\t\t{Location: \"/tmp\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmounts: []string{\n\t\t\t\t\"/home/users:/home/users\", \"/home/another:w\", \"/tmp:/users/tmp\", \"/tmp:/users/tmp:w\",\n\t\t\t},\n\t\t\twant: []config.Mount{\n\t\t\t\t{Location: \"/home/users\", MountPoint: \"/home/users\"},\n\t\t\t\t{Location: \"/home/another\", Writable: true},\n\t\t\t\t{Location: \"/tmp\", MountPoint: \"/users/tmp\"},\n\t\t\t\t{Location: \"/tmp\", MountPoint: \"/users/tmp\", Writable: true},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tmounts: []string{\n\t\t\t\t\"none\",\n\t\t\t},\n\t\t\twant: nil,\n\t\t},\n\t}\n\tfor i, tt := range tests {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tif got := mountsFromFlag(tt.mounts); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"mountsFromFlag() = %+v, want %+v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/status.go",
    "content": "package cmd\n\nimport (\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar statusCmdArgs struct {\n\textended bool\n\tjson     bool\n}\n\n// statusCmd represents the status command\nvar statusCmd = &cobra.Command{\n\tUse:   \"status [profile]\",\n\tShort: \"show the status of Colima\",\n\tLong:  `Show the status of Colima`,\n\tArgs:  cobra.MaximumNArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\treturn newApp().Status(statusCmdArgs.extended, statusCmdArgs.json)\n\t},\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(statusCmd)\n\n\tstatusCmd.Flags().BoolVarP(&statusCmdArgs.extended, \"extended\", \"e\", false, \"include additional details\")\n\tstatusCmd.Flags().BoolVarP(&statusCmdArgs.json, \"json\", \"j\", false, \"print json output\")\n}\n"
  },
  {
    "path": "cmd/stop.go",
    "content": "package cmd\n\nimport (\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar stopCmdArgs struct {\n\tforce bool\n}\n\n// stopCmd represents the stop command\nvar stopCmd = &cobra.Command{\n\tUse:   \"stop [profile]\",\n\tShort: \"stop Colima\",\n\tLong: `Stop Colima to free up resources.\n\nThe state of the VM is persisted at stop. A start afterwards\nshould return it back to its previous state.`,\n\tArgs: cobra.MaximumNArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\treturn newApp().Stop(stopCmdArgs.force)\n\t},\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(stopCmd)\n\n\tstopCmd.Flags().BoolVarP(&stopCmdArgs.force, \"force\", \"f\", false, \"stop without graceful shutdown\")\n}\n"
  },
  {
    "path": "cmd/template.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/config/configmanager\"\n\t\"github.com/abiosoft/colima/embedded\"\n\t\"github.com/spf13/cobra\"\n)\n\n// templateCmd represents the template command\nvar templateCmd = &cobra.Command{\n\tUse:     \"template\",\n\tAliases: []string{\"tmpl\", \"tpl\", \"t\"},\n\tShort:   \"edit the template for default configurations\",\n\tLong: `Edit the template for default configurations of new instances.\n`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif templateCmdArgs.Print {\n\t\t\tfmt.Println(templateFile())\n\t\t\treturn nil\n\t\t}\n\t\t// there are unwarranted []byte to string overheads.\n\t\t// not a big deal in this case\n\n\t\tabort, err := embedded.ReadString(\"defaults/abort.yaml\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading embedded file: %w\", err)\n\t\t}\n\t\tinfo, err := embedded.ReadString(\"defaults/template.yaml\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading embedded file: %w\", err)\n\t\t}\n\t\ttemplate, err := templateFileOrDefault()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading template file: %w\", err)\n\t\t}\n\n\t\ttmpFile, err := waitForUserEdit(templateCmdArgs.Editor, []byte(abort+\"\\n\"+info+\"\\n\"+template))\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error editing template file: %w\", err)\n\t\t}\n\t\tif tmpFile == \"\" {\n\t\t\treturn fmt.Errorf(\"empty file, template edit aborted\")\n\t\t}\n\t\tdefer func() {\n\t\t\t_ = os.Remove(tmpFile)\n\t\t}()\n\n\t\t// load and resave template to ensure the format is correct\n\t\tcf, err := configmanager.LoadFrom(tmpFile)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error in template: %w\", err)\n\t\t}\n\t\tif err := configmanager.SaveToFile(cf, templateFile()); err != nil {\n\t\t\treturn fmt.Errorf(\"error saving template: %w\", err)\n\t\t}\n\n\t\tlog.Println(\"configurations template saved\")\n\n\t\treturn nil\n\t},\n}\n\nfunc templateFile() string { return filepath.Join(config.TemplatesDir(), \"default.yaml\") }\n\nfunc templateFileOrDefault() (string, error) {\n\ttFile := templateFile()\n\tif _, err := os.Stat(tFile); err == nil {\n\t\tb, err := os.ReadFile(tFile)\n\t\tif err == nil {\n\t\t\treturn string(b), nil\n\t\t}\n\t}\n\n\treturn embedded.ReadString(\"defaults/colima.yaml\")\n}\n\nvar templateCmdArgs struct {\n\tEditor string\n\tPrint  bool\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(templateCmd)\n\n\ttemplateCmd.Flags().StringVar(&templateCmdArgs.Editor, \"editor\", \"\", `editor to use for edit e.g. vim, nano, code (default \"$EDITOR\" env var)`)\n\ttemplateCmd.Flags().BoolVar(&templateCmdArgs.Print, \"print\", false, `print out the configuration file path, without editing`)\n}\n"
  },
  {
    "path": "cmd/update.go",
    "content": "package cmd\n\nimport (\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/spf13/cobra\"\n)\n\n// statusCmd represents the status command\nvar updateCmd = &cobra.Command{\n\tUse:     \"update [profile]\",\n\tAliases: []string{\"u\", \"up\"},\n\tShort:   \"update the container runtime\",\n\tLong:    `Update the current container runtime.`,\n\tArgs:    cobra.MaximumNArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\treturn newApp().Update()\n\t},\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(updateCmd)\n}\n"
  },
  {
    "path": "cmd/util.go",
    "content": "package cmd\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/app\"\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc newApp() app.App {\n\tcolimaApp, err := app.New()\n\tif err != nil {\n\t\tlogrus.Fatal(\"Error: \", err)\n\t}\n\treturn colimaApp\n}\n\n// waitForUserEdit launches a temporary file with content using editor,\n// and waits for the user to close the editor.\n// It returns the filename (if saved), empty file name (if aborted), and an error (if any).\nfunc waitForUserEdit(editor string, content []byte) (string, error) {\n\ttmp, err := os.CreateTemp(\"\", \"colima-*.yaml\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error creating temporary file: %w\", err)\n\t}\n\tif _, err := tmp.Write(content); err != nil {\n\t\treturn \"\", fmt.Errorf(\"error writing temporary file: %w\", err)\n\t}\n\tif err := tmp.Close(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"error closing temporary file: %w\", err)\n\t}\n\n\tif err := launchEditor(editor, tmp.Name()); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// aborted\n\tif f, err := os.ReadFile(tmp.Name()); err == nil && len(bytes.TrimSpace(f)) == 0 {\n\t\treturn \"\", nil\n\t}\n\n\treturn tmp.Name(), nil\n}\n\nvar editors = []string{\n\t\"vim\",\n\t\"code --wait --new-window\",\n\t\"nano\",\n}\n\nfunc launchEditor(editor string, file string) error {\n\tif editor != \"\" {\n\t\tlog.Println(\"editing in\", editor)\n\t}\n\t// if not specified, prefer vscode if this a vscode terminal\n\tif editor == \"\" {\n\t\tif os.Getenv(\"TERM_PROGRAM\") == \"vscode\" {\n\t\t\tlog.Println(\"vscode detected, editing in vscode\")\n\t\t\teditor = \"code --wait\"\n\t\t}\n\t}\n\n\t// if not found, check the EDITOR env var\n\tif editor == \"\" {\n\t\tif e := os.Getenv(\"EDITOR\"); e != \"\" {\n\t\t\tlog.Println(\"editing in\", e, \"from\", \"$EDITOR environment variable\")\n\t\t\teditor = e\n\t\t}\n\t}\n\n\t// if not found, check the preferred editors\n\tif editor == \"\" {\n\t\tfor _, e := range editors {\n\t\t\ts := strings.Fields(e)\n\t\t\tif _, err := exec.LookPath(s[0]); err == nil {\n\t\t\t\teditor = e\n\t\t\t\tlog.Println(\"editing in\", e)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\t// if still not found, abort\n\tif editor == \"\" {\n\t\treturn fmt.Errorf(\"no editor found in $PATH, kindly set $EDITOR environment variable and try again\")\n\t}\n\n\t// some editors need the wait flag, let us add it if the user has not.\n\tswitch editor {\n\tcase \"code\", \"code-insiders\", \"code-oss\", \"codium\", \"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code\":\n\t\teditor = strconv.Quote(editor) + \" --wait --new-window\"\n\tcase \"mate\", \"/Applications/TextMate 2.app/Contents/MacOS/mate\", \"/Applications/TextMate 2.app/Contents/MacOS/TextMate\":\n\t\teditor = strconv.Quote(editor) + \" --wait\"\n\t}\n\n\treturn cli.CommandInteractive(\"sh\", \"-c\", editor+\" \"+file).Run()\n}\n"
  },
  {
    "path": "cmd/version.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/abiosoft/colima/app\"\n\t\"github.com/abiosoft/colima/cmd/root\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/model\"\n\t\"github.com/abiosoft/colima/store\"\n\t\"github.com/spf13/cobra\"\n)\n\n// versionCmd represents the version command\nvar versionCmd = &cobra.Command{\n\tUse:   \"version [profile]\",\n\tShort: \"print the version of Colima\",\n\tLong:  `Print the version of Colima`,\n\tArgs:  cobra.MaximumNArgs(1),\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tversion := config.AppVersion()\n\t\tfmt.Println(config.AppName, \"version\", version.Version)\n\t\tfmt.Println(\"git commit:\", version.Revision)\n\n\t\tif colimaApp, err := app.New(); err == nil {\n\t\t\t_ = colimaApp.Version()\n\n\t\t\t// Show AI model runner version if provisioned\n\t\t\ts, _ := store.Load()\n\t\t\tif s.RamalamaProvisioned {\n\t\t\t\tif modelVersion := model.GetRamalamaVersion(); modelVersion != \"\" {\n\t\t\t\t\tfmt.Println()\n\t\t\t\t\tfmt.Println(\"AI model runner\")\n\t\t\t\t\tfmt.Println(\"version:\", modelVersion)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n}\n\nfunc init() {\n\troot.Cmd().AddCommand(versionCmd)\n}\n"
  },
  {
    "path": "colima.nix",
    "content": "{ pkgs ? import <nixpkgs> }:\n\nwith pkgs;\n\nbuildGo123Module {\n  name = \"colima\";\n  pname = \"colima\";\n  src = ./.;\n  nativeBuildInputs = [ installShellFiles makeWrapper git ];\n  vendorHash = \"sha256-ZwgzKCOEhgKK2LNRLjnWP6qHI4f6OGORvt3CREJf55I=\";\n  CGO_ENABLED = 1;\n\n  subPackages = [ \"cmd/colima\" ];\n\n  # `nix-build` has .git folder but `nix build` does not, this caters for both cases\n  preConfigure = ''\n    export VERSION=\"$(git describe --tags --always || echo nix-build-at-\"$(date +%s)\")\"\n    export REVISION=\"$(git rev-parse HEAD || echo nix-unknown)\"\n    ldflags=\"-X github.com/abiosoft/colima/config.appVersion=$VERSION\n              -X github.com/abiosoft/colima/config.revision=$REVISION\"\n  '';\n\n  postInstall = ''\n    wrapProgram $out/bin/colima \\\n      --prefix PATH : ${lib.makeBinPath [ qemu lima ]}\n    installShellCompletion --cmd colima \\\n      --bash <($out/bin/colima completion bash) \\\n      --fish <($out/bin/colima completion fish) \\\n      --zsh <($out/bin/colima completion zsh)\n  '';\n}\n\n"
  },
  {
    "path": "config/config.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/osutil\"\n)\n\nconst (\n\tAppName    = \"colima\"\n\tenvProfile = \"COLIMA_PROFILE\" // environment variable for profile name\n)\n\n// VersionInfo is the application version info.\ntype VersionInfo struct {\n\tVersion  string\n\tRevision string\n}\n\nfunc AppVersion() VersionInfo { return VersionInfo{Version: appVersion, Revision: revision} }\nfunc EnvProfile() string      { return osutil.EnvVar(envProfile).Val() }\n\nvar (\n\tappVersion = \"development\"\n\trevision   = \"unknown\"\n)\n\n// Config is the application config.\ntype Config struct {\n\tCPU      int               `yaml:\"cpu,omitempty\"`\n\tDisk     int               `yaml:\"disk,omitempty\"`\n\tRootDisk int               `yaml:\"rootDisk,omitempty\"`\n\tMemory   float32           `yaml:\"memory,omitempty\"`\n\tArch     string            `yaml:\"arch,omitempty\"`\n\tCPUType  string            `yaml:\"cpuType,omitempty\"`\n\tNetwork  Network           `yaml:\"network,omitempty\"`\n\tEnv      map[string]string `yaml:\"env,omitempty\"` // environment variables\n\tHostname string            `yaml:\"hostname\"`\n\n\t// SSH\n\tSSHPort      int  `yaml:\"sshPort,omitempty\"`\n\tForwardAgent bool `yaml:\"forwardAgent,omitempty\"`\n\tSSHConfig    bool `yaml:\"sshConfig,omitempty\"` // config generation\n\n\t// VM\n\tVMType               string `yaml:\"vmType,omitempty\"`\n\tVZRosetta            bool   `yaml:\"rosetta,omitempty\"`\n\tBinfmt               *bool  `yaml:\"binfmt,omitempty\"`\n\tNestedVirtualization bool   `yaml:\"nestedVirtualization,omitempty\"`\n\tDiskImage            string `yaml:\"diskImage,omitempty\"`\n\tPortForwarder        string `yaml:\"portForwarder,omitempty\"` // \"ssh\", \"grpc\"\n\n\t// volume mounts\n\tMounts       []Mount `yaml:\"mounts,omitempty\"`\n\tMountType    string  `yaml:\"mountType,omitempty\"`\n\tMountINotify bool    `yaml:\"mountInotify,omitempty\"`\n\n\t// Runtime is one of docker, containerd.\n\tRuntime         string `yaml:\"runtime,omitempty\"`\n\tActivateRuntime *bool  `yaml:\"autoActivate,omitempty\"`\n\n\t// ModelRunner is the AI model runner (docker, ramalama).\n\tModelRunner string `yaml:\"modelRunner,omitempty\"`\n\n\t// Kubernetes configuration\n\tKubernetes Kubernetes `yaml:\"kubernetes,omitempty\"`\n\n\t// Docker configuration\n\tDocker map[string]any `yaml:\"docker,omitempty\"`\n\n\t// provision scripts\n\tProvision []Provision `yaml:\"provision,omitempty\"`\n}\n\n// Kubernetes is kubernetes configuration\ntype Kubernetes struct {\n\tEnabled bool     `yaml:\"enabled\"`\n\tVersion string   `yaml:\"version\"`\n\tK3sArgs []string `yaml:\"k3sArgs\"`\n\tPort    int      `yaml:\"port,omitempty\"`\n}\n\n// Network is VM network configuration\ntype Network struct {\n\tAddress         bool              `yaml:\"address\"`\n\tDNSResolvers    []net.IP          `yaml:\"dns\"`\n\tDNSHosts        map[string]string `yaml:\"dnsHosts\"`\n\tHostAddresses   bool              `yaml:\"hostAddresses\"`\n\tMode            string            `yaml:\"mode\"` // shared, bridged\n\tBridgeInterface string            `yaml:\"interface\"`\n\tPreferredRoute  bool              `yaml:\"preferredRoute\"`\n\tGatewayAddress  net.IP            `yaml:\"gatewayAddress\"`\n}\n\n// Mount is volume mount\ntype Mount struct {\n\tLocation   string `yaml:\"location\"`\n\tMountPoint string `yaml:\"mountPoint,omitempty\"`\n\tWritable   bool   `yaml:\"writable\"`\n}\n\n// Provision modes managed by Colima (not passed to Lima).\nconst (\n\tProvisionModeAfterBoot = \"after-boot\"\n\tProvisionModeReady     = \"ready\"\n)\n\ntype Provision struct {\n\tMode   string `yaml:\"mode\"`\n\tScript string `yaml:\"script\"`\n}\n\n// IsColimaMode returns true if the provision script is managed by Colima\n// rather than being passed to Lima.\nfunc (p Provision) IsColimaMode() bool {\n\treturn p.Mode == ProvisionModeAfterBoot || p.Mode == ProvisionModeReady\n}\n\nfunc (c Config) MountsOrDefault() []Mount {\n\t// explicit empty list means mount home directory (matches yaml.go)\n\tif c.Mounts != nil && len(c.Mounts) == 0 {\n\t\treturn []Mount{\n\t\t\t{Location: util.HomeDir(), Writable: true},\n\t\t}\n\t}\n\n\t// nil means no mounts, non-empty means user-specified mounts\n\treturn c.Mounts\n}\n\n// AutoActivate returns if auto-activation of host client config is enabled.\nfunc (c Config) AutoActivate() bool {\n\tif c.ActivateRuntime == nil {\n\t\treturn true\n\t}\n\treturn *c.ActivateRuntime\n}\n\n// Empty checks if the configuration is empty.\nfunc (c Config) Empty() bool { return c.Runtime == \"\" } // this may be better but not really needed.\n\nfunc (c Config) DriverLabel() string {\n\tif util.MacOS13OrNewer() && c.VMType == \"vz\" {\n\t\treturn \"macOS Virtualization.Framework\"\n\t} else if util.MacOS13OrNewerOnArm() && c.VMType == \"krunkit\" {\n\t\treturn \"Krunkit\"\n\t}\n\treturn \"QEMU\"\n}\n\n// Disk is an instance disk size\ntype Disk int\n\n// GiB returns the string represent of the disk in GiB.\nfunc (d Disk) GiB() string { return fmt.Sprintf(\"%dGiB\", d) }\n\n// Int returns the disk size in bytes.\nfunc (d Disk) Int() int64 { return 1024 * 1024 * 1024 * int64(d) }\n\n// CtxKey returns the context key for config.\nfunc CtxKey() any {\n\treturn struct{ name string }{name: \"colima_config\"}\n}\n"
  },
  {
    "path": "config/configmanager/configmanager.go",
    "content": "package configmanager\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/yamlutil\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// Save saves the config.\nfunc Save(c config.Config) error {\n\treturn yamlutil.Save(c, config.CurrentProfile().File())\n}\n\n// SaveFromFile loads configuration from file and save as config.\nfunc SaveFromFile(file string) error {\n\tc, err := LoadFrom(file)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn Save(c)\n}\n\n// SaveToFile saves configuration to file.\nfunc SaveToFile(c config.Config, file string) error {\n\treturn yamlutil.Save(c, file)\n}\n\n// LoadFrom loads config from file.\nfunc LoadFrom(file string) (config.Config, error) {\n\tvar c config.Config\n\tb, err := os.ReadFile(file)\n\tif err != nil {\n\t\treturn c, fmt.Errorf(\"could not load config from file: %w\", err)\n\t}\n\n\terr = yaml.Unmarshal(b, &c)\n\tif err != nil {\n\t\treturn c, fmt.Errorf(\"could not load config from file: %w\", err)\n\t}\n\n\treturn c, nil\n}\n\n// ValidateConfig validates config before we use it\nfunc ValidateConfig(c config.Config) error {\n\tvalidMountTypes := map[string]bool{\"9p\": true, \"sshfs\": true}\n\tvalidPortForwarders := map[string]bool{\"grpc\": true, \"ssh\": true, \"none\": true}\n\n\tif util.MacOS13OrNewer() {\n\t\tvalidMountTypes[\"virtiofs\"] = true\n\t}\n\tif _, ok := validMountTypes[c.MountType]; !ok {\n\t\treturn fmt.Errorf(\"invalid mountType: '%s'\", c.MountType)\n\t}\n\tvalidVMTypes := map[string]bool{\"qemu\": true}\n\tif util.MacOS13OrNewer() {\n\t\tvalidVMTypes[\"vz\"] = true\n\t}\n\tif util.MacOS13OrNewerOnArm() {\n\t\tvalidVMTypes[\"krunkit\"] = true\n\t}\n\tif c.VMType == \"krunkit\" && !util.MacOS13OrNewerOnArm() {\n\t\treturn fmt.Errorf(\"vmType 'krunkit' is only available on macOS with Apple Silicon\")\n\t}\n\tif _, ok := validVMTypes[c.VMType]; !ok {\n\t\treturn fmt.Errorf(\"invalid vmType: '%s'\", c.VMType)\n\t}\n\tif c.VMType == \"qemu\" {\n\t\tif err := util.AssertQemuImg(); err != nil {\n\t\t\treturn fmt.Errorf(\"cannot use vmType: '%s', error: %w\", c.VMType, err)\n\t\t}\n\t}\n\tif c.VMType == \"krunkit\" {\n\t\tif err := util.AssertKrunkit(); err != nil {\n\t\t\treturn fmt.Errorf(\"cannot use vmType: '%s', error: %w\", c.VMType, err)\n\t\t}\n\t}\n\n\tif c.DiskImage != \"\" {\n\t\tif strings.HasPrefix(c.DiskImage, \"http://\") || strings.HasPrefix(c.DiskImage, \"https://\") {\n\t\t\treturn fmt.Errorf(\"cannot use diskImage: remote URLs not supported, only local files can be specified\")\n\t\t}\n\t}\n\n\tif _, ok := validPortForwarders[c.PortForwarder]; !ok {\n\t\treturn fmt.Errorf(\"invalid port forwarder: '%s'\", c.PortForwarder)\n\t}\n\n\tif c.Network.GatewayAddress != nil {\n\t\tif err := validateGatewayAddress(c.Network.GatewayAddress); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Load loads the config.\n// Error is only returned if the config file exists but could not be loaded.\n// No error is returned if the config file does not exist.\nfunc Load() (c config.Config, err error) {\n\tf := config.CurrentProfile().File()\n\tif _, err := os.Stat(f); err != nil {\n\t\treturn c, nil\n\t}\n\n\treturn LoadFrom(f)\n}\n\n// LoadInstance is like Load but returns the config of the currently running instance.\nfunc LoadInstance() (config.Config, error) {\n\treturn LoadFrom(config.CurrentProfile().StateFile())\n}\n\n// Teardown deletes the config.\nfunc Teardown() error {\n\tdir := config.CurrentProfile().ConfigDir()\n\tif _, err := os.Stat(dir); err == nil {\n\t\treturn os.RemoveAll(dir)\n\t}\n\treturn nil\n}\n\n// Validates that gateway is a valid IPv4 address and that the last octet is “2”.\n// Lima uses the last octet as 2 for gateways.\nfunc validateGatewayAddress(gateway net.IP) error {\n\tip4 := gateway.To4()\n\tif ip4 == nil {\n\t\treturn fmt.Errorf(\"gateway %q is not IPv4\", gateway)\n\t}\n\n\t// Check last octet\n\tif ip4[3] != 2 {\n\t\treturn fmt.Errorf(\"the last octet of gateway %q is not 2\", gateway)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "config/files.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/fsutil\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// requiredDir is a directory that must exist on the filesystem\ntype requiredDir struct {\n\tonce sync.Once\n\n\t// dir is a func to enable deferring the value of the directory\n\t// until execution time.\n\t// if dir() returns an error, a fatal error is triggered.\n\tdir func() (string, error)\n\n\tcomputedDir *string\n}\n\n// Dir returns the directory path.\n// It ensures the directory is created on the filesystem by calling\n// `mkdir` prior to returning the directory path.\nfunc (r *requiredDir) Dir() string {\n\tif r.computedDir != nil {\n\t\treturn *r.computedDir\n\t}\n\n\tdir, err := r.dir()\n\tif err != nil {\n\t\tlogrus.Fatal(fmt.Errorf(\"cannot fetch required directory: %w\", err))\n\t}\n\n\tr.once.Do(func() {\n\t\tif err := fsutil.MkdirAll(dir, 0755); err != nil {\n\t\t\tlogrus.Fatal(fmt.Errorf(\"cannot make required directory: %w\", err))\n\t\t}\n\t})\n\n\tr.computedDir = &dir\n\treturn dir\n}\n\nvar (\n\tconfigBaseDir = requiredDir{\n\t\tdir: func() (string, error) {\n\t\t\t// colima home explicit config\n\t\t\tdir := os.Getenv(\"COLIMA_HOME\")\n\t\t\tif _, err := os.Stat(dir); err == nil {\n\t\t\t\treturn dir, nil\n\t\t\t}\n\n\t\t\t// user home directory\n\t\t\thomeDir, err := os.UserHomeDir()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\t// colima's config directory based on home directory\n\t\t\tdir = filepath.Join(homeDir, \".colima\")\n\t\t\t// validate existence of colima's config directory\n\t\t\t_, err = os.Stat(dir)\n\n\t\t\t// extra xdg config directory\n\t\t\txdgDir, xdg := os.LookupEnv(\"XDG_CONFIG_HOME\")\n\n\t\t\tif err == nil {\n\t\t\t\t// ~/.colima is found but xdg dir is set\n\t\t\t\tif xdg {\n\t\t\t\t\tlogrus.Warnln(\"found ~/.colima, ignoring $XDG_CONFIG_HOME...\")\n\t\t\t\t\tlogrus.Warnln(\"delete ~/.colima to use $XDG_CONFIG_HOME as config directory\")\n\t\t\t\t\tlogrus.Warnf(\"or run `mv ~/.colima \\\"%s\\\"`\", filepath.Join(xdgDir, \"colima\"))\n\t\t\t\t}\n\t\t\t\treturn dir, nil\n\t\t\t} else {\n\t\t\t\t// ~/.colima is missing and xdg dir is set\n\t\t\t\tif xdg {\n\t\t\t\t\treturn filepath.Join(xdgDir, \"colima\"), nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// macOS users are accustomed to ~/.colima\n\t\t\tif util.MacOS() {\n\t\t\t\treturn dir, nil\n\t\t\t}\n\n\t\t\t// other environments fall back to user config directory\n\t\t\tdir, err = os.UserConfigDir()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\n\t\t\treturn filepath.Join(dir, \"colima\"), nil\n\t\t},\n\t}\n\n\tcacheDir = requiredDir{\n\t\tdir: func() (string, error) {\n\t\t\tif dir := os.Getenv(\"COLIMA_CACHE_HOME\"); dir != \"\" {\n\t\t\t\treturn dir, nil\n\t\t\t}\n\n\t\t\tif dir := os.Getenv(\"XDG_CACHE_HOME\"); dir != \"\" {\n\t\t\t\treturn filepath.Join(dir, \"colima\"), nil\n\t\t\t}\n\t\t\t// else\n\t\t\tdir, err := os.UserCacheDir()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\treturn filepath.Join(dir, \"colima\"), nil\n\t\t},\n\t}\n\n\ttemplatesDir = requiredDir{\n\t\tdir: func() (string, error) {\n\t\t\tdir, err := configBaseDir.dir()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\treturn filepath.Join(dir, \"_templates\"), nil\n\t\t},\n\t}\n\n\tlimaDir = requiredDir{\n\t\tdir: func() (string, error) {\n\t\t\t// if LIMA_HOME env var is set, obey it.\n\t\t\tif dir := os.Getenv(\"LIMA_HOME\"); dir != \"\" {\n\t\t\t\treturn dir, nil\n\t\t\t}\n\n\t\t\tdir, err := configBaseDir.dir()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\treturn filepath.Join(dir, \"_lima\"), nil\n\t\t},\n\t}\n\n\tstoreDir = requiredDir{\n\t\tdir: func() (string, error) {\n\t\t\tdir, err := configBaseDir.dir()\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", err\n\t\t\t}\n\t\t\treturn filepath.Join(dir, \"_store\"), nil\n\t\t},\n\t}\n)\n\n// CacheDir returns the cache directory.\nfunc CacheDir() string { return cacheDir.Dir() }\n\n// TemplatesDir returns the templates' directory.\nfunc TemplatesDir() string { return templatesDir.Dir() }\n\n// LimaDir returns Lima directory.\nfunc LimaDir() string { return limaDir.Dir() }\n\nconst configFileName = \"colima.yaml\"\n\n// SSHConfigFile returns the path to generated ssh config.\nfunc SSHConfigFile() string { return filepath.Join(configBaseDir.Dir(), \"ssh_config\") }\n"
  },
  {
    "path": "config/profile.go",
    "content": "package config\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nvar profile = &Profile{ID: AppName, DisplayName: AppName, ShortName: \"default\"}\n\n// SetProfile sets the profile name for the application.\n// This is an avenue to test Colima without breaking an existing stable setup.\n// Not perfect, but good enough for testing.\nfunc SetProfile(profileName string) {\n\tprofile = ProfileFromName(profileName)\n\tprofile.Changed = true\n}\n\n// ProfileFromName retrieves profile given name.\nfunc ProfileFromName(name string) *Profile {\n\tvar i Profile\n\n\tswitch name {\n\tcase \"\", AppName, \"default\":\n\t\ti.ID = AppName\n\t\ti.DisplayName = AppName\n\t\ti.ShortName = \"default\"\n\t\treturn &i\n\t}\n\n\t// sanitize\n\tname = strings.TrimPrefix(name, \"colima-\")\n\n\t// if custom profile is specified,\n\t// use a prefix to prevent possible name clashes\n\ti.ID = \"colima-\" + name\n\ti.DisplayName = \"colima [profile=\" + name + \"]\"\n\ti.ShortName = name\n\treturn &i\n}\n\n// CurrentProfile returns the current running profile.\nfunc CurrentProfile() *Profile { return profile }\n\n// Profile is colima profile.\ntype Profile struct {\n\tID          string\n\tDisplayName string\n\tShortName   string\n\n\tChanged bool // indicates if the profile has been changed\n\n\tconfigDir *requiredDir\n}\n\n// ConfigDir returns the configuration directory.\nfunc (p *Profile) ConfigDir() string {\n\tif p.configDir == nil {\n\t\tp.configDir = &requiredDir{\n\t\t\tdir: func() (string, error) {\n\t\t\t\treturn filepath.Join(configBaseDir.Dir(), p.ShortName), nil\n\t\t\t},\n\t\t}\n\t}\n\treturn p.configDir.Dir()\n}\n\n// LimaInstanceDir returns the directory for the Lima instance.\nfunc (p *Profile) LimaInstanceDir() string {\n\treturn filepath.Join(limaDir.Dir(), p.ID)\n}\n\n// File returns the path to the config file.\nfunc (p *Profile) File() string {\n\treturn filepath.Join(p.ConfigDir(), configFileName)\n}\n\n// LimaFile returns the path to the lima config file.\nfunc (p *Profile) LimaFile() string {\n\treturn filepath.Join(p.LimaInstanceDir(), \"lima.yaml\")\n}\n\n// StateFile returns the path to the state file.\nfunc (p *Profile) StateFile() string {\n\treturn filepath.Join(p.LimaInstanceDir(), configFileName)\n}\n\nfunc (p *Profile) StoreFile() string {\n\treturn filepath.Join(storeDir.Dir(), p.ID+\".json\")\n}\n\nvar _ ProfileInfo = (*Profile)(nil)\n\n// ProfileInfo is the information about a profile.\ntype ProfileInfo interface {\n\t// ConfigDir returns the configuration directory.\n\tConfigDir() string\n\n\t// LimaInstanceDir returns the directory for the Lima instance.\n\tLimaInstanceDir() string\n\n\t// File returns the path to the config file.\n\tFile() string\n\n\t// LimaFile returns the path to the lima config file.\n\tLimaFile() string\n\n\t// StateFile returns the path to the state file.\n\tStateFile() string\n\n\t// StoreFile returns the path to the store file.\n\tStoreFile() string\n}\n"
  },
  {
    "path": "core/core.go",
    "content": "package core\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/coreos/go-semver/semver\"\n)\n\nconst limaVersion = \"v0.18.0\" // minimum Lima version supported\n\ntype (\n\thostActions  = environment.HostActions\n\tguestActions = environment.GuestActions\n)\n\n// SetupBinfmt downloads and install binfmt\nfunc SetupBinfmt(host hostActions, guest guestActions, arch environment.Arch) error {\n\tqemuArch := environment.AARCH64\n\tif arch.Value().GoArch() == \"arm64\" {\n\t\tqemuArch = environment.X8664\n\t}\n\n\tinstall := func() error {\n\t\tif err := guest.Run(\"sh\", \"-c\", \"sudo QEMU_PRESERVE_ARGV0=1 /usr/bin/binfmt --install 386,\"+qemuArch.GoArch()); err != nil {\n\t\t\treturn fmt.Errorf(\"error installing binfmt: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n\n\t// validate binfmt\n\tif err := guest.RunQuiet(\"command\", \"-v\", \"binfmt\"); err != nil {\n\t\treturn fmt.Errorf(\"binfmt not found: %w\", err)\n\t}\n\n\treturn install()\n}\n\n// LimaVersionSupported checks if the currently installed Lima version is supported.\nfunc LimaVersionSupported() error {\n\tvar values struct {\n\t\tVersion string `json:\"version\"`\n\t}\n\tvar buf bytes.Buffer\n\tcmd := cli.Command(\"limactl\", \"info\")\n\tcmd.Stdout = &buf\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"error checking Lima version: %w\", err)\n\t}\n\n\tif err := json.NewDecoder(&buf).Decode(&values); err != nil {\n\t\treturn fmt.Errorf(\"error decoding 'limactl info' json: %w\", err)\n\t}\n\t// remove pre-release hyphen\n\tparts := strings.SplitN(values.Version, \"-\", 2)\n\tif len(parts) > 0 {\n\t\tvalues.Version = parts[0]\n\t}\n\n\tif parts[0] == \"HEAD\" {\n\t\tlogrus.Warnf(\"to avoid compatibility issues, ensure lima development version (%s) in use is not lower than %s\", values.Version, limaVersion)\n\t\treturn nil\n\t}\n\n\tmin := semver.New(strings.TrimPrefix(limaVersion, \"v\"))\n\tcurrent, err := semver.NewVersion(strings.TrimPrefix(values.Version, \"v\"))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid semver version for Lima: %w\", err)\n\t}\n\n\tif min.Compare(*current) > 0 {\n\t\treturn fmt.Errorf(\"minimum Lima version supported is %s, current version is %s\", limaVersion, values.Version)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "daemon/daemon.go",
    "content": "package daemon\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/daemon/process\"\n\t\"github.com/abiosoft/colima/daemon/process/inotify\"\n\t\"github.com/abiosoft/colima/daemon/process/vmnet\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/fsutil\"\n\t\"github.com/abiosoft/colima/util/osutil\"\n)\n\n// Manager handles running background processes.\ntype Manager interface {\n\tStart(context.Context, config.Config) error\n\tStop(context.Context, config.Config) error\n\tRunning(context.Context, config.Config) (Status, error)\n\tDependency(ctx context.Context, conf config.Config, name string) (deps process.Dependency, root bool)\n}\n\ntype Status struct {\n\t// Parent process\n\tRunning bool\n\t// Subprocesses\n\tProcesses []processStatus\n}\ntype processStatus struct {\n\tName    string\n\tRunning bool\n\tError   error\n}\n\n// NewManager creates a new process manager.\nfunc NewManager(host environment.HostActions) Manager {\n\treturn &processManager{\n\t\thost: host,\n\t}\n}\n\nfunc CtxKey(s string) any { return struct{ key string }{key: s} }\n\nvar _ Manager = (*processManager)(nil)\n\ntype processManager struct {\n\thost environment.HostActions\n}\n\nfunc (l processManager) Dependency(ctx context.Context, conf config.Config, name string) (deps process.Dependency, root bool) {\n\tprocesses := processesFromConfig(conf)\n\n\tfor _, p := range processes {\n\t\tif p.Name() == name {\n\t\t\treturn process.Dependencies(p)\n\t\t}\n\t}\n\n\treturn process.Dependencies()\n}\n\nfunc (l processManager) init() error {\n\t// dependencies for network\n\tif err := fsutil.MkdirAll(process.Dir(), 0755); err != nil {\n\t\treturn fmt.Errorf(\"error preparing vmnet: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (l processManager) Running(ctx context.Context, conf config.Config) (s Status, err error) {\n\terr = l.host.RunQuiet(osutil.Executable(), \"daemon\", \"status\", config.CurrentProfile().ShortName)\n\tif err != nil {\n\t\treturn\n\t}\n\ts.Running = true\n\n\tctx = context.WithValue(ctx, process.CtxKeyDaemon(), s.Running)\n\n\tfor _, p := range processesFromConfig(conf) {\n\t\tpErr := p.Alive(ctx)\n\t\ts.Processes = append(s.Processes, processStatus{\n\t\t\tName:    p.Name(),\n\t\t\tRunning: pErr == nil,\n\t\t\tError:   pErr,\n\t\t})\n\t}\n\treturn\n}\n\nfunc (l processManager) Start(ctx context.Context, conf config.Config) error {\n\t_ = l.Stop(ctx, conf) // this is safe, nothing is done when not running\n\n\tif err := l.init(); err != nil {\n\t\treturn fmt.Errorf(\"error preparing daemon directory: %w\", err)\n\t}\n\n\targs := []string{osutil.Executable(), \"daemon\", \"start\", config.CurrentProfile().ShortName}\n\n\tif conf.Network.Address {\n\t\targs = append(args, \"--vmnet\")\n\t\targs = append(args, \"--vmnet-mode\", conf.Network.Mode)\n\t\targs = append(args, \"--vmnet-interface\", conf.Network.BridgeInterface)\n\t}\n\tif conf.MountINotify {\n\t\targs = append(args, \"--inotify\")\n\t\targs = append(args, \"--inotify-runtime\", conf.Runtime)\n\t\tfor _, mount := range conf.MountsOrDefault() {\n\t\t\tp, err := util.CleanPath(mount.Location)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error sanitising mount path for inotify: %w\", err)\n\t\t\t}\n\t\t\targs = append(args, \"--inotify-dir\", p)\n\t\t}\n\t}\n\n\tif cli.Settings.Verbose {\n\t\targs = append(args, \"--very-verbose\")\n\t}\n\n\thost := l.host.WithDir(util.HomeDir())\n\treturn host.RunQuiet(args...)\n}\nfunc (l processManager) Stop(ctx context.Context, conf config.Config) error {\n\tif s, err := l.Running(ctx, conf); err != nil || !s.Running {\n\t\treturn nil\n\t}\n\treturn l.host.RunQuiet(osutil.Executable(), \"daemon\", \"stop\", config.CurrentProfile().ShortName)\n}\n\nfunc processesFromConfig(conf config.Config) []process.Process {\n\tvar processes []process.Process\n\n\tif conf.Network.Address {\n\t\tprocesses = append(processes, vmnet.New(conf.Network.Mode, conf.Network.BridgeInterface))\n\t}\n\tif conf.MountINotify {\n\t\tprocesses = append(processes, inotify.New())\n\t}\n\n\treturn processes\n}\n"
  },
  {
    "path": "daemon/process/inotify/events.go",
    "content": "package inotify\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"time\"\n)\n\ntype modEvent struct {\n\tpath string // filename\n\tfs.FileMode\n}\n\nfunc (m modEvent) Mode() string { return fmt.Sprintf(\"%o\", m.FileMode) }\n\nfunc (f *inotifyProcess) handleEvents(ctx context.Context, watcher dirWatcher) error {\n\tlog := f.log\n\tlog.Trace(\"begin inotify event handler\")\n\n\tmod := make(chan modEvent)\n\tvols := make(chan []string)\n\n\tif err := f.monitorContainerVolumes(ctx, vols); err != nil {\n\t\treturn fmt.Errorf(\"error watching container volumes: %w\", err)\n\t}\n\n\tvar last time.Time\n\tvar cancelWatch context.CancelFunc\n\tvar currentVols []string\n\n\tvolsChanged := func(vols []string) bool {\n\t\tif len(currentVols) != len(vols) {\n\t\t\treturn true\n\t\t}\n\t\tfor i := range vols {\n\t\t\tif vols[i] != currentVols[i] {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\n\tcache := map[string]struct{}{}\n\n\tfor {\n\t\tselect {\n\n\t\t// exit signal\n\t\tcase <-ctx.Done():\n\t\t\tclose(mod)\n\t\t\treturn ctx.Err()\n\n\t\t// watch only container volumes\n\t\tcase vols := <-vols:\n\t\t\tif !volsChanged(vols) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.Tracef(\"volumes changed from: %+v, to: %+v\", currentVols, vols)\n\n\t\t\tcurrentVols = vols\n\n\t\t\tif cancel := cancelWatch; cancel != nil {\n\t\t\t\t// delay a bit to avoid zero downtime\n\t\t\t\ttime.AfterFunc(time.Second*1, cancel)\n\t\t\t}\n\n\t\t\tctx, cancel := context.WithCancel(ctx)\n\t\t\tcancelWatch = cancel\n\n\t\t\tgo func(ctx context.Context, vols []string, mod chan<- modEvent) {\n\t\t\t\tif err := watcher.Watch(ctx, vols, mod); err != nil {\n\t\t\t\t\tlog.Error(fmt.Errorf(\"error running watcher: %w\", err))\n\t\t\t\t}\n\t\t\t}(ctx, vols, mod)\n\n\t\t// handle modification events\n\t\tcase ev := <-mod:\n\t\t\tnow := time.Now()\n\n\t\t\t// rate limit, handle at most 50 unique items every 500 ms\n\t\t\tif now.Sub(last) < time.Millisecond*500 {\n\t\t\t\tif _, ok := cache[ev.path]; ok {\n\t\t\t\t\tcontinue // handled, ignore\n\t\t\t\t}\n\t\t\t\tif len(cache) > 50 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlast = now\n\t\t\t\tcache = map[string]struct{}{} // >500ms, reset unique cache\n\t\t\t}\n\n\t\t\t// cache current event\n\t\t\tcache[ev.path] = struct{}{}\n\n\t\t\t// validate that file exists\n\t\t\tif err := f.guest.RunQuiet(\"stat\", ev.path); err != nil {\n\t\t\t\tlog.Trace(fmt.Errorf(\"cannot stat '%s': %w\", ev.path, err))\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlog.Infof(\"syncing inotify event for %s \", ev.path)\n\t\t\tif err := f.guest.RunQuiet(\"sudo\", \"/bin/chmod\", ev.Mode(), ev.path); err != nil {\n\t\t\t\tlog.Trace(fmt.Errorf(\"error syncing inotify event: %w\", err))\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "daemon/process/inotify/inotify.go",
    "content": "package inotify\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/daemon/process\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nconst Name = \"inotify\"\nconst volumesInterval = 5 * time.Second\n\ntype Args struct {\n\tenvironment.GuestActions\n\tDirs    []string\n\tRuntime string\n}\n\nfunc CtxKeyArgs() any { return struct{ name string }{name: \"inotify_args\"} }\n\n// New returns inotify process.\nfunc New() process.Process {\n\treturn &inotifyProcess{\n\t\tlog: logrus.WithField(\"context\", \"inotify\"),\n\t}\n}\n\nvar _ process.Process = (*inotifyProcess)(nil)\n\ntype inotifyProcess struct {\n\tvmVols  []string\n\tguest   environment.GuestActions\n\truntime string\n\n\tlog *logrus.Entry\n}\n\n// Alive implements process.Process\nfunc (f *inotifyProcess) Alive(ctx context.Context) error {\n\tdaemonRunning, _ := ctx.Value(process.CtxKeyDaemon()).(bool)\n\n\t// if the parent is active, we can assume inotify is active.\n\tif daemonRunning {\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"inotify not running\")\n}\n\n// Dependencies implements process.Process\nfunc (*inotifyProcess) Dependencies() (deps []process.Dependency, root bool) {\n\treturn nil, false\n}\n\n// Name implements process.Process\nfunc (*inotifyProcess) Name() string {\n\treturn Name\n}\n\n// Start implements process.Process\nfunc (f *inotifyProcess) Start(ctx context.Context) error {\n\targs, ok := ctx.Value(CtxKeyArgs()).(Args)\n\tif !ok {\n\t\treturn fmt.Errorf(\"args missing in context\")\n\t}\n\tf.vmVols = omitChildrenDirectories(args.Dirs)\n\n\tf.guest = args.GuestActions\n\tf.runtime = args.Runtime\n\tlog := f.log\n\n\tlog.Info(\"waiting for VM to start\")\n\tf.waitForLima(ctx)\n\tlog.Info(\"VM started\")\n\n\twatcher := &defaultWatcher{log: log}\n\n\treturn f.handleEvents(ctx, watcher)\n}\n\n// waitForLima waits until lima starts and sets the directory to watch.\nfunc (f *inotifyProcess) waitForLima(ctx context.Context) {\n\tlog := f.log\n\n\t// wait for Lima to finish starting\n\tfor {\n\t\tlog.Info(\"waiting 5 secs for VM\")\n\n\t\t// 5 second interval\n\t\tafter := time.After(time.Second * 5)\n\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-after:\n\t\t\ti, err := limautil.Instance()\n\t\t\tif err != nil || !i.Running() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := f.guest.RunQuiet(\"uname\", \"-a\"); err == nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "daemon/process/inotify/volumes.go",
    "content": "package inotify\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/environment/container/containerd\"\n\t\"github.com/abiosoft/colima/environment/container/docker\"\n)\n\nfunc (f *inotifyProcess) monitorContainerVolumes(ctx context.Context, c chan<- []string) error {\n\tlog := f.log\n\n\tif f.runtime == \"\" {\n\t\treturn fmt.Errorf(\"empty runtime\")\n\t}\n\n\tfetch := func() ([]string, error) {\n\t\tvar vols []string\n\n\t\tswitch f.runtime {\n\n\t\tcase docker.Name:\n\t\t\tvols, err := f.fetchVolumes(docker.Name)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error fetching docker volumes: %w\", err)\n\t\t\t}\n\t\t\treturn vols, nil\n\n\t\tcase containerd.Name:\n\t\t\tvar namespaces []string\n\t\t\tout, err := f.guest.RunOutput(\"sudo\", \"nerdctl\", \"namespace\", \"list\", \"-q\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error retrieving containerd namespaces: %w\", err)\n\t\t\t}\n\t\t\tif out != \"\" {\n\t\t\t\tnamespaces = strings.Fields(out)\n\t\t\t}\n\n\t\t\tfor _, ns := range namespaces {\n\t\t\t\tv, err := f.fetchVolumes(\"sudo\", \"nerdctl\", \"--namespace\", ns)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"error retrieving containerd volumes: %w\", err)\n\t\t\t\t}\n\t\t\t\tif len(v) > 0 {\n\t\t\t\t\tvols = append(vols, v...)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn vols, nil\n\t\t}\n\n\t\treturn nil, nil\n\t}\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tlog.Trace(\"stop signal received\")\n\t\t\t\terr := ctx.Err()\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Trace(fmt.Errorf(\"error during stop: %w\", err))\n\t\t\t\t}\n\t\t\tcase <-time.After(volumesInterval):\n\t\t\t\tif vols, err := fetch(); err != nil {\n\t\t\t\t\tlog.Error(err)\n\t\t\t\t} else {\n\t\t\t\t\tc <- vols\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn nil\n}\n\nfunc (f *inotifyProcess) fetchVolumes(cmdArgs ...string) ([]string, error) {\n\tlog := f.log\n\n\t// fetch all containers\n\tvar containers []string\n\t{\n\t\targs := append([]string{}, cmdArgs...)\n\t\targs = append(args, \"ps\", \"-q\")\n\t\tout, err := f.guest.RunOutput(args...)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error listing containers: %w\", err)\n\t\t}\n\t\tcontainers = strings.Fields(out)\n\t\tif len(containers) == 0 {\n\t\t\treturn nil, nil\n\t\t}\n\t}\n\n\tlog.Tracef(\"found containers %+v\", containers)\n\n\t// fetch volumes\n\tvar resp []struct {\n\t\tMounts []struct {\n\t\t\tSource string `json:\"Source\"`\n\t\t} `json:\"Mounts\"`\n\t}\n\t{\n\t\targs := append([]string{}, cmdArgs...)\n\t\targs = append(args, \"inspect\")\n\t\targs = append(args, containers...)\n\n\t\tvar buf bytes.Buffer\n\t\tif err := f.guest.RunWith(nil, &buf, args...); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error inspecting containers: %w\", err)\n\t\t}\n\t\tif err := json.NewDecoder(&buf).Decode(&resp); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error decoding docker response\")\n\t\t}\n\t}\n\n\t// process and discard redundant volumes\n\tvols := []string{}\n\t{\n\t\tshouldMount := func(child string) bool {\n\t\t\t// ignore all invalid directories.\n\t\t\t// i.e. directories not within the mounted VM directories\n\t\t\tfor _, parent := range f.vmVols {\n\t\t\t\tif strings.HasPrefix(child, parent) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\n\t\tfor _, r := range resp {\n\t\t\tfor _, mount := range r.Mounts {\n\t\t\t\tif shouldMount(mount.Source) {\n\t\t\t\t\tvols = append(vols, mount.Source)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvols = omitChildrenDirectories(vols)\n\t\tlog.Tracef(\"found volumes %+v\", vols)\n\t}\n\n\treturn vols, nil\n}\n\nfunc omitChildrenDirectories(dirs []string) []string {\n\tsort.Strings(dirs) // sort to put the parent directories first\n\n\t// keep track for uniqueness\n\tset := map[string]struct{}{}\n\n\tvar newVols []string\n\n\tomitted := map[int]struct{}{}\n\tfor i := 0; i < len(dirs); i++ {\n\t\t// if the index is omitted, skip\n\t\tif _, ok := omitted[i]; ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tparent := dirs[i]\n\t\tif _, ok := set[parent]; !ok {\n\t\t\tnewVols = append(newVols, parent)\n\t\t\tset[parent] = struct{}{}\n\t\t}\n\n\t\tfor j := i + 1; j < len(dirs); j++ {\n\t\t\tchild := dirs[j]\n\t\t\tif strings.HasPrefix(child, strings.TrimSuffix(parent, \"/\")+\"/\") {\n\t\t\t\tomitted[j] = struct{}{}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn newVols\n}\n"
  },
  {
    "path": "daemon/process/inotify/volumes_test.go",
    "content": "package inotify\n\nimport (\n\t\"reflect\"\n\t\"strconv\"\n\t\"testing\"\n)\n\nfunc Test_omitChildrenDirectories(t *testing.T) {\n\ttests := []struct {\n\t\targs []string\n\t\twant []string\n\t}{\n\t\t{\n\t\t\targs: []string{\"/\", \"/user\", \"/user/someone\", \"/a\", \"/a/ee\", \"/a/bb\"},\n\t\t\twant: []string{\"/\"},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"/someone\", \"/user\", \"/user/someone\", \"/a\", \"/a/ee\", \"/a/bb\", \"/a\"},\n\t\t\twant: []string{\"/a\", \"/someone\", \"/user\"},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"/someone\", \"/user/colima/projects/myworks\", \"/user/colima/projects\", \"/user/colima/projects/myworks\", \"/user/colima/projects\", \"/someone\"},\n\t\t\twant: []string{\"/someone\", \"/user/colima/projects\"},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"/someone\", \"/user/colima/projects/myworks\", \"/user/colima/projects\"},\n\t\t\twant: []string{\"/someone\", \"/user/colima/projects\"},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"/user/colima/projects\"},\n\t\t\twant: []string{\"/user/colima/projects\"},\n\t\t},\n\t}\n\tfor i, tt := range tests {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tif got := omitChildrenDirectories(tt.args); !reflect.DeepEqual(got, tt.want) {\n\t\t\t\tt.Errorf(\"omitChildrenDirectories() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "daemon/process/inotify/watch.go",
    "content": "package inotify\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/rjeczalik/notify\"\n\t\"github.com/sirupsen/logrus\"\n)\n\ntype dirWatcher interface {\n\t// Watch watches directories recursively for changes and sends message via c on\n\t// modifications to files within the watched directories.\n\t//\n\t// Watch returns immediately and runs the watcher in the background.\n\t// An error is returned when the watcher can not be started in background.\n\t//\n\t// The watcher terminates on fatal error or when ctx is done.\n\tWatch(ctx context.Context, dirs []string, c chan<- modEvent) error\n}\n\ntype defaultWatcher struct {\n\tlog *logrus.Entry\n}\n\n// Watch implements dirWatcher\nfunc (d *defaultWatcher) Watch(ctx context.Context, dirs []string, mod chan<- modEvent) error {\n\tlog := d.log\n\tc := make(chan notify.EventInfo, 1)\n\n\tfor _, dir := range dirs {\n\t\tdir, err := util.CleanPath(dir)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid directory: %w\", err)\n\t\t}\n\t\terr = notify.Watch(dir+\"...\", c, notify.Write)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error watching directory recursively '%s': %w\", dir, err)\n\t\t}\n\t}\n\n\tgo func(ctx context.Context, c chan notify.EventInfo, mod chan<- modEvent) {\n\t\tfor {\n\t\t\tselect {\n\n\t\t\tcase <-ctx.Done():\n\t\t\t\tnotify.Stop(c)\n\t\t\t\tlog.Trace(\"stopping watcher\")\n\t\t\t\tif err := ctx.Err(); err != nil {\n\t\t\t\t\tlog.Trace(fmt.Errorf(\"error found in ctx: %w\", err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\tcase e := <-c:\n\t\t\t\tpath := e.Path()\n\n\t\t\t\tlog.Tracef(\"received event %s for %s\", e.Event().String(), path)\n\n\t\t\t\tstat, err := os.Stat(path)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Trace(fmt.Errorf(\"unable to stat inotify file '%s': %w\", path, err))\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif stat.IsDir() {\n\t\t\t\t\tlog.Tracef(\"'%s' is directory, ignoring.\", path)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// send modification event\n\t\t\t\tmod <- modEvent{path: path, FileMode: stat.Mode()}\n\t\t\t}\n\t\t}\n\t}(ctx, c, mod)\n\n\treturn nil\n}\n\nvar _ dirWatcher = (*defaultWatcher)(nil)\n"
  },
  {
    "path": "daemon/process/process.go",
    "content": "package process\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/abiosoft/colima/config\"\n\n\t\"github.com/abiosoft/colima/environment\"\n)\n\nfunc CtxKeyDaemon() any { return struct{ key string }{key: \"colima_daemon\"} }\n\n// Process is a background process managed by the daemon.\ntype Process interface {\n\t// Name for the background process\n\tName() string\n\t// Start starts the background process.\n\t// The process is expected to terminate when ctx is done.\n\tStart(ctx context.Context) error\n\t// Alive checks if the process is the alive.\n\tAlive(ctx context.Context) error\n\t// Dependencies are requirements for start to succeed.\n\t// root should be true if root access is required for\n\t// installing any of the dependencies.\n\tDependencies() (deps []Dependency, root bool)\n}\n\n// Dir is the directory for daemon files.\nfunc Dir() string { return filepath.Join(config.CurrentProfile().ConfigDir(), \"daemon\") }\n\n// Dependency is a requirement to be fulfilled before a process can be started.\ntype Dependency interface {\n\tInstalled() bool\n\tInstall(environment.HostActions) error\n}\n\n// Dependencies returns the dependencies for the processes.\n// root returns if root access is required\nfunc Dependencies(processes ...Process) (deps Dependency, root bool) {\n\t// check rootful for user info message\n\trootful := false\n\tfor _, p := range processes {\n\t\tdeps, root := p.Dependencies()\n\t\tfor _, dep := range deps {\n\t\t\tif !dep.Installed() && root {\n\t\t\t\trootful = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn processDeps(processes), rootful\n}\n\ntype processDeps []Process\n\nfunc (p processDeps) Installed() bool {\n\tfor _, process := range p {\n\t\tdeps, _ := process.Dependencies()\n\t\tfor _, d := range deps {\n\t\t\tif !d.Installed() {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc (p processDeps) Install(host environment.HostActions) error {\n\tfor _, process := range p {\n\t\tdeps, _ := process.Dependencies()\n\t\tfor _, d := range deps {\n\t\t\tif !d.Installed() {\n\t\t\t\tif err := d.Install(host); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"error occurred installing dependencies for '%s': %w\", process.Name(), err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "daemon/process/vmnet/deps.go",
    "content": "package vmnet\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/abiosoft/colima/daemon/process\"\n\t\"github.com/abiosoft/colima/embedded\"\n\t\"github.com/abiosoft/colima/environment\"\n)\n\nvar _ process.Dependency = sudoerFile{}\n\ntype sudoerFile struct{}\n\n// Installed implements Dependency\nfunc (s sudoerFile) Installed() bool { return embedded.SudoersInstalled() }\n\n// Install implements Dependency\nfunc (s sudoerFile) Install(host environment.HostActions) error {\n\treturn embedded.InstallSudoers(host)\n}\n\nvar _ process.Dependency = vmnetFile{}\n\nconst BinaryPath = \"/opt/colima/bin/socket_vmnet\"\nconst ClientBinaryPath = \"/opt/colima/bin/socket_vmnet_client\"\n\ntype vmnetFile struct{}\n\n// Installed implements Dependency\nfunc (v vmnetFile) Installed() bool {\n\tfor _, bin := range v.bins() {\n\t\tif _, err := os.Stat(bin); err != nil {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc (v vmnetFile) bins() []string {\n\treturn []string{BinaryPath, ClientBinaryPath}\n}\nfunc (v vmnetFile) Install(host environment.HostActions) error {\n\tarch := \"x86_64\"\n\tif runtime.GOARCH != \"amd64\" {\n\t\tarch = \"arm64\"\n\t}\n\n\t// read the embedded file\n\tgz, err := embedded.Read(\"network/vmnet_\" + arch + \".tar.gz\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error retrieving embedded vmnet file: %w\", err)\n\t}\n\n\t// write tar to tmp directory\n\tf, err := os.CreateTemp(\"\", \"vmnet.tar.gz\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating temp file: %w\", err)\n\t}\n\tif _, err := f.Write(gz); err != nil {\n\t\treturn fmt.Errorf(\"error writing temp file: %w\", err)\n\t}\n\t_ = f.Close() // not a fatal error\n\n\tdefer func() {\n\t\t_ = os.Remove(f.Name())\n\t}()\n\n\t// extract tar to desired location\n\tdir := optDir\n\tif err := host.RunInteractive(\"sudo\", \"mkdir\", \"-p\", dir); err != nil {\n\t\treturn fmt.Errorf(\"error preparing colima privileged dir: %w\", err)\n\t}\n\tif err := host.RunInteractive(\"sudo\", \"sh\", \"-c\", fmt.Sprintf(\"cd %s && tar xfz %s 2>/dev/null\", dir, f.Name())); err != nil {\n\t\treturn fmt.Errorf(\"error extracting vmnet archive: %w\", err)\n\t}\n\n\treturn nil\n}\n\nvar _ process.Dependency = vmnetRunDir{}\n\ntype vmnetRunDir struct{}\n\n// Install implements Dependency\nfunc (v vmnetRunDir) Install(host environment.HostActions) error {\n\treturn host.RunInteractive(\"sudo\", \"mkdir\", \"-p\", runDir())\n}\n\n// Installed implements Dependency\nfunc (v vmnetRunDir) Installed() bool {\n\tstat, err := os.Stat(runDir())\n\treturn err == nil && stat.IsDir()\n}\n\nconst optDir = \"/opt/colima\"\n\n// runDir is the directory to the rootful daemon run related files. e.g. pid files\nfunc runDir() string { return filepath.Join(optDir, \"run\") }\n"
  },
  {
    "path": "daemon/process/vmnet/vmnet.go",
    "content": "package vmnet\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/daemon/process\"\n\t\"github.com/abiosoft/colima/util/osutil\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nconst Name = \"vmnet\"\n\nconst (\n\tSubProcessEnvVar = \"COLIMA_VMNET\"\n\n\tNetGateway = \"192.168.106.1\"\n\tNetDHCPEnd = \"192.168.106.254\"\n)\n\nvar _ process.Process = (*vmnetProcess)(nil)\n\nfunc New(mode, netInterface string) process.Process {\n\treturn &vmnetProcess{\n\t\tmode:         mode,\n\t\tnetInterface: netInterface,\n\t}\n}\n\ntype vmnetProcess struct {\n\tmode         string\n\tnetInterface string\n}\n\nfunc (*vmnetProcess) Alive(ctx context.Context) error {\n\tinfo := Info()\n\tpidFile := info.PidFile\n\tsocketFile := info.Socket.File()\n\n\tif _, err := os.Stat(pidFile); err == nil {\n\t\tcmd := exec.CommandContext(ctx, \"sudo\", \"/usr/bin/pkill\", \"-0\", \"-F\", pidFile)\n\t\tif err := cmd.Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"error checking vmnet process: %w\", err)\n\t\t}\n\t}\n\n\tif _, err := os.Stat(socketFile); err != nil {\n\t\treturn fmt.Errorf(\"vmnet socket file not found error: %w\", err)\n\t}\n\tif n, err := net.Dial(\"unix\", socketFile); err != nil {\n\t\treturn fmt.Errorf(\"vmnet socket file error: %w\", err)\n\t} else {\n\t\tif err := n.Close(); err != nil {\n\t\t\tlogrus.Debugln(fmt.Errorf(\"error closing ping socket connection: %w\", err))\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Name implements process.BgProcess\nfunc (*vmnetProcess) Name() string { return Name }\n\n// Start implements process.BgProcess\nfunc (v *vmnetProcess) Start(ctx context.Context) error {\n\tinfo := Info()\n\tsocket := info.Socket.File()\n\tpid := info.PidFile\n\n\t// delete existing sockets if exist\n\t// errors ignored on purpose\n\t_ = forceDeleteFileIfExists(socket)\n\n\tdone := make(chan error, 1)\n\n\tgo func() {\n\t\t// rootfully start the vmnet daemon\n\t\tvar command *exec.Cmd\n\n\t\tif v.mode == \"bridged\" {\n\t\t\tcommand = cli.CommandInteractive(\"sudo\", BinaryPath,\n\t\t\t\t\"--vmnet-mode\", \"bridged\",\n\t\t\t\t\"--socket-group\", \"staff\",\n\t\t\t\t\"--vmnet-interface\", v.netInterface,\n\t\t\t\t\"--pidfile\", pid,\n\t\t\t\tsocket,\n\t\t\t)\n\t\t} else {\n\t\t\tcommand = cli.CommandInteractive(\"sudo\", BinaryPath,\n\t\t\t\t\"--vmnet-mode\", \"shared\",\n\t\t\t\t\"--socket-group\", \"staff\",\n\t\t\t\t\"--vmnet-gateway\", NetGateway,\n\t\t\t\t\"--vmnet-dhcp-end\", NetDHCPEnd,\n\t\t\t\t\"--pidfile\", pid,\n\t\t\t\tsocket,\n\t\t\t)\n\t\t}\n\n\t\tif cli.Settings.Verbose {\n\t\t\tcommand.Env = append(command.Env, os.Environ()...)\n\t\t\tcommand.Env = append(command.Env, \"DEBUG=1\")\n\t\t}\n\n\t\tdone <- command.Run()\n\t}()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\tif err := stop(pid); err != nil {\n\t\t\treturn fmt.Errorf(\"error stopping vmnet: %w\", err)\n\t\t}\n\tcase err := <-done:\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error running vmnet: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (vmnetProcess) Dependencies() (deps []process.Dependency, root bool) {\n\treturn []process.Dependency{\n\t\tsudoerFile{},\n\t\tvmnetFile{},\n\t\tvmnetRunDir{},\n\t}, true\n}\n\nfunc stop(pidFile string) error {\n\t// rootfully kill the vmnet process.\n\t// process is only assumed alive if the pidfile exists\n\tif _, err := os.Stat(pidFile); err == nil {\n\t\tif err := cli.CommandInteractive(\"sudo\", \"/usr/bin/pkill\", \"-F\", pidFile).Run(); err != nil {\n\t\t\treturn fmt.Errorf(\"error killing vmnet process: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc forceDeleteFileIfExists(name string) error {\n\tif stat, err := os.Stat(name); err == nil && !stat.IsDir() {\n\t\treturn os.Remove(name)\n\t}\n\treturn nil\n}\n\nfunc Info() struct {\n\tPidFile string\n\tSocket  osutil.Socket\n} {\n\treturn struct {\n\t\tPidFile string\n\t\tSocket  osutil.Socket\n\t}{\n\t\tPidFile: filepath.Join(runDir(), \"vmnet-\"+config.CurrentProfile().ShortName+\".pid\"),\n\t\tSocket:  osutil.Socket(filepath.Join(process.Dir(), \"vmnet.sock\")),\n\t}\n}\n"
  },
  {
    "path": "default.nix",
    "content": "with import <nixpkgs> { };\ncallPackage (import ./colima.nix) { }\n"
  },
  {
    "path": "docs/CONTRIBUTE.md",
    "content": "# Contributing to Colima\n\nThank you for your interest in contributing to Colima!\n\n## Getting Started\n\nColima is a Go project. To contribute, you will need Go installed (see the [Go installation guide](https://golang.org/doc/install)).\n\n### 1. Fork the Repository\n\nFirst, fork the Colima repository on GitHub. Then, clone your fork locally:\n\n```sh\ngit clone https://github.com/<your-username>/colima.git\ncd colima\n```\n\n### 2. Create a New Branch\n\nCreate a new branch for your changes:\n\n```sh\ngit checkout -b my-feature-branch\n```\n\n### 3. Commit Your Changes\n\nCommit your changes with a DCO signoff and the required commit message format. Each commit must include a signoff line:\n\n```\nSigned-off-by: Your Name <your.email@example.com>\n```\n\nYou can add this automatically with:\n\n```sh\ngit commit -s -m \"component: <message>\"\n# Example:\ngit commit -s -m \"cli: add my-command to colima start\"\n```\n\n### 4. Push Your Branch\n\nPush your branch to your fork:\n\n```sh\ngit push origin my-feature-branch\n```\n\n### 5. Open a Pull Request\n\nOpen a Pull Request against the main Colima repository.\n\n## 6. DCO Signoff\n\nColima 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.\n\n## Contribution Guidelines\n\n### Major Contributions\nMajor contributions (new features, significant changes) should be preceded by a GitHub issue discussing the proposed change.\n\n### Minor Contributions\nMinor contributions (small fixes, documentation e.t.c.) do not require a prior issue.\n\n### LLM Usage Disclosure\nIf 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.\n\n### LLM Reviewability\nLarge code contributions generated by LLMs that are not easily reviewable or understandable will be rejected.\n\n## Reviewing and Merging\n\nAll PRs are subject to review.\n\nKindly ensure that your PR passes all CI checks. You are also obliged to respond to review comments and update your PR as needed.\n\n## Need Help?\n\nIf 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.\n\n---\n\nThank you for helping to improve Colima!\n"
  },
  {
    "path": "docs/FAQ.md",
    "content": "# FAQs\n\n- [FAQs](#faqs)\n  - [How does Colima compare to Lima?](#how-does-colima-compare-to-lima)\n  - [Are Apple Silicon Macs supported?](#are-apple-silicon-macs-supported)\n  - [Are AI workloads supported?](#are-ai-workloads-supported)\n  - [Are older macOS versions supported?](#are-older-macos-versions-supported)\n  - [Does Colima support autostart?](#does-colima-support-autostart)\n  - [Can config file be used instead of cli flags?](#can-config-file-be-used-instead-of-cli-flags)\n    - [Specifying the config location](#specifying-the-config-location)\n    - [Editing the config](#editing-the-config)\n    - [Setting the default config](#setting-the-default-config)\n    - [Specifying the config editor](#specifying-the-config-editor)\n  - [How do I change where Colima files are stored?](#how-do-i-change-where-colima-files-are-stored)\n  - [How do I pass custom environment variables into the VM?](#how-do-i-pass-custom-environment-variables-into-the-vm)\n  - [Docker](#docker)\n    - [Can it run alongside Docker for Mac?](#can-it-run-alongside-docker-for-mac)\n    - [Docker socket location](#docker-socket-location)\n      - [v0.3.4 or older](#v034-or-older)\n      - [v0.4.0 or newer](#v040-or-newer)\n      - [Listing Docker contexts](#listing-docker-contexts)\n      - [Changing the active Docker context](#changing-the-active-docker-context)\n    - [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)\n    - [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)\n    - [Docker buildx plugin is missing](#docker-buildx-plugin-is-missing)\n      - [Installing Buildx](#installing-buildx)\n  - [Containerd](#containerd)\n    - [How to customize Containerd config?](#how-to-customize-containerd-config)\n      - [Per-profile overrides](#per-profile-overrides)\n  - [How does Colima compare to minikube, Kind, K3d?](#how-does-colima-compare-to-minikube-kind-k3d)\n    - [For Kubernetes](#for-kubernetes)\n    - [For Docker](#for-docker)\n  - [Is another Distro supported?](#is-another-distro-supported)\n    - [Version v0.5.6 and lower](#version-v056-and-lower)\n      - [Enabling Ubuntu layer](#enabling-ubuntu-layer)\n      - [Accessing the underlying Virtual Machine](#accessing-the-underlying-virtual-machine)\n    - [Version v0.6.0 and newer](#version-v060-and-newer)\n  - [The Virtual Machine's IP is not reachable](#the-virtual-machines-ip-is-not-reachable)\n    - [Enable reachable IP address](#enable-reachable-ip-address)\n  - [Incus instances are not reachable from the host](#incus-instances-are-not-reachable-from-the-host)\n  - [How can disk space be recovered?](#how-can-disk-space-be-recovered)\n    - [Automatic](#automatic)\n    - [Manual](#manual)\n  - [How can disk size be increased?](#how-can-disk-size-be-increased)\n  - [Are Lima overrides supported?](#are-lima-overrides-supported)\n    - [Example: Adding provision scripts](#example-adding-provision-scripts)\n  - [How can the VM and its tools be updated?](#how-can-the-vm-and-its-tools-be-updated)\n    - [Updating Colima](#updating-colima)\n    - [Updating the container runtime](#updating-the-container-runtime)\n    - [Accessing the Virtual Machine](#accessing-the-virtual-machine)\n  - [Troubleshooting](#troubleshooting)\n    - [Colima not starting](#colima-not-starting)\n      - [Broken status](#broken-status)\n      - [FATA\\[0000\\] error starting vm: error at 'starting': exit status 1](#fata0000-error-starting-vm-error-at-starting-exit-status-1)\n    - [Issues after an upgrade](#issues-after-an-upgrade)\n    - [Colima cannot access the internet.](#colima-cannot-access-the-internet)\n    - [Docker Compose and Buildx showing runc error](#docker-compose-and-buildx-showing-runc-error)\n      - [Version v0.5.6 or lower](#version-v056-or-lower)\n    - [Issue with Docker bind mount showing empty](#issue-with-docker-bind-mount-showing-empty)\n  - [How can Docker version be updated?](#how-can-docker-version-be-updated)\n  - [How can I delete container data](#how-can-i-delete-container-data)\n\n## How does Colima compare to Lima?\n\nColima is basically a higher level usage of Lima and utilises Lima to provide Docker, Containerd and/or Kubernetes.\n\n## Are Apple Silicon Macs supported?\n\nColima supports and works on both Intel and Apple Silicon Macs.\n\nFeedbacks would be appreciated.\n\n## Are AI workloads supported?\n\nYes, Colima supports GPU accelerated containers for AI workloads on Apple Silicon Macs running macOS 13 or newer.\n\nTo get started, start Colima with Docker runtime and krunkit VM type:\n\n```sh\ncolima start --runtime docker --vm-type krunkit\n```\n\nThen setup and run AI models:\n\n```sh\ncolima model setup\ncolima model run gemma3\n```\n\nMultiple model registries are supported including HuggingFace (default) and Ollama:\n\n```sh\ncolima model run hf://tinyllama\ncolima model run ollama://tinyllama\n```\n\nFor more options, run `colima model --help`.\n\n## Are older macOS versions supported?\n\nColima is supported and regularly tested on the latest macOS version. However, Colima requires macOS 13 or newer.\n\nYou 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/).\n## Does Colima support autostart?\n\nSince v0.5.6 Colima supports foreground mode via the `--foreground` flag. i.e. `colima start --foreground`.\n\nIf Colima has been installed using brew, the easiest way to autostart Colima is to use brew services.\n\n```sh\nbrew services start colima\n```\n\n## Can config file be used instead of cli flags?\n\nYes, from v0.4.0, Colima support YAML configuration file.\n\n### Specifying the config location\n\nSet the `$COLIMA_HOME` environment variable, otherwise it defaults to `$HOME/.colima`.\n\n### Editing the config\n\n```\ncolima start --edit\n```\n\nFor manual edit, the config file is located at `$HOME/.colima/default/colima.yaml`.\n\nFor other profiles, `$HOME/.colima/<profile-name>/colima.yaml`\n\n### Setting the default config\n\n```\ncolima template\n```\n\nFor manual edit, the template file is located at `$HOME/.colima/_templates/default.yaml`.\n\n### Specifying the config editor\n\nSet the `$EDITOR` environment variable or use the `--editor` flag.\n\n```sh\ncolima start --edit --editor code # one-off config\ncolima template --editor code # default config\n```\n\n## How do I change where Colima files are stored?\n\nColima supports these environment variables, set on your host machine:\n\n| Variable | Description |\n|----------|-------------|\n| `COLIMA_HOME` | Colima configuration directory (default: `$HOME/.colima`) |\n| `COLIMA_CACHE_HOME` | Colima cache directory (default is host-specific, see [os.UserCacheDir()](https://pkg.go.dev/os#UserCacheDir)) |\n| `COLIMA_PROFILE` | Active profile name (default: `default`) |\n| `DOCKER_CONFIG` | Path to Docker client configuration directory (default: `~/.docker`) |\n\n## How do I pass custom environment variables into the VM?\n\nPass environment variables into the VM at startup using the YAML configuration file:\n\n```yaml\nenv:\n  MY_VAR: value\n```\n\nYou can also use command-line flags:\n\n```bash session\n# On your host machine...\n$ colima start --env MY_VAR=value\n\n# Then, within the VM...\n$ colima ssh\nuser@colima:~$ env | grep MY_VAR\nMY_VAR=value\n```\n\n## Docker\n\n### Can it run alongside Docker for Mac?\n\nYes, from version v0.3.0 Colima leverages Docker contexts and can thereby run alongside Docker for Mac.\n\nColima makes itself the default Docker context on startup and should work straight away.\n\n### Docker socket location\n\n#### v0.3.4 or older\n\nDocker socket is located at `$HOME/.colima/docker.sock`\n\n#### v0.4.0 or newer\n\nDocker socket is located at `$HOME/.colima/default/docker.sock`\n\nIt can also be retrieved by checking status\n\n```\ncolima status\n```\n\n#### Listing Docker contexts\n\n```\ndocker context list\n```\n\n#### Changing the active Docker context\n\n```\ndocker context use <context-name>\n```\n### Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?\n\nColima uses Docker contexts to allow co-existence with other Docker servers and sets itself as the default Docker context on startup.\n\nHowever, some applications are not aware of Docker contexts and may lead to the error.\n\nThis can be fixed by any of the following approaches. Ensure the Docker socket path by checking the [socket location](#docker-socket-location).\n\n1. Setting application specific Docker socket path if supported by the application. e.g. JetBrains IDEs.\n\n2. Setting the `DOCKER_HOST` environment variable to point to Colima socket.\n\n   ```sh\n   export DOCKER_HOST=\"unix://$HOME/.colima/default/docker.sock\"\n   ```\n3. Linking the Colima socket to the default socket path. **Note** that this may break other Docker servers.\n\n   ```sh\n   sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock\n   ```\n\n\n### How to customize Docker config (e.g., adding insecure registries or registry mirrors)?\n\n* v0.3.4 or lower\n\n  On first startup, Colima generates Docker daemon.json file at `$HOME/.colima/docker/daemon.json`.\n  Modify the daemon.json file accordingly and restart Colima.\n\n* v0.4.0 or newer\n\n  Start Colima with `--edit` flag.\n\n  ```sh\n  colima start --edit\n  ```\n\n  Add the Docker config to the `docker` section.\n\n  ```diff\n  - docker: {}\n  + docker:\n  +   insecure-registries:\n  +     - myregistry.com:5000\n  +     - host.docker.internal:5000\n  ```\n**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.\n\nFor example, if adding registry mirrors, modifications are needed as follows:\n\nFirst, colima:\n\n```sh\ncolima start --edit\n```\n\n```diff\n- docker: {}\n+ docker:\n+   registry-mirrors:\n+     - https://my.dockerhub.mirror.something\n+     - https://my.quayio.mirror.something\n```\n\nAs an alternative approach to the **colima start --edit**, make the changes via the **template** command (affecting the configuration for any new instances):\n\n```sh\ncolima template\n```\n\nThen, the Docker ~/.docker/daemon.json file (as compared to the default):\n\n```diff\n- \"experimental\": false,\n+ \"experimental\": false,\n+ \"registry-mirrors\": [\n+   \"https://my.dockerhub.mirror.something\",\n+   \"https://my.quayio.mirror.something\"\n+ ]\n```\n\n### Docker buildx plugin is missing\n\n`buildx` can be installed as a Docker plugin\n\n#### Installing Buildx\n\nUsing homebrew\n```sh\nbrew install docker-buildx\n# Follow the caveats mentioned in the install instructions:\n# mkdir -p ~/.docker/cli-plugins\n# ln -sfn $(which docker-buildx) ~/.docker/cli-plugins/docker-buildx\ndocker buildx version # verify installation\n```\nAlternatively\n```sh\nARCH=amd64 # change to 'arm64' for m1\nVERSION=v0.11.2\ncurl -LO https://github.com/docker/buildx/releases/download/${VERSION}/buildx-${VERSION}.darwin-${ARCH}\nmkdir -p ~/.docker/cli-plugins\nmv buildx-${VERSION}.darwin-${ARCH} ~/.docker/cli-plugins/docker-buildx\nchmod +x ~/.docker/cli-plugins/docker-buildx\ndocker buildx version # verify installation\n```\n\n## Containerd\n\n### How to customize Containerd config?\n\nOn first startup with the containerd runtime, Colima generates default config files at the standard user config locations:\n\n| File | Location |\n|------|----------|\n| Containerd config | `~/.config/containerd/config.toml` |\n| BuildKit config | `~/.config/buildkit/buildkitd.toml` |\n\nThese follow the standard rootless containerd/buildkit config paths and are shared across all Colima profiles.\n\nModify the files accordingly and restart Colima for changes to take effect.\n\n```sh\n# edit the containerd config\n$EDITOR ~/.config/containerd/config.toml\n\n# restart colima\ncolima stop && colima start --runtime containerd\n```\n\n#### Per-profile overrides\n\nTo use a different config for a specific profile, place the config file at `$HOME/.colima/<profile-name>/containerd/config.toml` (or `buildkitd.toml`). Per-profile configs take priority over the central config.\n\nThe resolution order is:\n\n1. `~/.colima/<profile>/containerd/<file>` (per-profile override)\n2. `~/.config/containerd/<file>` or `~/.config/buildkit/<file>` (central)\n3. Embedded default\n\n**Note:** `$XDG_CONFIG_HOME` is respected for the central config location if set.\n\n## How does Colima compare to minikube, Kind, K3d?\n\n### For Kubernetes\n\nYes, you can create a Kubernetes cluster with minikube (with Docker driver), Kind or K3d instead of enabling Kubernetes\nin Colima.\n\nThose are better options if you need multiple clusters, or do not need Docker and Kubernetes to share the same images and runtime.\n\nColima with Docker runtime is fully compatible with Minikube (with Docker driver), Kind and K3d.\n\n### For Docker\n\nMinikube with Docker runtime can expose the cluster's Docker with `minikube docker-env`. But there are some caveats.\n\n- Kubernetes is not optional, even if you only need Docker.\n\n- 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.\n\n## Is another Distro supported?\n\n### Version v0.5.6 and lower\n\nColima uses a lightweight Alpine image with bundled dependencies.\nTherefore, user interaction with the Virtual Machine is expected to be minimal (if any).\n\nHowever, Colima optionally provides Ubuntu container as a layer.\n\n\n#### Enabling Ubuntu layer\n\n* CLI\n  ```\n  colima start --layer=true\n  ```\n\n* Config\n  ```diff\n  - layer: false\n  + layer: true\n  ```\n\n#### Accessing the underlying Virtual Machine\n\nWhen the layer is enabled, the underlying Virtual Machine is abstracted and both the `ssh` and `ssh-config` commands routes to the layer.\n\nThe 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.\n\n### Version v0.6.0 and newer\n\nColima uses Ubuntu as the underlying image. Other distros are not supported.\n\n## The Virtual Machine's IP is not reachable\n\nReachable IP address is not enabled by default due to root privilege and slower startup time.\n\n### Enable reachable IP address\n\n**NOTE:** this is only supported on macOS\n\n* CLI\n  ```\n  colima start --network-address\n  ```\n* Config\n  ```diff\n  network:\n  -  address: false\n  +  address: true\n  ```\n\n## Incus instances are not reachable from the host\n\n<small>**Requires v0.10.0**</small>\n\nIncus containers and virtual machines are not reachable from the host by default. This is because network address is not enabled by default.\n\nTo fix this, stop Colima and restart with network address enabled:\n\n```sh\ncolima stop\ncolima start --network-address\n```\n\nOr enable it in the config file:\n\n```sh\ncolima start --edit\n```\n\n```diff\nnetwork:\n-  address: false\n+  address: true\n```\n\n## How can disk space be recovered?\n\nDisk space can be freed in the VM by removing containers or running `docker system prune`.\nHowever, it will not reflect on the host on Colima versions v0.4.x or lower.\n\n### Automatic\n\nFor Colima v0.5.0 and above, unused disk space in the VM is released on startup. A restart would suffice.\n\n### Manual\n\nFor Colima v0.5.0 and above, user can manually recover the disk space by running `sudo fstrim -a` in the VM.\n\n```sh\n# '-v' may be added for verbose output\ncolima ssh -- sudo fstrim -a\n```\n\n## How can disk size be increased?\n\nDisk size is automatically increased on start up based on configuration in `colima.yaml`\n\n```diff\n- disk: 150\n+ disk: 250\n```\n\n__Note:__ This feature is available from Version 0.5.3.\n\n\n## Are Lima overrides supported?\n\nYes, however this should only be done by advanced users.\n\nLima supports `override.yaml` and `default.yaml` files that can modify the VM configuration.\n\nThe override file is located at `$HOME/.colima/_lima/_config/override.yaml` (or `$LIMA_HOME/_config/override.yaml` if `LIMA_HOME` is set).\n\nSettings in `override.yaml` are applied **before** the instance config, while settings in `default.yaml` are applied **after** (as fallback defaults).\n\n**Note:** Overriding the image is not supported as Colima's image includes bundled dependencies that would be missing in a user-specified image.\n\n### Example: Adding provision scripts\n\nProvision scripts can be added via Lima overrides to run commands during VM boot.\n\n```yaml\n# $HOME/.colima/_lima/_config/override.yaml\nprovision:\n  - mode: system\n    script: |\n      #!/bin/bash\n      set -eux -o pipefail\n      # install additional packages\n      apt-get update && apt-get install -y curl\n```\n\nAlternatively, provision scripts can be specified directly in `colima.yaml`:\n\n```sh\ncolima start --edit\n```\n\n```diff\n- provision: []\n+ provision:\n+   - mode: system\n+     script: |\n+       #!/bin/bash\n+       set -eux -o pipefail\n+       apt-get update && apt-get install -y curl\n```\n\n## How can the VM and its tools be updated?\n\n### Updating Colima\n\n```sh\nbrew upgrade colima\n```\n\nAfter upgrading, delete and recreate the instance to use the latest VM image:\n\n```sh\ncolima delete\ncolima start\n```\n\nTo test the upgrade without affecting the existing setup, use a separate profile:\n\n```sh\ncolima start debug\n```\n\n### Updating the container runtime\n\nFrom v0.7.6, the container runtime (Docker, containerd) can be updated independently:\n\n```sh\ncolima update\n```\n\nThis updates Docker (or containerd) to the latest version without needing to update Colima itself.\n\n### Accessing the Virtual Machine\n\nSSH into the VM to inspect or modify it directly:\n\n```sh\ncolima ssh\n```\n\nRun a single command without an interactive session:\n\n```sh\ncolima ssh -- uname -a\n```\n\n## Troubleshooting\n\nThese are some common issues reported by users and how to troubleshoot them.\n\n### Colima not starting\n\nThere are multiple reasons that could cause Colima to fail to start.\n\n#### Broken status\n\nThis is the case when the output of `colima list` shows a broken status. This can happen due to macOS restart.\n\n```\ncolima list\nPROFILE    STATUS     ARCH       CPUS    MEMORY    DISK     RUNTIME    ADDRESS\ndefault    Broken     aarch64    2       2GiB      60GiB\n```\nThis can be fixed by forcefully stopping Colima. The state will be changed to `Stopped` and it should start up normally afterwards.\n\n```\ncolima stop --force\n```\n\n#### FATA[0000] error starting vm: error at 'starting': exit status 1\n\nThis indicates that a fatal error is preventing Colima from starting, you can enable the debug log with `--verbose` flag to get more info.\n\nIf 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.\n\n1. Running on a device without virtualization support.\n2. Running an x86_64 version of homebrew (and Colima) on an M1 device.\n\n### Issues after an upgrade\n\nThe recommended way to troubleshoot after an upgrade is to test with a separate profile.\n\n```sh\n# start with a profile named 'debug'\ncolima start debug\n```\nIf the separate profile starts successfully without issues, then the issue would be resolved by resetting the default profile.\n\n```\ncolima delete\ncolima start\n```\n\n### Colima cannot access the internet.\n\nFailure for Colima to access the internet is usually down to DNS.\n\nTry custom DNS server(s)\n\n```sh\ncolima start --dns 8.8.8.8 --dns 1.1.1.1\n```\n\nPing an internet address from within the VM to ascertain\n\n```\ncolima ssh -- ping -c4 google.com\nPING google.com (216.58.223.238): 56 data bytes\n64 bytes from 216.58.223.238: seq=0 ttl=42 time=0.082 ms\n64 bytes from 216.58.223.238: seq=1 ttl=42 time=0.557 ms\n64 bytes from 216.58.223.238: seq=2 ttl=42 time=0.465 ms\n64 bytes from 216.58.223.238: seq=3 ttl=42 time=0.457 ms\n\n--- google.com ping statistics ---\n4 packets transmitted, 4 packets received, 0% packet loss\nround-trip min/avg/max = 0.082/0.390/0.557 ms\n```\n\n### Docker Compose and Buildx showing runc error\n\n#### Version v0.5.6 or lower\n\nRecent versions of Buildkit may show the following error.\n\n```console\nrunc 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\n```\n\nFrom v0.5.6, start Colima with `--cgroups-v2` flag as a workaround.\n\n**This is fixed in v0.6.0.**\n\n### Issue with Docker bind mount showing empty\n\nWhen 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.\n\nThis 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.\n\n## How can Docker version be updated?\n\nEach Colima release includes the latest Docker version at the time of release.\n\nFrom 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.\n\n## How can I delete container data\n\nFrom 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`.\n\nTo clear all data, `colima delete --data`  should be run instead. The `--data` flag ensures that the container data is also deleted.\n"
  },
  {
    "path": "docs/INSTALL.md",
    "content": "# Installation Options\n\n## Homebrew\n\nStable Version\n\n```\nbrew install colima\n```\n\nDevelopment Version\n\n```\nbrew install --HEAD colima\n```\n\n## MacPorts\n\nStable version\n\n```\nsudo port install colima\n```\n\n## Nix\n\nOnly stable Version\n\n```\nnix-env -i colima\n```\n\nOr using solely in a `nix-shell`\n\n```\nnix-shell -p colima\n```\n\n## Arch\n\nInstall dependencies\n```\nsudo pacman -S qemu-full go docker\n```\nInstall Lima and Colima from Aur\n```\nyay -S lima-bin colima-bin\n```\n\n\n## Binary\n\nBinaries are available with every release on the [releases page](https://github.com/abiosoft/colima/releases).\n\n```sh\n# download binary\ncurl -LO https://github.com/abiosoft/colima/releases/latest/download/colima-$(uname)-$(uname -m)\n\n# install in $PATH\nsudo install colima-$(uname)-$(uname -m) /usr/local/bin/colima\n```\n\n## Building from Source\n\nRequires [Go](https://golang.org).\n\n```sh\n# clone repo and cd into it\ngit clone https://github.com/abiosoft/colima\ncd colima\nmake\nsudo make install\n```\n"
  },
  {
    "path": "embedded/defaults/abort.yaml",
    "content": "# ============================================================================================ #\n# To abort, delete the contents of this file including the comments and save as an empty file\n# ============================================================================================ #\n"
  },
  {
    "path": "embedded/defaults/colima.yaml",
    "content": "# Number of CPUs to be allocated to the virtual machine.\n# Default: 2\ncpu: 2\n\n# Size of the disk in GiB to be allocated to the virtual machine for container data.\n# NOTE: value can only be increased after virtual machine has been created.\n#\n# Default: 100\ndisk: 100\n\n# Size of the memory in GiB to be allocated to the virtual machine.\n# Default: 2\nmemory: 2\n\n# Architecture of the virtual machine (x86_64, aarch64, host).\n#\n# NOTE: value cannot be changed after virtual machine is created.\n# Default: host\narch: host\n\n# Container runtime to be used (docker, containerd).\n#\n# NOTE: value cannot be changed after virtual machine is created.\n# Default: docker\nruntime: docker\n\n# AI model runner (docker, ramalama).\n# Both require krunkit VM type for GPU access.\n# docker: Uses Docker Model Runner.\n# ramalama: Uses Ramalama.\n#\n# Default: docker\nmodelRunner: docker\n\n# Set custom hostname for the virtual machine.\n# Default: colima\n#          colima-profile_name for other profiles\nhostname: null\n\n# Kubernetes configuration for the virtual machine.\nkubernetes:\n  # Enable kubernetes.\n  # Default: false\n  enabled: false\n\n  # Kubernetes version to use.\n  # This needs to exactly match a k3s version https://github.com/k3s-io/k3s/releases\n  # Default: latest stable release\n  version: v1.35.0+k3s1\n\n  # Additional args to pass to k3s https://docs.k3s.io/cli/server\n  # Default: traefik is disabled\n  k3sArgs: [--disable=traefik]\n\n  # Kubernetes port to listen on\n  # A common port is 6443, though left unbound to ensure no port conflicts\n  # Default: pick random unbound port\n  port: 0\n\n# Auto-activate on the Host for client access.\n# Setting to true does the following on startup\n#  - sets as active Docker context (for Docker runtime).\n#  - sets as active Kubernetes context (if Kubernetes is enabled).\n#  - sets as active Incus remote (for Incus runtime).\n# Default: true\nautoActivate: true\n\n# Network configurations for the virtual machine.\nnetwork:\n  # Assign reachable IP address to the virtual machine.\n  # NOTE: this is currently macOS only and ignored on Linux.\n  # Default: false\n  address: false\n\n  # Network mode for the virtual machine (shared, bridged).\n  # NOTE: this is currently macOS only and ignored on Linux.\n  # Default: shared\n  mode: shared\n\n  # Network interface to use for bridged mode.\n  # This is only used when mode is set to bridged.\n  # NOTE: this is currently macOS only and ignored on Linux.\n  # Default: en0\n  interface: en0\n\n  # Use the assigned IP address as the preferred route for the VM.\n  # Note: this only has an effect when `address` is set to true.\n  # Default: false\n  preferredRoute: false\n\n  # Custom DNS resolvers for the virtual machine.\n  #\n  # EXAMPLE\n  # dns: [8.8.8.8, 1.1.1.1]\n  #\n  # Default: []\n  dns: []\n\n  # DNS hostnames to resolve to custom targets using the internal resolver.\n  # This setting has no effect if a custom DNS resolver list is supplied above.\n  # It does not configure the /etc/hosts files of any machine or container.\n  # The value can be an IP address or another host.\n  #\n  # EXAMPLE\n  # dnsHosts:\n  #   example.com: 1.2.3.4\n  dnsHosts:\n    host.docker.internal: host.lima.internal\n\n  # Replicate host IP addresses in the VM. This enables port forwarding to specific\n  # host IP addresses.\n  #   e.g. `docker run --port 10.0.1.2:8080:8080 alpine` would only forward to the\n  #   specified IP address.\n  #\n  # Default: false\n  hostAddresses: false\n\n  # Custom gateway address for the virtual machine.\n  # The last octet needs to be 2.\n  #\n  # EXAMPLE\n  # gatewayAddress: 192.168.10.2\n  #\n  # Default: 192.168.5.2\n  gatewayAddress: 192.168.5.2\n\n# ===================================================================== #\n# ADVANCED CONFIGURATION\n# ===================================================================== #\n\n# Forward the host's SSH agent to the virtual machine.\n# Default: false\nforwardAgent: false\n\n# Docker daemon configuration that maps directly to daemon.json.\n# https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file.\n# NOTE: some settings may affect Colima's ability to start docker. e.g. `hosts`.\n#\n# EXAMPLE - disable buildkit\n# docker:\n#   features:\n#     buildkit: false\n#\n# EXAMPLE - add insecure registries\n# docker:\n#   insecure-registries:\n#     - myregistry.com:5000\n#     - host.docker.internal:5000\n#\n# Colima default behaviour: buildkit enabled\n# Default: {}\ndocker: {}\n\n# Virtual Machine type (krunkit, qemu, vz)\n# NOTE: this is macOS 13 only. For Linux and macOS <13.0, qemu is always used.\n#\n# vz is macOS virtualization framework and requires macOS 13.\n# krunkit runs super‑light VMs on macOS/ARM64 with a focus on GPU access. It is experimental.\n#\n# NOTE: value cannot be changed after virtual machine is created.\n# Default: qemu\nvmType: qemu\n\n# Port forwarder for the virtual machine (ssh, grpc, none).\n# ssh is more stable but supports only TCP.\n# grpc supports both TCP and UDP, but is experimental.\n# none disables port forwarding.\n#\n# Default: ssh\nportForwarder: ssh\n\n# Utilise rosetta for amd64 emulation (requires m1 mac and vmType `vz`)\n# Default: false\nrosetta: false\n\n# Enable foreign architecture emulation via binfmt (e.g. amd64 on arm64, arm64 on amd64)\n# Default: true\nbinfmt: true\n\n# Enable nested virtualization for the virtual machine (requires m3 mac and vmType `vz`)\n# Default: false\nnestedVirtualization: false\n\n# Volume mount driver for the virtual machine (virtiofs, 9p, sshfs).\n#\n# virtiofs is limited to macOS and vmType `vz`. It is the fastest of the options.\n#\n# 9p is the recommended and the most stable option for vmType `qemu`.\n#\n# sshfs is faster than 9p but the least reliable of the options (when there are lots\n# of concurrent reads or writes).\n#\n# NOTE: value cannot be changed after virtual machine is created.\n# Default: virtiofs (for vz), sshfs (for qemu)\nmountType: sshfs\n\n# Propagate inotify file events to the VM.\n# NOTE: this is experimental.\nmountInotify: false\n\n# The CPU type for the virtual machine (requires vmType `qemu`).\n# Options available for host emulation can be checked with: `qemu-system-$(arch) -cpu help`.\n# Instructions are also supported by appending to the cpu type e.g. \"qemu64,+ssse3\".\n# Default: host\ncpuType: host\n\n# Custom provision scripts for the virtual machine.\n# Provisioning scripts are executed on startup and therefore needs to be idempotent.\n#\n# EXAMPLE - script executed as root\n# provision:\n#   - mode: system\n#     script: apt-get install htop vim\n#\n# EXAMPLE - script executed as user\n# provision:\n#   - mode: user\n#     script: |\n#       [ -f ~/.provision ] && exit 0;\n#       echo provisioning as $USER...\n#       touch ~/.provision\n#\n# EXAMPLE - script executed after VM boot, before container runtimes start\n# provision:\n#   - mode: after-boot\n#     script: echo \"VM is up, containers not yet started\"\n#\n# EXAMPLE - script executed after VM and container runtimes are ready\n# provision:\n#   - mode: ready\n#     script: echo \"everything is ready\"\n#\n# Default: []\nprovision: []\n\n# Modify ~/.ssh/config automatically to include a SSH config for the virtual machine.\n# SSH config will still be generated in $COLIMA_HOME/ssh_config regardless.\n# Default: true\nsshConfig: true\n\n# The port number for the SSH server for the virtual machine.\n# When set to 0, a random available port is used.\n#\n# Default: 0\nsshPort: 0\n\n# Configure volume mounts for the virtual machine.\n# Colima mounts user's home directory by default to provide a familiar\n# user experience.\n#\n# EXAMPLE\n# mounts:\n#   - location: ~/secrets\n#     writable: false\n#   - location: ~/projects\n#     writable: true\n#\n# Colima default behaviour: $HOME is mounted as writable.\n# Default: []\nmounts: []\n\n# Specify a custom disk image for the virtual machine.\n# When not specified, Colima downloads an appropriate disk image from Github at\n# https://github.com/abiosoft/colima-core/releases.\n# The file path to a custom disk image can be specified to override the behaviour.\n#\n# Default: \"\"\ndiskImage: \"\"\n\n# Size of the disk in GiB for the root filesystem of the virtual machine.\n# This value is ignored if no runtime is in use. i.e. `none` runtime.\n# Default: 20\nrootDisk: 20\n\n# Environment variables for the virtual machine.\n#\n# EXAMPLE\n# env:\n#   KEY: value\n#   ANOTHER_KEY: another value\n#\n# Default: {}\nenv: {}\n"
  },
  {
    "path": "embedded/defaults/template.yaml",
    "content": "# New instances will be created with the following configurations.\n"
  },
  {
    "path": "embedded/embed.go",
    "content": "package embedded\n\nimport (\n\t\"embed\"\n)\n\n//go:embed network k3s defaults images\nvar fs embed.FS\n\n// FS returns the underlying embed.FS\nfunc FS() embed.FS { return fs }\n\nfunc read(file string) ([]byte, error) { return fs.ReadFile(file) }\n\n// Read reads the content of file\nfunc Read(file string) ([]byte, error) { return read(file) }\n\n// ReadString reads the content of file as string\nfunc ReadString(file string) (string, error) {\n\tb, err := read(file)\n\treturn string(b), err\n}\n"
  },
  {
    "path": "embedded/images/images.txt",
    "content": "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\narm64 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\narm64 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\narm64 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\namd64 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\namd64 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\namd64 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\namd64 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\n"
  },
  {
    "path": "embedded/images/images_sha.sh",
    "content": "#!/usr/bin/env bash\n\nset -eux\n\nBASE_URL=https://github.com/abiosoft/colima-core/releases/download\nBASE_FILENAME=ubuntu-24.04-minimal-cloudimg\nVERSION=v0.10.1\nRUNTIMES=\"none docker containerd incus\"\nARCHS=\"arm64 amd64\"\n\nDIR=\"$(dirname $0)\"\nFILE=\"${DIR}/images.txt\"\n\n# reset output files\necho -n >$FILE\n\nfor arch in ${ARCHS}; do\n    for runtime in ${RUNTIMES}; do\n        URL=\"${BASE_URL}/${VERSION}/${BASE_FILENAME}-${arch}-${runtime}.qcow2\"\n        SHA=\"$(curl -sL ${URL}.sha512sum)\"\n        echo \"$arch $runtime ${URL} ${SHA}\" >>$FILE\n    done\ndone\n"
  },
  {
    "path": "embedded/k3s/flannel.json",
    "content": "{\n    \"name\": \"cbr0\",\n    \"cniVersion\": \"0.3.1\",\n    \"plugins\": [\n        {\n            \"type\": \"flannel\",\n            \"delegate\": {\n                \"hairpinMode\": true,\n                \"forceAddress\": true,\n                \"isDefaultGateway\": true\n            }\n        },\n        {\n            \"type\": \"portmap\",\n            \"capabilities\": {\n                \"portMappings\": true\n            }\n        }\n    ]\n}"
  },
  {
    "path": "embedded/network/sudo.txt",
    "content": "# starting vmnet daemon\n%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 *\n%staff ALL=(root:wheel) NOPASSWD:NOSETENV: /opt/colima/bin/socket_vmnet --vmnet-mode bridged --socket-group staff *\n# terminating vmnet daemon\n%staff ALL=(root:wheel) NOPASSWD:NOSETENV: /usr/bin/pkill -F /opt/colima/run/*.pid\n# validating vmnet daemon\n%staff ALL=(root:wheel) NOPASSWD:NOSETENV: /usr/bin/pkill -0 -F /opt/colima/run/*.pid\n# adding route to Incus container network\n%staff ALL=(root:wheel) NOPASSWD:NOSETENV: /sbin/route add -net 192.168.100.0/24 *\n# removing route to Incus container network\n%staff ALL=(root:wheel) NOPASSWD:NOSETENV: /sbin/route delete -net 192.168.100.0/24\n"
  },
  {
    "path": "embedded/sudoers.go",
    "content": "package embedded\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst sudoersPath = \"/etc/sudoers.d/colima\"\nconst sudoersEmbeddedPath = \"network/sudo.txt\"\n\n// SudoersInstaller provides the ability to run commands on the host\n// for installing the sudoers file.\ntype SudoersInstaller interface {\n\tRunInteractive(args ...string) error\n\tRunWith(stdin io.Reader, stdout io.Writer, args ...string) error\n}\n\n// SudoersInstalled checks if the sudoers file contains the expected embedded content.\nfunc SudoersInstalled() bool {\n\ttxt, err := Read(sudoersEmbeddedPath)\n\tif err != nil {\n\t\treturn false\n\t}\n\tb, err := os.ReadFile(sudoersPath)\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn bytes.Contains(b, txt)\n}\n\n// InstallSudoers installs the embedded sudoers file if it is not already\n// installed with the expected content. This may prompt for a sudo password.\nfunc InstallSudoers(host SudoersInstaller) error {\n\tif SudoersInstalled() {\n\t\treturn nil\n\t}\n\n\ttxt, err := ReadString(sudoersEmbeddedPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading embedded sudoers file: %w\", err)\n\t}\n\n\tlog.Println(\"setting up network permissions, sudo password may be required\")\n\n\tdir := filepath.Dir(sudoersPath)\n\tif err := host.RunInteractive(\"sudo\", \"mkdir\", \"-p\", dir); err != nil {\n\t\treturn fmt.Errorf(\"error preparing sudoers directory: %w\", err)\n\t}\n\n\tstdin := strings.NewReader(txt)\n\tstdout := &bytes.Buffer{}\n\tif err := host.RunWith(stdin, stdout, \"sudo\", \"sh\", \"-c\", \"cat > \"+sudoersPath); err != nil {\n\t\treturn fmt.Errorf(\"error writing sudoers file: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "environment/container/containerd/buildkitd.toml",
    "content": "[worker.oci]\nenabled = false\n\n[worker.containerd]\nenabled = true\n\n[grpc]\ngid = 1000\n"
  },
  {
    "path": "environment/container/containerd/config.toml",
    "content": "[grpc]\ngid = 1000\n"
  },
  {
    "path": "environment/container/containerd/containerd.go",
    "content": "package containerd\n\nimport (\n\t\"context\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/environment/guest/systemctl\"\n)\n\n// Name is container runtime name\nconst Name = \"containerd\"\n\nvar configDir = func() string { return config.CurrentProfile().ConfigDir() }\n\n// HostSocketFiles returns the path to the socket files on host.\nfunc HostSocketFiles() (files struct {\n\tContainerd string\n\tBuildkitd  string\n}) {\n\tfiles.Containerd = filepath.Join(configDir(), \"containerd.sock\")\n\tfiles.Buildkitd = filepath.Join(configDir(), \"buildkitd.sock\")\n\n\treturn files\n}\n\n// This is written with assumption that Lima is the VM,\n// which provides nerdctl/containerd support out of the box.\n// There may be need to make this flexible for non-Lima VMs.\n\n//go:embed config.toml\nvar containerdConf []byte\n\n//go:embed buildkitd.toml\nvar buildKitConf []byte\n\nconst containerdConfFile = \"/etc/containerd/config.toml\"\nconst buildKitConfFile = \"/etc/buildkit/buildkitd.toml\"\n\nfunc newRuntime(host environment.HostActions, guest environment.GuestActions) environment.Container {\n\treturn &containerdRuntime{\n\t\thost:         host,\n\t\tguest:        guest,\n\t\tsystemctl:    systemctl.New(guest),\n\t\tCommandChain: cli.New(Name),\n\t}\n}\n\nfunc init() {\n\tenvironment.RegisterContainer(Name, newRuntime, false)\n}\n\nvar _ environment.Container = (*containerdRuntime)(nil)\n\ntype containerdRuntime struct {\n\thost      environment.HostActions\n\tguest     environment.GuestActions\n\tsystemctl systemctl.Systemctl\n\tcli.CommandChain\n}\n\nfunc (c containerdRuntime) Name() string {\n\treturn Name\n}\n\nfunc (c containerdRuntime) Provision(ctx context.Context) error {\n\ta := c.Init(ctx)\n\n\t// containerd config\n\ta.Add(func() error {\n\t\tprofilePath := filepath.Join(configDir(), \"containerd\", \"config.toml\")\n\t\tcentralPath := filepath.Join(userConfigDir(), \"containerd\", \"config.toml\")\n\t\treturn c.provisionConfig(profilePath, centralPath, containerdConfFile, containerdConf)\n\t})\n\n\t// buildkitd config\n\ta.Add(func() error {\n\t\tprofilePath := filepath.Join(configDir(), \"containerd\", \"buildkitd.toml\")\n\t\tcentralPath := filepath.Join(userConfigDir(), \"buildkit\", \"buildkitd.toml\")\n\t\treturn c.provisionConfig(profilePath, centralPath, buildKitConfFile, buildKitConf)\n\t})\n\n\treturn a.Exec()\n}\n\n// userConfigDir returns the user config directory following XDG conventions.\n// This is ~/.config on Linux/macOS, used for central config file locations\n// that follow the containerd/buildkit rootless conventions.\nfunc userConfigDir() string {\n\tif dir := os.Getenv(\"XDG_CONFIG_HOME\"); dir != \"\" {\n\t\treturn dir\n\t}\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn filepath.Join(home, \".config\")\n}\n\n// provisionConfig writes a config file to the VM. Config files are resolved\n// in the following order:\n//  1. Per-profile override at ~/.colima/<profile>/containerd/<file>\n//  2. Central config at ~/.config/containerd/<file> (or ~/.config/buildkit/<file>)\n//  3. Embedded default\n//\n// On first run, the default config is written to the central location for\n// user discovery and editing.\nfunc (c containerdRuntime) provisionConfig(profilePath, centralPath, guestPath string, defaultConf []byte) error {\n\t// 1. per-profile override takes highest priority\n\tif data, err := os.ReadFile(profilePath); err == nil {\n\t\treturn c.guest.Write(guestPath, data)\n\t}\n\n\t// 2. central config\n\tif data, err := os.ReadFile(centralPath); err == nil {\n\t\treturn c.guest.Write(guestPath, data)\n\t}\n\n\t// 3. no user config found; write the default to the central location\n\t// for discoverability and use it\n\tif err := os.MkdirAll(filepath.Dir(centralPath), 0755); err != nil {\n\t\treturn fmt.Errorf(\"error creating config directory: %w\", err)\n\t}\n\tif err := os.WriteFile(centralPath, defaultConf, 0644); err != nil {\n\t\treturn fmt.Errorf(\"error writing default config: %w\", err)\n\t}\n\n\treturn c.guest.Write(guestPath, defaultConf)\n}\n\nfunc (c containerdRuntime) Start(ctx context.Context) error {\n\ta := c.Init(ctx)\n\n\ta.Add(func() error {\n\t\treturn c.systemctl.Restart(\"containerd.service\")\n\t})\n\n\t// service startup takes few seconds, retry at most 10 times before giving up.\n\ta.Retry(\"\", time.Second*5, 10, func(int) error {\n\t\treturn c.guest.RunQuiet(\"sudo\", \"nerdctl\", \"info\")\n\t})\n\n\ta.Add(func() error {\n\t\treturn c.systemctl.Start(\"buildkit.service\")\n\t})\n\n\treturn a.Exec()\n}\n\nfunc (c containerdRuntime) Running(ctx context.Context) bool {\n\treturn c.systemctl.Active(\"containerd.service\")\n}\n\nfunc (c containerdRuntime) Stop(ctx context.Context, force bool) error {\n\ta := c.Init(ctx)\n\ta.Add(func() error {\n\t\treturn c.systemctl.Stop(\"containerd.service\", force)\n\t})\n\treturn a.Exec()\n}\n\nfunc (c containerdRuntime) Teardown(context.Context) error {\n\t// teardown not needed, will be part of VM teardown\n\treturn nil\n}\n\nfunc (c containerdRuntime) Dependencies() []string {\n\t// no dependencies\n\treturn nil\n}\n\nfunc (c containerdRuntime) Version(ctx context.Context) string {\n\tversion, _ := c.guest.RunOutput(\"sudo\", \"nerdctl\", \"version\", \"--format\", `client: {{.Client.Version}}{{printf \"\\n\"}}server: {{(index .Server.Components 0).Version}}`)\n\treturn version\n}\n\nfunc (c *containerdRuntime) Update(ctx context.Context) (bool, error) {\n\treturn false, fmt.Errorf(\"update not supported for the %s runtime\", Name)\n}\n\n// DataDirs represents the data disk for the container runtime.\nfunc DataDisk() environment.DataDisk {\n\treturn environment.DataDisk{\n\t\tDirs:   diskDirs,\n\t\tFSType: \"ext4\",\n\t\tPreMount: []string{\n\t\t\t\"systemctl stop containerd.service\",\n\t\t\t\"systemctl stop buildkit.service\",\n\t\t},\n\t}\n}\n\nvar diskDirs = []environment.DiskDir{\n\t{Name: \"containerd\", Path: \"/var/lib/containerd\"},\n\t{Name: \"buildkit\", Path: \"/var/lib/buildkit\"},\n\t{Name: \"nerdctl\", Path: \"/var/lib/nerdctl\"},\n\t{Name: \"rancher\", Path: \"/var/lib/rancher\"},\n\t{Name: \"cni\", Path: \"/var/lib/cni\"},\n}\n"
  },
  {
    "path": "environment/container/docker/config.toml",
    "content": "disabled_plugins = [\"cri\"]\n\n[grpc]\ngid = 1000\n"
  },
  {
    "path": "environment/container/docker/containerd.go",
    "content": "package docker\n\nimport (\n\t\"context\"\n\t_ \"embed\"\n\t\"fmt\"\n)\n\nconst containerdConfFile = \"/etc/containerd/config.toml\"\nconst containerdConfFileBackup = \"/etc/containerd/config.colima.bak.toml\"\n\n//go:embed config.toml\nvar containerdConf []byte\n\nfunc (d dockerRuntime) provisionContainerd(ctx context.Context) error {\n\ta := d.Init(ctx)\n\n\t// containerd config\n\ta.Add(func() error {\n\t\tif _, err := d.guest.Stat(containerdConfFileBackup); err == nil {\n\t\t\t// backup already exists, no need to overwrite\n\t\t\treturn nil\n\t\t}\n\n\t\t// backup existing containerd config\n\t\tif err := d.guest.Run(\"sudo\", \"cp\", containerdConfFile, containerdConfFileBackup); err != nil {\n\t\t\treturn fmt.Errorf(\"error backing up %s: %w\", containerdConfFile, err)\n\t\t}\n\n\t\t// write new containerd config\n\t\tif err := d.guest.Write(containerdConfFile, containerdConf); err != nil {\n\t\t\treturn fmt.Errorf(\"error writing %s: %w\", containerdConfFile, err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\ta.Add(func() error {\n\t\t// restart containerd service\n\t\treturn d.systemctl.Restart(\"containerd.service\")\n\t})\n\n\treturn a.Exec()\n}\n"
  },
  {
    "path": "environment/container/docker/context.go",
    "content": "package docker\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/abiosoft/colima/config\"\n)\n\nvar configDir = func() string { return config.CurrentProfile().ConfigDir() }\n\n// HostSocketFile returns the path to the docker socket on host.\nfunc HostSocketFile() string { return filepath.Join(configDir(), \"docker.sock\") }\nfunc LegacyDefaultHostSocketFile() string {\n\treturn filepath.Join(filepath.Dir(configDir()), \"docker.sock\")\n}\n\nfunc (d dockerRuntime) contextCreated() bool {\n\treturn d.host.RunQuiet(\"docker\", \"context\", \"inspect\", config.CurrentProfile().ID) == nil\n}\n\nfunc (d dockerRuntime) setupContext() error {\n\tif d.contextCreated() {\n\t\treturn nil\n\t}\n\n\tprofile := config.CurrentProfile()\n\n\treturn d.host.Run(\"docker\", \"context\", \"create\", profile.ID,\n\t\t\"--description\", profile.DisplayName,\n\t\t\"--docker\", \"host=unix://\"+HostSocketFile(),\n\t)\n}\n\nfunc (d dockerRuntime) useContext() error {\n\treturn d.host.Run(\"docker\", \"context\", \"use\", config.CurrentProfile().ID)\n}\n\nfunc (d dockerRuntime) teardownContext() error {\n\tif !d.contextCreated() {\n\t\treturn nil\n\t}\n\n\treturn d.host.Run(\"docker\", \"context\", \"rm\", \"--force\", config.CurrentProfile().ID)\n}\n"
  },
  {
    "path": "environment/container/docker/daemon.go",
    "content": "package docker\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n)\n\nconst daemonFile = \"/etc/docker/daemon.json\"\nconst hostGatewayIPKey = \"host-gateway-ip\"\n\nfunc getHostGatewayIp(d dockerRuntime, conf map[string]any) (string, error) {\n\t// get host-gateway ip from the guest\n\tip, err := d.guest.RunOutput(\"sh\", \"-c\", \"grep 'host.lima.internal' /etc/hosts | awk -F' ' '{print $1}'\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error retrieving host gateway IP address: %w\", err)\n\t}\n\t// if set by the user, use the user specified value\n\tif _, ok := conf[hostGatewayIPKey]; ok {\n\t\tif gip, ok := conf[hostGatewayIPKey].(string); ok {\n\t\t\tip = gip\n\t\t}\n\t}\n\tif net.ParseIP(ip) == nil {\n\t\treturn \"\", fmt.Errorf(\"invalid host gateway IP address: '%s'\", ip)\n\t}\n\n\treturn ip, nil\n}\n\nfunc resolveHostProxy(hostProxy, hostGateway string) string {\n\tu, err := url.Parse(hostProxy)\n\tif err != nil {\n\t\treturn hostProxy\n\t}\n\tips, err := net.LookupIP(u.Hostname())\n\tif err != nil {\n\t\treturn hostProxy\n\t}\n\tfor _, ip := range ips {\n\t\tif ip.IsLoopback() {\n\t\t\tnewHost := hostGateway\n\t\t\tif u.Port() != \"\" {\n\t\t\t\tnewHost = net.JoinHostPort(newHost, u.Port())\n\t\t\t}\n\t\t\tu.Host = newHost\n\t\t\thostProxy = u.String()\n\t\t\tbreak\n\t\t}\n\t}\n\treturn hostProxy\n}\n\nfunc (d dockerRuntime) createDaemonFile(conf map[string]any, env map[string]string) error {\n\tif conf == nil {\n\t\tconf = map[string]any{}\n\t}\n\n\t// enable buildkit (if not set by user)\n\tif _, ok := conf[\"features\"]; !ok {\n\t\tconf[\"features\"] = map[string]any{\n\t\t\t\"buildkit\":               true,\n\t\t\t\"containerd-snapshotter\": true,\n\t\t}\n\t}\n\n\t// enable cgroupfs for k3s (if not set by user)\n\tif _, ok := conf[\"exec-opts\"]; !ok {\n\t\tconf[\"exec-opts\"] = []string{\"native.cgroupdriver=cgroupfs\"}\n\t} else if opts, ok := conf[\"exec-opts\"].([]string); ok {\n\t\tconf[\"exec-opts\"] = append(opts, \"native.cgroupdriver=cgroupfs\")\n\t}\n\t// remove host-gateway-ip if set by the user\n\t// to avoid clash with systemd configuration\n\tdelete(conf, hostGatewayIPKey)\n\n\t// add proxy vars if set\n\t// according to https://docs.docker.com/config/daemon/systemd/#httphttps-proxy\n\tif vars := d.proxyEnvVars(env); !vars.empty() {\n\t\tproxyConf := map[string]any{}\n\t\thostGatewayIP, err := getHostGatewayIp(d, conf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif vars.http != \"\" {\n\t\t\tproxyConf[\"http-proxy\"] = resolveHostProxy(vars.http, hostGatewayIP)\n\t\t}\n\t\tif vars.https != \"\" {\n\t\t\tproxyConf[\"https-proxy\"] = resolveHostProxy(vars.https, hostGatewayIP)\n\t\t}\n\t\tif vars.no != \"\" {\n\t\t\tproxyConf[\"no-proxy\"] = vars.no\n\t\t}\n\t\tconf[\"proxies\"] = proxyConf\n\t}\n\n\tb, err := json.MarshalIndent(conf, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshaling daemon.json: %w\", err)\n\t}\n\treturn d.guest.Write(daemonFile, b)\n}\n\nfunc (d dockerRuntime) addHostGateway(conf map[string]any) error {\n\t// get host-gateway ip from the guest\n\tip, err := getHostGatewayIp(d, conf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// set host-gateway ip as systemd service file\n\tcontent := fmt.Sprintf(systemdUnitFileContent, ip)\n\tif err := d.guest.Write(systemdUnitFilename, []byte(content)); err != nil {\n\t\treturn fmt.Errorf(\"error creating systemd unit file: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d dockerRuntime) reloadAndRestartSystemdService() error {\n\tif err := d.systemctl.DaemonReload(); err != nil {\n\t\treturn fmt.Errorf(\"error reloading systemd daemon: %w\", err)\n\t}\n\tif err := d.systemctl.Restart(\"docker.service\"); err != nil {\n\t\treturn fmt.Errorf(\"error restarting docker: %w\", err)\n\t}\n\treturn nil\n}\n\nconst systemdUnitFilename = \"/etc/systemd/system/docker.service.d/docker.conf\"\nconst systemdUnitFileContent string = `\n[Service]\nLimitNOFILE=infinity\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --host-gateway-ip=%s\n`\n"
  },
  {
    "path": "environment/container/docker/docker.go",
    "content": "package docker\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/environment/guest/systemctl\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/debutil\"\n)\n\n// Name is container runtime name.\nconst Name = \"docker\"\n\nvar _ environment.Container = (*dockerRuntime)(nil)\n\nfunc init() {\n\tenvironment.RegisterContainer(Name, newRuntime, false)\n}\n\ntype dockerRuntime struct {\n\thost      environment.HostActions\n\tguest     environment.GuestActions\n\tsystemctl systemctl.Systemctl\n\tcli.CommandChain\n}\n\n// newRuntime creates a new docker runtime.\nfunc newRuntime(host environment.HostActions, guest environment.GuestActions) environment.Container {\n\treturn &dockerRuntime{\n\t\thost:         host,\n\t\tguest:        guest,\n\t\tsystemctl:    systemctl.New(guest),\n\t\tCommandChain: cli.New(Name),\n\t}\n}\n\nfunc (d dockerRuntime) Name() string {\n\treturn Name\n}\n\nfunc (d dockerRuntime) Provision(ctx context.Context) error {\n\ta := d.Init(ctx)\n\tlog := d.Logger(ctx)\n\n\tconf, _ := ctx.Value(config.CtxKey()).(config.Config)\n\n\t// provision containerd\n\ta.Add(func() error {\n\t\treturn d.provisionContainerd(ctx)\n\t})\n\n\t// daemon.json\n\ta.Add(func() error {\n\t\t// these are not fatal errors\n\t\tif err := d.createDaemonFile(conf.Docker, conf.Env); err != nil {\n\t\t\tlog.Warnln(err)\n\t\t}\n\t\tif err := d.addHostGateway(conf.Docker); err != nil {\n\t\t\tlog.Warnln(err)\n\t\t}\n\t\tif err := d.reloadAndRestartSystemdService(); err != nil {\n\t\t\tlog.Warnln(err)\n\t\t}\n\t\treturn nil\n\t})\n\n\t// docker context\n\ta.Add(d.setupContext)\n\tif conf.AutoActivate() {\n\t\ta.Add(d.useContext)\n\t}\n\n\treturn a.Exec()\n}\n\nfunc (d dockerRuntime) Start(ctx context.Context) error {\n\ta := d.Init(ctx)\n\n\ta.Retry(\"\", time.Second, 60, func(int) error {\n\t\treturn d.systemctl.Start(\"docker.service\")\n\t})\n\n\t// service startup takes few seconds, retry for a minute before giving up.\n\ta.Retry(\"\", time.Second, 60, func(int) error {\n\t\treturn d.guest.RunQuiet(\"sudo\", \"docker\", \"info\")\n\t})\n\n\t// ensure docker is accessible without root\n\t// otherwise, restart to ensure user is added to docker group\n\ta.Add(func() error {\n\t\tif err := d.guest.RunQuiet(\"docker\", \"info\"); err == nil {\n\t\t\treturn nil\n\t\t}\n\t\tctx := context.WithValue(ctx, cli.CtxKeyQuiet, true)\n\t\treturn d.guest.Restart(ctx)\n\t})\n\n\treturn a.Exec()\n}\n\nfunc (d dockerRuntime) Running(ctx context.Context) bool {\n\treturn d.systemctl.Active(\"docker.service\")\n}\n\nfunc (d dockerRuntime) Stop(ctx context.Context, force bool) error {\n\ta := d.Init(ctx)\n\n\ta.Add(func() error {\n\t\tif !d.Running(ctx) {\n\t\t\treturn nil\n\t\t}\n\t\treturn d.systemctl.Stop(\"docker.service\", force)\n\t})\n\n\t// clear docker context settings\n\t// since the container runtime can be changed on startup,\n\t// it is better to not leave unnecessary traces behind\n\ta.Add(d.teardownContext)\n\n\treturn a.Exec()\n}\n\nfunc (d dockerRuntime) Teardown(ctx context.Context) error {\n\ta := d.Init(ctx)\n\n\t// clear docker context settings\n\ta.Add(d.teardownContext)\n\n\treturn a.Exec()\n}\n\nfunc (d dockerRuntime) Dependencies() []string {\n\treturn []string{\"docker\"}\n}\n\nfunc (d dockerRuntime) Version(ctx context.Context) string {\n\tversion, _ := d.host.RunOutput(\"docker\", \"--context\", config.CurrentProfile().ID, \"version\", \"--format\", `client: v{{.Client.Version}}{{printf \"\\n\"}}server: v{{.Server.Version}}`)\n\treturn version\n}\n\nfunc (d *dockerRuntime) Update(ctx context.Context) (bool, error) {\n\tpackages := []string{\n\t\t\"docker-ce\",\n\t\t\"docker-ce-cli\",\n\t\t\"containerd.io\",\n\t}\n\n\treturn debutil.UpdateRuntime(ctx, d.guest, d, packages...)\n}\n\n// DataDirs represents the data disk for the container runtime.\nfunc DataDisk() environment.DataDisk {\n\treturn environment.DataDisk{\n\t\tDirs:   diskDirs,\n\t\tFSType: \"ext4\",\n\t\tPreMount: []string{\n\t\t\t\"systemctl stop docker.service\",\n\t\t\t\"systemctl stop containerd.service\",\n\t\t},\n\t}\n}\n\nvar diskDirs = []environment.DiskDir{\n\t{Name: \"docker\", Path: \"/var/lib/docker\"},\n\t{Name: \"containerd\", Path: \"/var/lib/containerd\"},\n\t{Name: \"rancher\", Path: \"/var/lib/rancher\"},\n\t{Name: \"cni\", Path: \"/var/lib/cni\"},\n\t{Name: \"ramalama\", Path: \"/var/lib/ramalama\"},\n}\n\n// DockerDir returns the path to Docker config.\nfunc DockerDir() string {\n\t// if DOCKER_CONFIG env var is set, obey it.\n\tif dir := os.Getenv(\"DOCKER_CONFIG\"); dir != \"\" {\n\t\treturn dir\n\t}\n\n\treturn filepath.Join(util.HomeDir(), \".docker\")\n}\n"
  },
  {
    "path": "environment/container/docker/proxy.go",
    "content": "package docker\n\nimport (\n\t\"os\"\n\t\"strings\"\n)\n\ntype proxyVars struct {\n\thttp  string\n\thttps string\n\tno    string\n}\n\nfunc (p proxyVars) empty() bool {\n\treturn p.http == \"\" && p.https == \"\"\n}\n\ntype proxyVarKey string\n\nvar (\n\thttpProxy  proxyVarKey = \"http_proxy\"\n\thttpsProxy proxyVarKey = \"https_proxy\"\n\tnoProxy    proxyVarKey = \"no_proxy\"\n)\n\n// keys return both the lower case and upper case env var keys.\n// e.g. http_proxy and HTTP_PROXY\nfunc (p proxyVarKey) Keys() []string {\n\treturn []string{string(p), strings.ToUpper(string(p))}\n}\n\nfunc (d dockerRuntime) proxyEnvVars(env map[string]string) proxyVars {\n\tgetVal := func(key proxyVarKey) string {\n\t\tfor _, k := range key.Keys() {\n\t\t\t// config\n\t\t\tif val, ok := env[k]; ok {\n\t\t\t\treturn val\n\t\t\t}\n\t\t\t// os\n\t\t\tif val := os.Getenv(k); val != \"\" {\n\t\t\t\treturn val\n\t\t\t}\n\t\t}\n\t\treturn \"\"\n\t}\n\n\treturn proxyVars{\n\t\thttp:  getVal(httpProxy),\n\t\thttps: getVal(httpsProxy),\n\t\tno:    getVal(noProxy),\n\t}\n}\n"
  },
  {
    "path": "environment/container/incus/config.yaml",
    "content": "networks:\n  - config:\n      ipv4.address: {{.BridgeGateway}}\n      ipv4.nat: \"true\"\n      ipv6.address: auto\n    description: \"\"\n    name: {{.Interface}}\n    type: \"\"\n    project: default\n{{ if .SetStorage }}\nstorage_pools:\n  - config:\n      size: {{.Disk}}GiB\n    description: \"\"\n    name: default\n    driver: zfs\n{{ end }}\nprofiles:\n  - config: {}\n    description: \"\"\n    devices:\n      eth0:\n        name: eth0\n        network: {{.Interface}}\n        type: nic\n      root:\n        path: /\n        pool: default\n        type: disk\n    name: default\nprojects: []\ncluster: null\n"
  },
  {
    "path": "environment/container/incus/incus.go",
    "content": "package incus\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t_ \"embed\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/environment/guest/systemctl\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/debutil\"\n)\n\nconst incusBridgeInterface = \"incusbr0\"\n\nfunc newRuntime(host environment.HostActions, guest environment.GuestActions) environment.Container {\n\treturn &incusRuntime{\n\t\thost:         host,\n\t\tguest:        guest,\n\t\tsystemctl:    systemctl.New(guest),\n\t\tCommandChain: cli.New(Name),\n\t}\n}\n\nvar configDir = func() string { return config.CurrentProfile().ConfigDir() }\n\n// HostSocketFile returns the path to the containerd socket on host.\nfunc HostSocketFile() string { return filepath.Join(configDir(), \"incus.sock\") }\n\nconst (\n\tName = \"incus\"\n\n\tstorageDriver = \"zfs\"\n\n\tpoolName    = \"default\"\n\tpoolMetaDir = \"/var/lib/incus/storage-pools/\" + poolName\n\n\tpoolDisksDir = \"/var/lib/incus/disks\"\n\tpoolDiskFile = poolDisksDir + \"/\" + poolName + \".img\"\n)\n\nfunc init() {\n\tenvironment.RegisterContainer(Name, newRuntime, false)\n}\n\nvar _ environment.Container = (*incusRuntime)(nil)\n\ntype incusRuntime struct {\n\thost      environment.HostActions\n\tguest     environment.GuestActions\n\tsystemctl systemctl.Systemctl\n\tcli.CommandChain\n}\n\n// Dependencies implements environment.Container.\nfunc (c *incusRuntime) Dependencies() []string {\n\treturn []string{\"incus\"}\n}\n\n// Provision implements environment.Container.\nfunc (c *incusRuntime) Provision(ctx context.Context) error {\n\tconf := ctx.Value(config.CtxKey()).(config.Config)\n\tlog := c.Logger(ctx)\n\n\t// start incus to check if already fully provisioned.\n\t// after a full /var/lib/incus restore from external disk, incus\n\t// reads its previous database and restores networks/pools automatically.\n\t_ = c.systemctl.Start(\"incus.service\")\n\n\tif found, _, _ := c.findNetwork(incusBridgeInterface); found {\n\t\t// already provisioned (e.g. full restore from external disk)\n\t\treturn nil\n\t}\n\n\temptyDisk := true\n\trecoverStorage := false\n\tif limautil.DiskProvisioned(Name) {\n\t\temptyDisk = false\n\t\t// previous disk exists\n\t\t// ignore storage, recovery would be attempted later\n\t\trecoverStorage = cli.Prompt(\"existing Incus data found, would you like to recover the storage pool(s)\")\n\t}\n\n\tvar value struct {\n\t\tDisk          int\n\t\tInterface     string\n\t\tBridgeGateway string\n\t\tSetStorage    bool\n\t}\n\tvalue.Disk = conf.Disk\n\tvalue.Interface = incusBridgeInterface\n\tvalue.BridgeGateway = bridgeGateway\n\tvalue.SetStorage = emptyDisk // set only when the disk is empty\n\n\tbuf, err := util.ParseTemplate(configYaml, value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error parsing incus config template: %w\", err)\n\t}\n\n\tstdin := bytes.NewReader(buf)\n\tif err := c.guest.RunWith(stdin, nil, \"sudo\", \"incus\", \"admin\", \"init\", \"--preseed\"); err != nil {\n\t\treturn fmt.Errorf(\"error setting up incus: %w\", err)\n\t}\n\n\t// provision successful\n\tif emptyDisk {\n\t\treturn nil\n\t}\n\n\tif !recoverStorage {\n\t\treturn c.wipeDisk(conf.Disk)\n\t}\n\n\tif _, err := c.guest.Stat(poolDiskFile); err != nil {\n\t\tlog.Warnln(fmt.Errorf(\"cannot recover disk: %w, creating new storage pool\", err))\n\t\treturn c.wipeDisk(conf.Disk)\n\t}\n\n\tfor {\n\t\tif err := c.recoverDisk(ctx); err != nil {\n\t\t\tlog.Warnln(err)\n\n\t\t\tif cli.Prompt(\"recovery failed for default storage pool, try again\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlog.Warnln(\"discarding disk, creating new storage pool\")\n\t\t\treturn c.wipeDisk(conf.Disk)\n\t\t}\n\t\tbreak\n\t}\n\n\treturn nil\n}\n\n// Running implements environment.Container.\nfunc (c *incusRuntime) Running(ctx context.Context) bool {\n\treturn c.systemctl.Active(\"incus.service\")\n}\n\n// Start implements environment.Container.\nfunc (c *incusRuntime) Start(ctx context.Context) error {\n\tconf, _ := ctx.Value(config.CtxKey()).(config.Config)\n\n\ta := c.Init(ctx)\n\n\t// incus should already be started\n\t// this is mainly to ascertain it has started\n\n\tif c.poolImported() {\n\t\ta.Add(func() error {\n\t\t\treturn c.systemctl.Start(\"incus.service\")\n\t\t})\n\t} else {\n\t\t// pool not yet imported\n\t\t// restart incus to import pool\n\t\ta.Add(func() error {\n\t\t\treturn c.systemctl.Restart(\"incus.service\")\n\t\t})\n\t}\n\n\t// sync disk size for the default pool\n\tif conf.Disk > 0 {\n\t\ta.Add(func() error {\n\t\t\t// this can fail silently\n\t\t\t_ = c.guest.RunQuiet(\"sudo\", \"incus\", \"storage\", \"set\", \"default\", \"size=\"+config.Disk(conf.Disk).GiB())\n\t\t\treturn nil\n\t\t})\n\t}\n\n\ta.Add(func() error {\n\t\t// attempt to set remote\n\t\tif err := c.setRemote(conf.AutoActivate()); err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\t// workaround missing user in incus-admin by restarting\n\t\tctx := context.WithValue(ctx, cli.CtxKeyQuiet, true)\n\t\tif err := c.guest.Restart(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// attempt once again to set remote\n\t\treturn c.setRemote(conf.AutoActivate())\n\t})\n\n\ta.Add(func() error {\n\t\tif err := c.addDockerRemote(); err != nil {\n\t\t\treturn cli.ErrNonFatal(err)\n\t\t}\n\t\treturn nil\n\t})\n\n\ta.Add(func() error {\n\t\tif err := c.addContainerRoute(); err != nil {\n\t\t\treturn cli.ErrNonFatal(err)\n\t\t}\n\t\treturn nil\n\t})\n\n\treturn a.Exec()\n}\n\n// Stop implements environment.Container.\nfunc (c *incusRuntime) Stop(ctx context.Context, force bool) error {\n\ta := c.Init(ctx)\n\n\ta.Add(func() error {\n\t\t_ = c.removeContainerRoute()\n\t\treturn nil\n\t})\n\n\ta.Add(func() error {\n\t\treturn c.systemctl.Stop(\"incus.service\", force)\n\t})\n\n\ta.Add(c.unsetRemote)\n\n\treturn a.Exec()\n}\n\n// Teardown implements environment.Container.\nfunc (c *incusRuntime) Teardown(ctx context.Context) error {\n\ta := c.Init(ctx)\n\n\ta.Add(func() error {\n\t\t_ = c.removeContainerRoute()\n\t\treturn nil\n\t})\n\n\ta.Add(c.unsetRemote)\n\n\treturn a.Exec()\n}\n\n// Version implements environment.Container.\nfunc (c *incusRuntime) Version(ctx context.Context) string {\n\tversion, _ := c.host.RunOutput(\"incus\", \"version\", config.CurrentProfile().ID+\":\")\n\treturn version\n}\n\nfunc (c incusRuntime) Name() string {\n\treturn Name\n}\n\nfunc (c incusRuntime) setRemote(activate bool) error {\n\tname := config.CurrentProfile().ID\n\n\t// add remote\n\tif !c.hasRemote(name) {\n\t\tif err := c.host.RunQuiet(\"incus\", \"remote\", \"add\", name, \"unix://\"+HostSocketFile()); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// if activate, set default to new remote\n\tif activate {\n\t\treturn c.host.RunQuiet(\"incus\", \"remote\", \"switch\", name)\n\t}\n\n\treturn nil\n}\n\nfunc (c incusRuntime) unsetRemote() error {\n\t// if default remote, set default to local\n\tif c.isDefaultRemote() {\n\t\tif err := c.host.RunQuiet(\"incus\", \"remote\", \"switch\", \"local\"); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// if has remote, remove remote\n\tif c.hasRemote(config.CurrentProfile().ID) {\n\t\treturn c.host.RunQuiet(\"incus\", \"remote\", \"remove\", config.CurrentProfile().ID)\n\t}\n\n\treturn nil\n}\n\nfunc (c incusRuntime) hasRemote(name string) bool {\n\tremotes, err := c.fetchRemotes()\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t_, ok := remotes[name]\n\treturn ok\n}\n\nfunc (c incusRuntime) fetchRemotes() (remoteInfo, error) {\n\tb, err := c.host.RunOutput(\"incus\", \"remote\", \"list\", \"--format\", \"json\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error fetching remotes: %w\", err)\n\t}\n\n\tvar remotes remoteInfo\n\tif err := json.NewDecoder(strings.NewReader(b)).Decode(&remotes); err != nil {\n\t\treturn nil, fmt.Errorf(\"error decoding remotes response: %w\", err)\n\t}\n\n\treturn remotes, nil\n}\n\nfunc (c incusRuntime) isDefaultRemote() bool {\n\tremote, _ := c.host.RunOutput(\"incus\", \"remote\", \"get-default\")\n\treturn remote == config.CurrentProfile().ID\n}\n\nfunc (c incusRuntime) addDockerRemote() error {\n\tif c.hasRemote(\"docker\") {\n\t\t// already added\n\t\treturn nil\n\t}\n\n\treturn c.host.RunQuiet(\"incus\", \"remote\", \"add\", \"docker\", \"https://docker.io\", \"--protocol=oci\")\n}\n\nfunc (c incusRuntime) findNetwork(interfaceName string) (found bool, info networkInfo, err error) {\n\tb, err := c.guest.RunOutput(\"sudo\", \"incus\", \"network\", \"list\", \"--format\", \"json\")\n\tif err != nil {\n\t\treturn found, info, fmt.Errorf(\"error listing networks: %w\", err)\n\t}\n\tvar resp []networkInfo\n\tif err := json.NewDecoder(strings.NewReader(b)).Decode(&resp); err != nil {\n\t\treturn found, info, fmt.Errorf(\"error decoding networks into struct: %w\", err)\n\t}\n\tfor _, n := range resp {\n\t\tif n.Name == interfaceName {\n\t\t\treturn true, n, nil\n\t\t}\n\t}\n\n\treturn\n}\n\n//go:embed config.yaml\nvar configYaml string\n\ntype remoteInfo map[string]struct {\n\tAddr string `json:\"Addr\"`\n}\n\ntype networkInfo struct {\n\tName    string `json:\"name\"`\n\tManaged bool   `json:\"managed\"`\n\tType    string `json:\"type\"`\n}\n\nfunc (c *incusRuntime) Update(ctx context.Context) (bool, error) {\n\tpackages := []string{\n\t\t\"incus\",\n\t\t\"incus-base\",\n\t\t\"incus-client\",\n\t\t\"incus-extra\",\n\t\t\"incus-ui-canonical\",\n\t}\n\n\treturn debutil.UpdateRuntime(ctx, c.guest, c, packages...)\n}\n\nfunc (c *incusRuntime) poolImported() bool {\n\tscript := strings.NewReplacer(\n\t\t\"{pool_name}\", poolName,\n\t).Replace(\"sudo zpool list -H -o name | grep '^{pool_name}$'\")\n\treturn c.guest.RunQuiet(\"sh\", \"-c\", script) == nil\n}\n\nfunc (c *incusRuntime) recoverDisk(ctx context.Context) error {\n\tvar disks []string\n\tstr, err := c.guest.RunOutput(\"sh\", \"-c\", \"sudo ls \"+poolDisksDir+\" | grep '.img$'\")\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot list storage pool disks: %w\", err)\n\t}\n\n\tdisks = strings.Fields(str)\n\tif len(disks) == 0 {\n\t\treturn fmt.Errorf(\"no existing storage pool disks found\")\n\t}\n\n\tlog := c.Logger(ctx)\n\n\tlog.Println()\n\tlog.Println(\"Running 'incus admin recover' ...\")\n\tlog.Println()\n\tlog.Println(fmt.Sprintf(\"Found %d storage pool source(s):\", len(disks)))\n\tfor _, disk := range disks {\n\t\tlog.Println(\"  \" + poolDisksDir + \"/\" + disk)\n\t}\n\tlog.Println()\n\n\tif err := c.guest.RunInteractive(\"sudo\", \"incus\", \"admin\", \"recover\"); err != nil {\n\t\treturn fmt.Errorf(\"error recovering storage pool: %w\", err)\n\t}\n\n\tout, err := c.guest.RunOutput(\"sudo\", \"incus\", \"storage\", \"list\", \"name=\"+poolName, \"-c\", \"n\", \"--format\", \"compact,noheader\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif out != poolName {\n\t\treturn fmt.Errorf(\"default storage pool recovery failure\")\n\t}\n\n\treturn nil\n}\n\nfunc (c *incusRuntime) wipeDisk(size int) error {\n\t// prepare by deleting relevant files/directories\n\tdeleteScript := strings.NewReplacer(\n\t\t\"{disk_file}\", poolDiskFile,\n\t\t\"{meta_dir}\", poolMetaDir,\n\t).Replace(\"sudo rm -rf {disk_file} {meta_dir}\")\n\n\tif err := c.guest.RunQuiet(\"sh\", \"-c\", deleteScript); err != nil {\n\t\treturn fmt.Errorf(\"error preparing storage pools directory: %w\", err)\n\t}\n\n\t// create new storage pool\n\tvar diskSize = fmt.Sprintf(\"%dGiB\", size)\n\treturn c.guest.RunQuiet(\"sudo\", \"incus\", \"storage\", \"create\", poolName, storageDriver, \"size=\"+diskSize)\n}\n\n// migrationScript returns a script that migrates from the old disk layout\n// (separate incus-disks and incus-backups subdirectories) to the new layout\n// (full /var/lib/incus directory).\nfunc migrationScript() string {\n\tmountPoint := limautil.MountPoint()\n\treturn `MOUNT_POINT=\"` + mountPoint + `\"\nif [ -d \"$MOUNT_POINT/incus-disks\" ] && [ ! -d \"$MOUNT_POINT/incus\" ]; then\n  mkdir -p \"$MOUNT_POINT/incus\"\n  if [ -d /var/lib/incus ]; then\n    cp -a /var/lib/incus/. \"$MOUNT_POINT/incus/\"\n  fi\n  rm -rf \"$MOUNT_POINT/incus/disks\"\n  mv \"$MOUNT_POINT/incus-disks\" \"$MOUNT_POINT/incus/disks\"\n  if [ -d \"$MOUNT_POINT/incus-backups\" ]; then\n    rm -rf \"$MOUNT_POINT/incus/backups\"\n    mv \"$MOUNT_POINT/incus-backups\" \"$MOUNT_POINT/incus/backups\"\n  fi\nfi`\n}\n\n// DataDisk represents the data disk for the container runtime.\nfunc DataDisk() environment.DataDisk {\n\treturn environment.DataDisk{\n\t\tFSType: \"ext4\",\n\t\tDirs: []environment.DiskDir{\n\t\t\t{Name: \"incus\", Path: \"/var/lib/incus\"},\n\t\t},\n\t\tPreMount: []string{\n\t\t\t\"systemctl stop incus.service || true\",\n\t\t\t\"systemctl stop incus.socket || true\",\n\t\t\tmigrationScript(),\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "environment/container/incus/route.go",
    "content": "package incus\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/embedded\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n\t\"github.com/abiosoft/colima/util\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst BridgeSubnet = \"192.168.100.0/24\"\nconst bridgeGateway = \"192.168.100.1/24\"\n\n// addContainerRoute adds a macOS route for the Incus container subnet\n// via the VM's col0 IP address, making containers directly reachable from the host.\nfunc (c *incusRuntime) addContainerRoute() error {\n\tif !util.MacOS() {\n\t\treturn nil\n\t}\n\n\tvmIP := limautil.IPAddress(config.CurrentProfile().ID)\n\tif vmIP == \"127.0.0.1\" || vmIP == \"\" {\n\t\treturn nil\n\t}\n\n\tif !util.SubnetAvailable(BridgeSubnet) {\n\t\tlog.Warnf(\"subnet %s conflicts with host network, skipping route setup\", BridgeSubnet)\n\t\treturn nil\n\t}\n\n\tif err := embedded.InstallSudoers(c.host); err != nil {\n\t\treturn fmt.Errorf(\"error setting up sudoers for route: %w\", err)\n\t}\n\n\t// delete any stale route first (ignore errors)\n\t_ = c.removeContainerRoute()\n\n\tif err := c.host.RunQuiet(\"sudo\", \"/sbin/route\", \"add\", \"-net\", BridgeSubnet, vmIP); err != nil {\n\t\treturn fmt.Errorf(\"error adding route for %s via %s: %w\", BridgeSubnet, vmIP, err)\n\t}\n\n\treturn nil\n}\n\n// removeContainerRoute removes the macOS route for the Incus container subnet.\nfunc (c *incusRuntime) removeContainerRoute() error {\n\tif !util.MacOS() {\n\t\treturn nil\n\t}\n\n\tif !util.RouteExists(BridgeSubnet) {\n\t\treturn nil\n\t}\n\n\treturn c.host.RunQuiet(\"sudo\", \"/sbin/route\", \"delete\", \"-net\", BridgeSubnet)\n}\n"
  },
  {
    "path": "environment/container/kubernetes/cni.go",
    "content": "package kubernetes\n\nimport (\n\t_ \"embed\"\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/embedded\"\n\t\"github.com/abiosoft/colima/environment\"\n)\n\nfunc installCniConfig(guest environment.GuestActions, a *cli.ActiveCommandChain) {\n\t// fix cni config\n\ta.Add(func() error {\n\t\tflannelFile := \"/etc/cni/net.d/10-flannel.conflist\"\n\t\tcniConfDir := filepath.Dir(flannelFile)\n\t\tif err := guest.Run(\"sudo\", \"mkdir\", \"-p\", cniConfDir); err != nil {\n\t\t\treturn fmt.Errorf(\"error creating cni config dir: %w\", err)\n\t\t}\n\n\t\tflannel, err := embedded.Read(\"k3s/flannel.json\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error reading embedded flannel config: %w\", err)\n\t\t}\n\t\treturn guest.Write(flannelFile, flannel)\n\t})\n}\n"
  },
  {
    "path": "environment/container/kubernetes/k3s.go",
    "content": "package kubernetes\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/environment/container/containerd\"\n\t\"github.com/abiosoft/colima/environment/container/docker\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/downloader\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nconst listenPortKey = \"k3s_listen_port\"\n\nfunc hasK3sArg(k3sArgs []string, argName string) bool {\n\tfor _, arg := range k3sArgs {\n\t\tif strings.HasPrefix(arg, argName+\"=\") {\n\t\t\treturn true\n\t\t}\n\t\tif strings.HasPrefix(arg, argName+\" \") {\n\t\t\treturn true\n\t\t}\n\t\tif arg == argName {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc installK3s(host environment.HostActions,\n\tguest environment.GuestActions,\n\ta *cli.ActiveCommandChain,\n\tlog *logrus.Entry,\n\tcontainerRuntime string,\n\tk3sVersion string,\n\tk3sArgs []string,\n\tk3sListenPort int,\n) {\n\tinstallK3sBinary(host, guest, a, k3sVersion)\n\tinstallK3sCache(host, guest, a, log, containerRuntime, k3sVersion)\n\tinstallK3sCluster(host, guest, a, containerRuntime, k3sVersion, k3sArgs, k3sListenPort)\n}\n\nfunc installK3sBinary(\n\thost environment.HostActions,\n\tguest environment.GuestActions,\n\ta *cli.ActiveCommandChain,\n\tk3sVersion string,\n) {\n\tdownloadPath := \"/tmp/k3s\"\n\n\tbaseURL := \"https://github.com/k3s-io/k3s/releases/download/\" + k3sVersion + \"/\"\n\tshaSumTxt := \"sha256sum-\" + guest.Arch().GoArch() + \".txt\"\n\n\turl := baseURL + \"k3s\"\n\tshaURL := baseURL + shaSumTxt\n\tif guest.Arch().GoArch() == \"arm64\" {\n\t\turl += \"-arm64\"\n\t}\n\ta.Add(func() error {\n\t\tr := downloader.Request{\n\t\t\tURL: url,\n\t\t\tSHA: &downloader.SHA{Size: 256, URL: shaURL},\n\t\t}\n\t\treturn downloader.DownloadToGuest(host, guest, r, downloadPath)\n\t})\n\ta.Add(func() error {\n\t\treturn guest.Run(\"sudo\", \"install\", downloadPath, \"/usr/local/bin/k3s\")\n\t})\n}\n\nfunc installK3sCache(\n\thost environment.HostActions,\n\tguest environment.GuestActions,\n\ta *cli.ActiveCommandChain,\n\tlog *logrus.Entry,\n\tcontainerRuntime string,\n\tk3sVersion string,\n) {\n\tbaseURL := \"https://github.com/k3s-io/k3s/releases/download/\" + k3sVersion + \"/\"\n\timageTar := \"k3s-airgap-images-\" + guest.Arch().GoArch() + \".tar\"\n\tshaSumTxt := \"sha256sum-\" + guest.Arch().GoArch() + \".txt\"\n\timageTarGz := imageTar + \".gz\"\n\tdownloadPathTar := \"/tmp/\" + imageTar\n\tdownloadPathTarGz := \"/tmp/\" + imageTarGz\n\turl := baseURL + imageTarGz\n\tshaURL := baseURL + shaSumTxt\n\ta.Add(func() error {\n\t\tr := downloader.Request{\n\t\t\tURL: url,\n\t\t\tSHA: &downloader.SHA{Size: 256, URL: shaURL},\n\t\t}\n\t\treturn downloader.DownloadToGuest(host, guest, r, downloadPathTarGz)\n\t})\n\ta.Add(func() error {\n\t\treturn guest.Run(\"gzip\", \"-f\", \"-d\", downloadPathTarGz)\n\t})\n\n\tairGapDir := \"/var/lib/rancher/k3s/agent/images/\"\n\ta.Add(func() error {\n\t\treturn guest.Run(\"sudo\", \"mkdir\", \"-p\", airGapDir)\n\t})\n\ta.Add(func() error {\n\t\treturn guest.Run(\"sudo\", \"cp\", downloadPathTar, airGapDir)\n\t})\n\n\t// load OCI images for K3s\n\t// this can be safely ignored if failed as the images would be pulled afterwards.\n\tswitch containerRuntime {\n\tcase containerd.Name:\n\t\ta.Stage(\"loading oci images\")\n\t\ta.Add(func() error {\n\t\t\tif err := guest.Run(\"sudo\", \"nerdctl\", \"-n\", \"k8s.io\", \"load\", \"-i\", downloadPathTar, \"--all-platforms\"); err != nil {\n\t\t\t\tlog.Warnln(fmt.Errorf(\"error loading oci images: %w\", err))\n\t\t\t\tlog.Warnln(\"startup may delay a bit as images will be pulled from oci registry\")\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\tcase docker.Name:\n\t\ta.Stage(\"loading oci images\")\n\t\ta.Add(func() error {\n\t\t\tif err := guest.Run(\"sudo\", \"docker\", \"load\", \"-i\", downloadPathTar); err != nil {\n\t\t\t\tlog.Warnln(fmt.Errorf(\"error loading oci images: %w\", err))\n\t\t\t\tlog.Warnln(\"startup may delay a bit as images will be pulled from oci registry\")\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n}\n\nfunc installK3sCluster(\n\thost environment.HostActions,\n\tguest environment.GuestActions,\n\ta *cli.ActiveCommandChain,\n\tcontainerRuntime string,\n\tk3sVersion string,\n\tk3sArgs []string,\n\tk3sListenPort int,\n) {\n\t// install k3s last to ensure it is the last step\n\tdownloadPath := \"/tmp/k3s-install.sh\"\n\turl := \"https://raw.githubusercontent.com/k3s-io/k3s/\" + k3sVersion + \"/install.sh\"\n\ta.Add(func() error {\n\t\tr := downloader.Request{URL: url}\n\t\treturn downloader.DownloadToGuest(host, guest, r, downloadPath)\n\t})\n\ta.Add(func() error {\n\t\treturn guest.Run(\"sudo\", \"install\", downloadPath, \"/usr/local/bin/k3s-install.sh\")\n\t})\n\n\targs := append([]string{\n\t\t\"--write-kubeconfig-mode\", \"644\",\n\t}, k3sArgs...)\n\n\ta.Retry(\"waiting for VM IP address\", time.Second*5, 4, func(retryCount int) error {\n\t\tipAddress := limautil.IPAddress(config.CurrentProfile().ID)\n\t\tif ipAddress == \"\" {\n\t\t\treturn fmt.Errorf(\"no IP address assigned to network interface\")\n\t\t}\n\n\t\tif ipAddress == \"127.0.0.1\" {\n\t\t\targs = append(args, \"--flannel-iface\", \"eth0\")\n\t\t} else {\n\t\t\tif !hasK3sArg(k3sArgs, \"--advertise-address\") {\n\t\t\t\targs = append(args, \"--advertise-address\", ipAddress)\n\t\t\t}\n\t\t\tif !hasK3sArg(k3sArgs, \"--flannel-iface\") {\n\t\t\t\targs = append(args, \"--flannel-iface\", limautil.NetInterface)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n\n\tswitch containerRuntime {\n\tcase docker.Name:\n\t\targs = append(args, \"--docker\")\n\tcase containerd.Name:\n\t\targs = append(args, \"--container-runtime-endpoint\", \"unix:///run/containerd/containerd.sock\")\n\t}\n\n\ta.Add(func() error {\n\t\tport, err := getPortNumber(guest, k3sListenPort)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\targs = append(args, \"--https-listen-port\", strconv.Itoa(port))\n\t\treturn nil\n\t})\n\n\ta.Add(func() error {\n\t\treturn guest.Run(\"sh\", \"-c\", \"INSTALL_K3S_SKIP_DOWNLOAD=true INSTALL_K3S_SKIP_ENABLE=true k3s-install.sh \"+strings.Join(args, \" \"))\n\t})\n}\n\n// getPortNumber retrieves the previously set port number.\n// If missing, an available random port is set and return.\nfunc getPortNumber(guest environment.GuestActions, k3sListenPort int) (int, error) {\n\t// port previously set, reuse it\n\tif port, err := strconv.Atoi(guest.Get(listenPortKey)); err == nil && port > 0 {\n\t\treturn port, nil\n\t}\n\n\t// for backward compatibility\n\t// if the instance already exists, assume default port 6443\n\tif m := guest.Get(masterAddressKey); m != \"\" {\n\t\treturn 6443, nil\n\t}\n\n\tvar port int\n\tif k3sListenPort > 0 {\n\t\t// template configured port\n\t\tport = k3sListenPort\n\t} else {\n\t\t// new instance, assign random port\n\t\tport = util.RandomAvailablePort()\n\t}\n\n\tif err := guest.Set(listenPortKey, strconv.Itoa(port)); err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn port, nil\n}\n"
  },
  {
    "path": "environment/container/kubernetes/kubeconfig.go",
    "content": "package kubernetes\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n)\n\nconst masterAddressKey = \"master_address\"\n\nfunc (c kubernetesRuntime) provisionKubeconfig(ctx context.Context) error {\n\tip := limautil.IPAddress(config.CurrentProfile().ID)\n\tif ip == c.guest.Get(masterAddressKey) {\n\t\treturn nil\n\t}\n\n\tlog := c.Logger(ctx)\n\ta := c.Init(ctx)\n\n\ta.Stage(\"updating config\")\n\n\t// remove existing configs (if any)\n\t// this is safe as the profile name is unique to colima\n\tc.unsetKubeconfig(a)\n\n\t// ensure host kube directory exists\n\thostHome := c.host.Env(\"HOME\")\n\tif hostHome == \"\" {\n\t\treturn fmt.Errorf(\"error retrieving home directory on host\")\n\t}\n\n\tprofile := config.CurrentProfile().ID\n\thostKubeDir := filepath.Join(hostHome, \".kube\")\n\ta.Add(func() error {\n\t\treturn c.host.Run(\"mkdir\", \"-p\", filepath.Join(hostKubeDir, \".\"+profile))\n\t})\n\n\tkubeconfFile := filepath.Join(hostKubeDir, \"config\")\n\tenvKubeConfFile := c.host.Env(\"KUBECONFIG\")\n\tif envKubeConfFile != \"\" {\n\t\tkubeconfFile = filepath.SplitList(envKubeConfFile)[0]\n\t}\n\ttmpkubeconfFile := filepath.Join(hostKubeDir, \".\"+profile, \"colima-temp\")\n\n\t// manipulate in VM and save to host\n\ta.Add(func() error {\n\t\tkubeconfig, err := c.guest.Read(\"/etc/rancher/k3s/k3s.yaml\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error fetching kubeconfig on guest: %w\", err)\n\t\t}\n\t\t// replace name\n\t\tkubeconfig = strings.ReplaceAll(kubeconfig, \": default\", \": \"+profile)\n\n\t\t// replace IP\n\t\tif ip != \"\" && ip != \"127.0.0.1\" {\n\t\t\tkubeconfig = strings.ReplaceAll(kubeconfig, \"https://127.0.0.1:\", \"https://\"+ip+\":\")\n\t\t}\n\n\t\t// save on the host\n\t\treturn c.host.Write(tmpkubeconfFile, []byte(kubeconfig))\n\t})\n\n\t// merge on host\n\ta.Add(func() (err error) {\n\t\t// prepare new host with right env var.\n\t\tenvVar := fmt.Sprintf(\"KUBECONFIG=%s:%s\", kubeconfFile, tmpkubeconfFile)\n\t\thost := c.host.WithEnv(envVar)\n\n\t\t// get merged config\n\t\tkubeconfig, err := host.RunOutput(\"kubectl\", \"config\", \"view\", \"--raw\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// save\n\t\treturn host.Write(tmpkubeconfFile, []byte(kubeconfig))\n\t})\n\n\t// backup current settings and save new config\n\ta.Add(func() error {\n\t\t// backup existing file if exists\n\t\tif stat, err := c.host.Stat(kubeconfFile); err == nil && !stat.IsDir() {\n\t\t\tbackup := filepath.Join(filepath.Dir(tmpkubeconfFile), fmt.Sprintf(\"config-bak-%d\", time.Now().Unix()))\n\t\t\tif err := c.host.Run(\"cp\", kubeconfFile, backup); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error backing up kubeconfig: %w\", err)\n\t\t\t}\n\t\t}\n\t\t// save new config\n\t\tif err := c.host.Run(\"cp\", tmpkubeconfFile, kubeconfFile); err != nil {\n\t\t\treturn fmt.Errorf(\"error updating kubeconfig: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\t// set new context\n\tconf, _ := ctx.Value(config.CtxKey()).(config.Config)\n\tif conf.AutoActivate() {\n\t\ta.Add(func() error {\n\t\t\tout, err := c.host.RunOutput(\"kubectl\", \"config\", \"use-context\", profile)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tlog.Println(out)\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// save settings\n\ta.Add(func() error {\n\t\treturn c.guest.Set(masterAddressKey, ip)\n\t})\n\n\treturn a.Exec()\n}\n\nfunc (c kubernetesRuntime) unsetKubeconfig(a *cli.ActiveCommandChain) {\n\tprofile := config.CurrentProfile().ID\n\ta.Add(func() error {\n\t\treturn c.host.Run(\"kubectl\", \"config\", \"unset\", \"users.\"+profile)\n\t})\n\ta.Add(func() error {\n\t\treturn c.host.Run(\"kubectl\", \"config\", \"unset\", \"contexts.\"+profile)\n\t})\n\ta.Add(func() error {\n\t\treturn c.host.Run(\"kubectl\", \"config\", \"unset\", \"clusters.\"+profile)\n\t})\n\t// kubectl config unset current-context\n\ta.Add(func() error {\n\t\tif c, _ := c.host.RunOutput(\"kubectl\", \"config\", \"current-context\"); c != config.CurrentProfile().ID {\n\t\t\treturn nil\n\t\t}\n\t\treturn c.host.Run(\"kubectl\", \"config\", \"unset\", \"current-context\")\n\t})\n}\n\nfunc (c kubernetesRuntime) teardownKubeconfig(a *cli.ActiveCommandChain) {\n\ta.Stage(\"reverting config\")\n\tc.unsetKubeconfig(a)\n\ta.Add(func() error {\n\t\treturn c.guest.Set(masterAddressKey, \"\")\n\t})\n}\n"
  },
  {
    "path": "environment/container/kubernetes/kubernetes.go",
    "content": "package kubernetes\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/environment/container/containerd\"\n\t\"github.com/abiosoft/colima/environment/container/docker\"\n\t\"github.com/abiosoft/colima/environment/guest/systemctl\"\n)\n\n// Name is container runtime name\n\nconst (\n\tName           = \"kubernetes\"\n\tDefaultVersion = \"v1.35.0+k3s1\"\n\n\tConfigKey = \"kubernetes_config\"\n)\n\nfunc newRuntime(host environment.HostActions, guest environment.GuestActions) environment.Container {\n\treturn &kubernetesRuntime{\n\t\thost:         host,\n\t\tguest:        guest,\n\t\tsystemctl:    systemctl.New(guest),\n\t\tCommandChain: cli.New(Name),\n\t}\n}\n\nfunc init() {\n\tenvironment.RegisterContainer(Name, newRuntime, true)\n}\n\nvar _ environment.Container = (*kubernetesRuntime)(nil)\n\ntype kubernetesRuntime struct {\n\thost      environment.HostActions\n\tguest     environment.GuestActions\n\tsystemctl systemctl.Systemctl\n\tcli.CommandChain\n}\n\nfunc (c kubernetesRuntime) Name() string {\n\treturn Name\n}\n\nfunc (c kubernetesRuntime) isInstalled() bool {\n\t// it is installed if uninstall script is present.\n\treturn c.guest.RunQuiet(\"command\", \"-v\", \"k3s-uninstall.sh\") == nil\n}\n\nfunc (c kubernetesRuntime) isVersionInstalled(version string) bool {\n\t// validate version change via cli flag/config.\n\tout, err := c.guest.RunOutput(\"k3s\", \"--version\")\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn strings.Contains(out, version)\n}\n\nfunc (c kubernetesRuntime) Running(context.Context) bool {\n\treturn c.guest.RunQuiet(\"sudo\", \"service\", \"k3s\", \"status\") == nil\n}\n\nfunc (c kubernetesRuntime) runtime() string {\n\treturn c.guest.Get(environment.ContainerRuntimeKey)\n}\n\nfunc (c kubernetesRuntime) config() config.Kubernetes {\n\tconf := config.Kubernetes{Version: DefaultVersion}\n\tif b := c.guest.Get(ConfigKey); b != \"\" {\n\t\t_ = json.Unmarshal([]byte(b), &conf)\n\t}\n\treturn conf\n}\n\nfunc (c kubernetesRuntime) setConfig(conf config.Kubernetes) error {\n\tb, err := json.Marshal(conf)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error encoding kubernetes config to json: %w\", err)\n\t}\n\n\treturn c.guest.Set(ConfigKey, string(b))\n}\n\nfunc (c *kubernetesRuntime) Provision(ctx context.Context) error {\n\tlog := c.Logger(ctx)\n\ta := c.Init(ctx)\n\tif c.Running(ctx) {\n\t\treturn nil\n\t}\n\n\tappConf, ok := ctx.Value(config.CtxKey()).(config.Config)\n\truntime := appConf.Runtime\n\tconf := appConf.Kubernetes\n\n\tif !ok {\n\t\t// this should be a restart/start while vm is active\n\t\t// retrieve value in the vm\n\t\truntime = c.runtime()\n\t\tconf = c.config()\n\t}\n\n\tif conf.Version == \"\" {\n\t\t// this ensure if `version` tag in `kubernetes` section in yaml is empty,\n\t\t// it should assign with the `DefaultVersion` for the baseURL\n\t\tconf.Version = DefaultVersion\n\t}\n\n\tif c.isVersionInstalled(conf.Version) {\n\t\t// runtime has changed, ensure the required images are in the registry\n\t\tif currentRuntime := c.runtime(); currentRuntime != \"\" && currentRuntime != runtime {\n\t\t\ta.Stagef(\"changing runtime to %s\", runtime)\n\t\t\tinstallK3sCache(c.host, c.guest, a, log, runtime, conf.Version)\n\t\t}\n\t\t// other settings may have changed e.g. ingress\n\t\tinstallK3sCluster(c.host, c.guest, a, runtime, conf.Version, conf.K3sArgs, conf.Port)\n\t} else {\n\t\tif c.isInstalled() {\n\t\t\ta.Stagef(\"version changed to %s, downloading and installing\", conf.Version)\n\t\t} else {\n\t\t\tif ok {\n\t\t\t\ta.Stage(\"downloading and installing\")\n\t\t\t} else {\n\t\t\t\ta.Stage(\"installing\")\n\t\t\t}\n\t\t}\n\t\tinstallK3s(c.host, c.guest, a, log, runtime, conf.Version, conf.K3sArgs, conf.Port)\n\t}\n\n\t// this needs to happen on each startup\n\t{\n\t\t// cni is used by both cri-dockerd and containerd\n\t\tinstallCniConfig(c.guest, a)\n\t}\n\n\t// provision successful, now we can persist the version\n\ta.Add(func() error { return c.setConfig(conf) })\n\n\treturn a.Exec()\n}\n\nfunc (c kubernetesRuntime) Start(ctx context.Context) error {\n\tlog := c.Logger(ctx)\n\ta := c.Init(ctx)\n\tif c.Running(ctx) {\n\t\tlog.Println(\"already running\")\n\t\treturn nil\n\t}\n\n\ta.Add(func() error {\n\t\treturn c.systemctl.Start(\"k3s.service\")\n\t})\n\ta.Retry(\"\", time.Second*2, 10, func(int) error {\n\t\treturn c.guest.RunQuiet(\"kubectl\", \"cluster-info\")\n\t})\n\n\tif err := a.Exec(); err != nil {\n\t\treturn err\n\t}\n\n\treturn c.provisionKubeconfig(ctx)\n}\n\nfunc (c kubernetesRuntime) Stop(ctx context.Context, force bool) error {\n\ta := c.Init(ctx)\n\ta.Add(func() error {\n\t\treturn c.guest.Run(\"k3s-killall.sh\")\n\t})\n\n\t// k3s is buggy with external containerd for now\n\t// cleanup is manual\n\tif !force {\n\t\ta.Add(c.stopAllContainers)\n\t}\n\n\treturn a.Exec()\n}\n\nfunc (c kubernetesRuntime) deleteAllContainers() error {\n\tids := c.runningContainerIDs()\n\tif ids == \"\" {\n\t\treturn nil\n\t}\n\n\tvar args []string\n\n\tswitch c.runtime() {\n\tcase containerd.Name:\n\t\targs = []string{\"nerdctl\", \"-n\", \"k8s.io\", \"rm\", \"-f\"}\n\tcase docker.Name:\n\t\targs = []string{\"docker\", \"rm\", \"-f\"}\n\tdefault:\n\t\treturn nil\n\t}\n\n\targs = append(args, strings.Fields(ids)...)\n\n\treturn c.guest.Run(\"sudo\", \"sh\", \"-c\", strings.Join(args, \" \"))\n}\n\nfunc (c kubernetesRuntime) stopAllContainers() error {\n\tids := c.runningContainerIDs()\n\tif ids == \"\" {\n\t\treturn nil\n\t}\n\n\tvar args []string\n\n\tswitch c.runtime() {\n\tcase containerd.Name:\n\t\targs = []string{\"nerdctl\", \"-n\", \"k8s.io\", \"kill\"}\n\tcase docker.Name:\n\t\targs = []string{\"docker\", \"kill\"}\n\tdefault:\n\t\treturn nil\n\t}\n\n\targs = append(args, strings.Fields(ids)...)\n\n\treturn c.guest.Run(\"sudo\", \"sh\", \"-c\", strings.Join(args, \" \"))\n}\n\nfunc (c kubernetesRuntime) runningContainerIDs() string {\n\tvar args []string\n\n\tswitch c.runtime() {\n\tcase containerd.Name:\n\t\targs = []string{\"sudo\", \"nerdctl\", \"-n\", \"k8s.io\", \"ps\", \"-q\"}\n\tcase docker.Name:\n\t\targs = []string{\"sudo\", \"sh\", \"-c\", `docker ps --format '{{.Names}}'| grep \"k8s_\"`}\n\tdefault:\n\t\treturn \"\"\n\t}\n\n\tids, _ := c.guest.RunOutput(args...)\n\tif ids == \"\" {\n\t\treturn \"\"\n\t}\n\treturn strings.ReplaceAll(ids, \"\\n\", \" \")\n}\n\nfunc (c kubernetesRuntime) Teardown(ctx context.Context) error {\n\ta := c.Init(ctx)\n\n\tif c.isInstalled() {\n\t\ta.Add(func() error {\n\t\t\treturn c.guest.Run(\"k3s-uninstall.sh\")\n\t\t})\n\t}\n\n\t// k3s is buggy with external containerd for now\n\t// cleanup is manual\n\ta.Add(c.deleteAllContainers)\n\n\tc.teardownKubeconfig(a)\n\n\treturn a.Exec()\n}\n\nfunc (c kubernetesRuntime) Dependencies() []string {\n\treturn []string{\"kubectl\"}\n}\n\nfunc (c kubernetesRuntime) Version(context.Context) string {\n\tversion, _ := c.host.RunOutput(\"kubectl\", \"--context\", config.CurrentProfile().ID, \"version\", \"--short\")\n\treturn version\n}\n\nfunc (c *kubernetesRuntime) Update(ctx context.Context) (bool, error) {\n\treturn false, fmt.Errorf(\"update not supported for the %s runtime\", Name)\n}\n"
  },
  {
    "path": "environment/container.go",
    "content": "package environment\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n)\n\n// IsNoneRuntime returns if runtime is none.\nfunc IsNoneRuntime(runtime string) bool { return runtime == \"none\" }\n\n// Container is container environment.\ntype Container interface {\n\t// Name is the name of the container runtime. e.g. docker, containerd\n\tName() string\n\t// Provision provisions/installs the container runtime.\n\t// Should be idempotent.\n\tProvision(ctx context.Context) error\n\t// Start starts the container runtime.\n\tStart(ctx context.Context) error\n\t// Stop stops the container runtime.\n\t// If force is true, the runtime is killed immediately without graceful shutdown.\n\tStop(ctx context.Context, force bool) error\n\t// Teardown tears down/uninstall the container runtime.\n\tTeardown(ctx context.Context) error\n\t// Update the container runtime.\n\tUpdate(ctx context.Context) (bool, error)\n\t// Version returns the container runtime version.\n\tVersion(ctx context.Context) string\n\t// Running returns if the container runtime is currently running.\n\tRunning(ctx context.Context) bool\n\n\tDependencies\n}\n\n// NewContainer creates a new container environment.\nfunc NewContainer(runtime string, host HostActions, guest GuestActions) (Container, error) {\n\tif _, ok := containerRuntimes[runtime]; !ok {\n\t\treturn nil, fmt.Errorf(\"unsupported container runtime '%s'\", runtime)\n\t}\n\n\treturn containerRuntimes[runtime].Func(host, guest), nil\n}\n\n// NewContainerFunc is implemented by container runtime implementations to create a new instance.\ntype NewContainerFunc func(host HostActions, guest GuestActions) Container\n\nvar containerRuntimes = map[string]containerRuntimeFunc{}\n\ntype containerRuntimeFunc struct {\n\tFunc   NewContainerFunc\n\tHidden bool\n}\n\n// RegisterContainer registers a new container runtime.\n// If hidden is true, the container is not displayed as an available runtime.\nfunc RegisterContainer(name string, f NewContainerFunc, hidden bool) {\n\tif _, ok := containerRuntimes[name]; ok {\n\t\tlog.Fatalf(\"container runtime '%s' already registered\", name)\n\t}\n\tcontainerRuntimes[name] = containerRuntimeFunc{Func: f, Hidden: hidden}\n}\n\n// ContainerRuntimes return the names of available container runtimes.\nfunc ContainerRuntimes() (names []string) {\n\tfor name, cont := range containerRuntimes {\n\t\tif cont.Hidden {\n\t\t\tcontinue\n\t\t}\n\t\tnames = append(names, name)\n\t}\n\treturn\n}\n\n// DataDisk holds the configuration for mounting an external runtime disk.\ntype DataDisk struct {\n\tDirs     []DiskDir // the directories to be mounted\n\tPreMount []string  // the scripts to run before mounting the directories\n\tFSType   string    // the filesystem type for the disk e.g. ext4\n}\n\n// DiskDir is a directory mounted in a data disk.\ntype DiskDir struct {\n\tName string\n\tPath string\n}\n"
  },
  {
    "path": "environment/environment.go",
    "content": "package environment\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\n\t\"github.com/abiosoft/colima/config\"\n)\n\ntype runActions interface {\n\t// Run runs command\n\tRun(args ...string) error\n\t// RunQuiet runs command whilst suppressing the output.\n\t// Useful for commands that only the exit code matters.\n\tRunQuiet(args ...string) error\n\t// RunOutput runs command and returns its output.\n\tRunOutput(args ...string) (string, error)\n\t// RunInteractive runs command interactively.\n\tRunInteractive(args ...string) error\n\t// RunWith runs with stdin and stdout.\n\tRunWith(stdin io.Reader, stdout io.Writer, args ...string) error\n}\n\ntype fileActions interface {\n\tRead(fileName string) (string, error)\n\tWrite(fileName string, body []byte) error\n\tStat(fileName string) (os.FileInfo, error)\n}\n\n// HostActions are actions performed on the host.\ntype HostActions interface {\n\trunActions\n\tfileActions\n\t// WithEnv creates a new instance based on the current instance\n\t// with the specified environment variables.\n\tWithEnv(env ...string) HostActions\n\t// WithDir creates a new instance based on the current instance\n\t// with the working directory set to dir.\n\tWithDir(dir string) HostActions\n\t// Env retrieves environment variable on the host.\n\tEnv(string) string\n}\n\n// GuestActions are actions performed on the guest i.e. VM.\ntype GuestActions interface {\n\trunActions\n\tfileActions\n\t// Start starts up the VM\n\tStart(ctx context.Context, conf config.Config) error\n\t// Stop shuts down the VM\n\tStop(ctx context.Context, force bool) error\n\t// Restart restarts the VM\n\tRestart(ctx context.Context) error\n\t// SSH performs an ssh connection to the VM\n\tSSH(workingDir string, args ...string) error\n\t// Created returns if the VM has been previously created.\n\tCreated() bool\n\t// Running returns if the VM is currently running.\n\tRunning(ctx context.Context) bool\n\t// Env retrieves environment variable in the VM.\n\tEnv(string) (string, error)\n\t// Get retrieves a configuration in the VM.\n\tGet(key string) string\n\t// Set sets configuration in the VM.\n\tSet(key, value string) error\n\t// User returns the username of the user in the VM.\n\tUser() (string, error)\n\t// Arch returns the architecture of the VM.\n\tArch() Arch\n}\n\n// Dependencies are dependencies that must exist on the host.\ntype Dependencies interface {\n\t// Dependencies are dependencies that must exist on the host.\n\t// TODO this may need to accommodate non-brew installable dependencies\n\tDependencies() []string\n}\n"
  },
  {
    "path": "environment/guest/systemctl/systemctl.go",
    "content": "package systemctl\n\nimport \"github.com/abiosoft/colima/environment\"\n\n// Runner is the subset of environment.GuestActions that Systemctl requires.\n// Using a narrow interface makes Systemctl easier to test and more loosely coupled.\ntype Runner interface {\n\tRun(args ...string) error\n\tRunQuiet(args ...string) error\n}\n\n// compile-time check: environment.GuestActions satisfies runner.\nvar _ Runner = (environment.GuestActions)(nil)\n\n// Systemctl provides a typed wrapper for running systemctl commands in the guest VM.\ntype Systemctl struct {\n\trunner Runner\n}\n\n// New creates a new Systemctl instance backed by the given guest.\nfunc New(guest Runner) Systemctl {\n\treturn Systemctl{runner: guest}\n}\n\n// Start starts a systemd service.\nfunc (s Systemctl) Start(service string) error {\n\treturn s.runner.Run(\"sudo\", \"systemctl\", \"start\", service)\n}\n\n// Restart restarts a systemd service.\nfunc (s Systemctl) Restart(service string) error {\n\treturn s.runner.Run(\"sudo\", \"systemctl\", \"restart\", service)\n}\n\n// Stop stops a systemd service. If force is true, it is killed immediately without graceful shutdown.\nfunc (s Systemctl) Stop(service string, force bool) error {\n\tverb := \"stop\"\n\tif force {\n\t\tverb = \"kill\"\n\t}\n\treturn s.runner.Run(\"sudo\", \"systemctl\", verb, service)\n}\n\n// Active returns whether a systemd service is currently active.\nfunc (s Systemctl) Active(service string) bool {\n\treturn s.runner.RunQuiet(\"systemctl\", \"is-active\", service) == nil\n}\n\n// DaemonReload reloads the systemd manager configuration.\nfunc (s Systemctl) DaemonReload() error {\n\treturn s.runner.Run(\"sudo\", \"systemctl\", \"daemon-reload\")\n}\n"
  },
  {
    "path": "environment/guest/systemctl/systemctl_test.go",
    "content": "package systemctl\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\n// mockGuest records args passed to Run/RunQuiet and controls whether they succeed.\ntype mockGuest struct {\n\tlastArgs []string\n\terr      error\n}\n\nfunc (m *mockGuest) Run(args ...string) error      { m.lastArgs = args; return m.err }\nfunc (m *mockGuest) RunQuiet(args ...string) error { m.lastArgs = args; return m.err }\n\nfunc TestStart(t *testing.T) {\n\tg := &mockGuest{}\n\ts := New(g)\n\n\tif err := s.Start(\"docker.service\"); err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\n\tassertArgs(t, g.lastArgs, []string{\"sudo\", \"systemctl\", \"start\", \"docker.service\"})\n}\n\nfunc TestRestart(t *testing.T) {\n\tg := &mockGuest{}\n\ts := New(g)\n\n\tif err := s.Restart(\"containerd.service\"); err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\n\tassertArgs(t, g.lastArgs, []string{\"sudo\", \"systemctl\", \"restart\", \"containerd.service\"})\n}\n\nfunc TestStop(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tforce    bool\n\t\twantVerb string\n\t}{\n\t\t{name: \"graceful\", force: false, wantVerb: \"stop\"},\n\t\t{name: \"force\", force: true, wantVerb: \"kill\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tg := &mockGuest{}\n\t\t\ts := New(g)\n\n\t\t\tif err := s.Stop(\"docker.service\", tt.force); err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tassertArgs(t, g.lastArgs, []string{\"sudo\", \"systemctl\", tt.wantVerb, \"docker.service\"})\n\t\t})\n\t}\n}\n\nfunc TestActive(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tguestOK bool\n\t\twant    bool\n\t}{\n\t\t{name: \"active\", guestOK: true, want: true},\n\t\t{name: \"inactive\", guestOK: false, want: false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tg := &mockGuest{}\n\t\t\tif !tt.guestOK {\n\t\t\t\tg.err = os.ErrProcessDone\n\t\t\t}\n\t\t\ts := New(g)\n\n\t\t\tgot := s.Active(\"docker.service\")\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"Active() = %v, want %v\", got, tt.want)\n\t\t\t}\n\n\t\t\tassertArgs(t, g.lastArgs, []string{\"systemctl\", \"is-active\", \"docker.service\"})\n\t\t})\n\t}\n}\n\nfunc TestDaemonReload(t *testing.T) {\n\tg := &mockGuest{}\n\ts := New(g)\n\n\tif err := s.DaemonReload(); err != nil {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n\n\tassertArgs(t, g.lastArgs, []string{\"sudo\", \"systemctl\", \"daemon-reload\"})\n}\n\n// assertArgs fails the test if got and want differ.\nfunc assertArgs(t *testing.T, got, want []string) {\n\tt.Helper()\n\tif len(got) != len(want) {\n\t\tt.Errorf(\"args = %v, want %v\", got, want)\n\t\treturn\n\t}\n\tfor i := range want {\n\t\tif got[i] != want[i] {\n\t\t\tt.Errorf(\"args[%d] = %q, want %q (full: %v)\", i, got[i], want[i], got)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "environment/host/host.go",
    "content": "package host\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/util/terminal\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/environment\"\n)\n\n// New creates a new host environment.\nfunc New() environment.Host {\n\treturn &hostEnv{}\n}\n\nvar _ environment.Host = (*hostEnv)(nil)\n\ntype hostEnv struct {\n\tenv []string\n\tdir string // working directory\n}\n\nfunc (h hostEnv) clone() hostEnv {\n\tvar newHost hostEnv\n\tnewHost.env = append(newHost.env, h.env...)\n\tnewHost.dir = h.dir\n\treturn newHost\n}\n\nfunc (h hostEnv) WithEnv(env ...string) environment.HostActions {\n\tnewHost := h.clone()\n\t// append new env vars\n\tnewHost.env = append(newHost.env, env...)\n\treturn newHost\n}\n\nfunc (h hostEnv) WithDir(dir string) environment.HostActions {\n\tnewHost := h.clone()\n\tnewHost.dir = dir\n\treturn newHost\n}\n\nfunc (h hostEnv) Run(args ...string) error {\n\tif len(args) == 0 {\n\t\treturn errors.New(\"args not specified\")\n\t}\n\tcmd := cli.Command(args[0], args[1:]...)\n\tcmd.Env = append(os.Environ(), h.env...)\n\tif h.dir != \"\" {\n\t\tcmd.Dir = h.dir\n\t}\n\n\tlineHeight := 6\n\tif cli.Settings.Verbose {\n\t\tlineHeight = -1 // disable scrolling\n\t}\n\n\tout := terminal.NewVerboseWriter(lineHeight)\n\tcmd.Stdout = out\n\tcmd.Stderr = out\n\n\terr := cmd.Run()\n\tif err == nil {\n\t\treturn out.Close()\n\t}\n\treturn err\n}\n\nfunc (h hostEnv) RunQuiet(args ...string) error {\n\tif len(args) == 0 {\n\t\treturn errors.New(\"args not specified\")\n\t}\n\tcmd := cli.Command(args[0], args[1:]...)\n\tcmd.Env = append(os.Environ(), h.env...)\n\tif h.dir != \"\" {\n\t\tcmd.Dir = h.dir\n\t}\n\n\tvar errBuf bytes.Buffer\n\tcmd.Stdout = nil\n\tcmd.Stderr = &errBuf\n\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn errCmd(cmd.Args, errBuf, err)\n\t}\n\n\treturn nil\n}\n\nfunc (h hostEnv) RunOutput(args ...string) (string, error) {\n\tif len(args) == 0 {\n\t\treturn \"\", errors.New(\"args not specified\")\n\t}\n\n\tcmd := cli.Command(args[0], args[1:]...)\n\tcmd.Env = append(os.Environ(), h.env...)\n\tif h.dir != \"\" {\n\t\tcmd.Dir = h.dir\n\t}\n\n\tvar buf, errBuf bytes.Buffer\n\tcmd.Stdout = &buf\n\tcmd.Stderr = &errBuf\n\n\terr := cmd.Run()\n\tif err != nil {\n\t\treturn \"\", errCmd(cmd.Args, errBuf, err)\n\t}\n\n\treturn strings.TrimSpace(buf.String()), nil\n}\n\nfunc errCmd(args []string, stderr bytes.Buffer, err error) error {\n\t// this is going to be part of a log output,\n\t// reading the first line of the error should suffice\n\toutput, _ := stderr.ReadString('\\n')\n\tif len(output) > 0 {\n\t\toutput = output[:len(output)-1]\n\t}\n\treturn fmt.Errorf(\"error running %v, output: %s, err: %s\", args, strconv.Quote(output), strconv.Quote(err.Error()))\n}\n\nfunc (h hostEnv) RunInteractive(args ...string) error {\n\tif len(args) == 0 {\n\t\treturn errors.New(\"args not specified\")\n\t}\n\tcmd := cli.CommandInteractive(args[0], args[1:]...)\n\tcmd.Env = append(os.Environ(), h.env...)\n\tif h.dir != \"\" {\n\t\tcmd.Dir = h.dir\n\t}\n\treturn cmd.Run()\n}\n\nfunc (h hostEnv) RunWith(stdin io.Reader, stdout io.Writer, args ...string) error {\n\tif len(args) == 0 {\n\t\treturn errors.New(\"args not specified\")\n\t}\n\tcmd := cli.CommandInteractive(args[0], args[1:]...)\n\tcmd.Env = append(os.Environ(), h.env...)\n\tif h.dir != \"\" {\n\t\tcmd.Dir = h.dir\n\t}\n\n\tcmd.Stdin = stdin\n\tcmd.Stdout = stdout\n\n\tvar buf bytes.Buffer\n\tcmd.Stderr = &buf\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn errCmd(cmd.Args, buf, err)\n\t}\n\n\treturn nil\n}\n\nfunc (h hostEnv) Env(s string) string {\n\treturn os.Getenv(s)\n}\n\nfunc (h hostEnv) Read(fileName string) (string, error) {\n\tb, err := os.ReadFile(fileName)\n\treturn string(b), err\n}\n\nfunc (h hostEnv) Write(fileName string, body []byte) error {\n\treturn os.WriteFile(fileName, body, 0644)\n}\n\nfunc (h hostEnv) Stat(fileName string) (os.FileInfo, error) {\n\treturn os.Stat(fileName)\n}\n\n// IsInstalled checks if dependencies are installed.\nfunc IsInstalled(dependencies environment.Dependencies) error {\n\tvar missing []string\n\tcheck := func(p string) error {\n\t\t_, err := exec.LookPath(p)\n\t\treturn err\n\t}\n\tfor _, p := range dependencies.Dependencies() {\n\t\tif check(p) != nil {\n\t\t\tmissing = append(missing, p)\n\t\t}\n\t}\n\n\tif len(missing) > 0 {\n\t\treturn fmt.Errorf(\"%s not found, run 'brew install %s' to install\", strings.Join(missing, \", \"), strings.Join(missing, \" \"))\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "environment/host.go",
    "content": "package environment\n\n// Host is the host environment.\ntype Host interface {\n\tHostActions\n}\n"
  },
  {
    "path": "environment/vm/lima/certs.go",
    "content": "package lima\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/abiosoft/colima/environment/container/docker\"\n\t\"github.com/abiosoft/colima/util/downloader\"\n)\n\nfunc (l limaVM) copyCerts() error {\n\tlog := l.Logger(context.Background())\n\terr := func() error {\n\t\tdockerCertsDirHost := filepath.Join(docker.DockerDir(), \"certs.d\")\n\t\tdockerCertsDirsGuest := []string{\"/etc/docker/certs.d\", \"/etc/ssl/certs\"}\n\t\tif _, err := l.host.Stat(dockerCertsDirHost); err != nil {\n\t\t\t// no certs found\n\t\t\treturn nil\n\t\t}\n\n\t\t// copy certs from host to a temp location in the guest using limactl copy,\n\t\t// then use sudo to move them to the final destinations.\n\t\ttmpDir := \"/tmp/docker-certs\"\n\t\tif err := l.RunQuiet(\"rm\", \"-rf\", tmpDir); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := l.RunQuiet(\"mkdir\", \"-p\", tmpDir); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := downloader.CopyToGuest(l.host, dockerCertsDirHost, tmpDir); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// move from temp to final destinations\n\t\tfor _, dir := range dockerCertsDirsGuest {\n\t\t\tif err := l.RunQuiet(\"sudo\", \"mkdir\", \"-p\", dir); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := l.RunQuiet(\"sudo\", \"cp\", \"-R\", tmpDir+\"/.\", dir); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// cleanup temp\n\t\t_ = l.RunQuiet(\"rm\", \"-rf\", tmpDir)\n\n\t\treturn nil\n\t}()\n\n\t// not a fatal error, a warning suffices.\n\tif err != nil {\n\t\tlog.Warnln(fmt.Errorf(\"cannot copy registry certs to vm: %w\", err))\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "environment/vm/lima/config.go",
    "content": "package lima\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"path/filepath\"\n)\n\nconst configFile = \"/etc/colima/colima.json\"\n\nfunc (l limaVM) getConf() map[string]string {\n\tlog := l.Logger(context.Background())\n\n\tobj := map[string]string{}\n\tb, err := l.Read(configFile)\n\tif err != nil {\n\t\tlog.Trace(fmt.Errorf(\"error reading config file: %w\", err))\n\n\t\treturn obj\n\t}\n\n\t// we do not care if it fails\n\t_ = json.Unmarshal([]byte(b), &obj)\n\n\treturn obj\n}\nfunc (l limaVM) Get(key string) string {\n\tif val, ok := l.getConf()[key]; ok {\n\t\treturn val\n\t}\n\n\treturn \"\"\n}\n\nfunc (l limaVM) Set(key, value string) error {\n\tobj := l.getConf()\n\tobj[key] = value\n\n\tb, err := json.Marshal(obj)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling settings to json: %w\", err)\n\t}\n\n\tif err := l.Run(\"sudo\", \"mkdir\", \"-p\", filepath.Dir(configFile)); err != nil {\n\t\treturn fmt.Errorf(\"error saving settings: %w\", err)\n\t}\n\n\tif err := l.Write(configFile, b); err != nil {\n\t\treturn fmt.Errorf(\"error saving settings: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "environment/vm/lima/daemon.go",
    "content": "package lima\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/daemon\"\n\t\"github.com/abiosoft/colima/daemon/process/inotify\"\n\t\"github.com/abiosoft/colima/daemon/process/vmnet\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limaconfig\"\n\t\"github.com/abiosoft/colima/util\"\n)\n\nfunc (l *limaVM) startDaemon(ctx context.Context, conf config.Config) (context.Context, error) {\n\t// vmnet is used by QEMU or bridged mode\n\tuseVmnet := conf.VMType == limaconfig.QEMU || conf.Network.Mode == \"bridged\"\n\n\t// network daemon is only needed for vmnet\n\tconf.Network.Address = conf.Network.Address && useVmnet\n\n\t// limited to macOS (with vmnet required)\n\t// or with inotify enabled\n\tif !util.MacOS() || (!conf.MountINotify && !conf.Network.Address) {\n\t\treturn ctx, nil\n\t}\n\n\tctxKeyVmnet := daemon.CtxKey(vmnet.Name)\n\tctxKeyInotify := daemon.CtxKey(inotify.Name)\n\n\t// use a nested chain for convenience\n\ta := l.Init(ctx)\n\tlog := a.Logger()\n\n\tnetworkInstalledKey := struct{ key string }{key: \"network_installed\"}\n\n\t// add inotify to daemon\n\tif conf.MountINotify {\n\t\ta.Add(func() error {\n\t\t\tctx = context.WithValue(ctx, ctxKeyInotify, true)\n\t\t\tdeps, _ := l.daemon.Dependency(ctx, conf, inotify.Name)\n\t\t\tif err := deps.Install(l.host); err != nil {\n\t\t\t\treturn fmt.Errorf(\"error setting up inotify dependencies: %w\", err)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// add network processes to daemon\n\tif useVmnet {\n\t\ta.Add(func() error {\n\t\t\tif conf.Network.Address {\n\t\t\t\ta.Stage(\"preparing network\")\n\t\t\t\tctx = context.WithValue(ctx, ctxKeyVmnet, true)\n\t\t\t}\n\t\t\tdeps, root := l.daemon.Dependency(ctx, conf, vmnet.Name)\n\t\t\tif deps.Installed() {\n\t\t\t\tctx = context.WithValue(ctx, networkInstalledKey, true)\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// if user interaction is not required (i.e. root),\n\t\t\t// no need for another verbose info.\n\t\t\tif root {\n\t\t\t\tlog.Println(\"setting up reachable IP address\")\n\t\t\t\tlog.Println(\"sudo password may be required\")\n\t\t\t}\n\n\t\t\t// install deps\n\t\t\terr := deps.Install(l.host)\n\t\t\tif err != nil {\n\t\t\t\tctx = context.WithValue(ctx, networkInstalledKey, false)\n\t\t\t}\n\t\t\treturn err\n\t\t})\n\t}\n\n\t// start daemon\n\ta.Add(func() error {\n\t\treturn l.daemon.Start(ctx, conf)\n\t})\n\n\tstatusKey := struct{ key string }{key: \"daemonStatus\"}\n\t// delay to ensure that the processes have started\n\tif conf.Network.Address || conf.MountINotify {\n\t\ta.Retry(\"\", time.Second*1, 15, func(i int) error {\n\t\t\ts, err := l.daemon.Running(ctx, conf)\n\t\t\tctx = context.WithValue(ctx, statusKey, s)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif !s.Running {\n\t\t\t\treturn fmt.Errorf(\"daemon is not running\")\n\t\t\t}\n\t\t\tfor _, p := range s.Processes {\n\t\t\t\tif !p.Running {\n\t\t\t\t\treturn p.Error\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// network failure is not fatal\n\tif err := a.Exec(); err != nil {\n\t\tif useVmnet {\n\t\t\tfunc() {\n\t\t\t\tinstalled, _ := ctx.Value(networkInstalledKey).(bool)\n\t\t\t\tif !installed {\n\t\t\t\t\tlog.Warnln(fmt.Errorf(\"error setting up network dependencies: %w\", err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tstatus, ok := ctx.Value(statusKey).(daemon.Status)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif !status.Running {\n\t\t\t\t\tlog.Warnln(fmt.Errorf(\"error starting network: %w\", err))\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tfor _, p := range status.Processes {\n\t\t\t\t\t// TODO: handle inotify separate from network\n\t\t\t\t\tif p.Name == inotify.Name {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif !p.Running {\n\t\t\t\t\t\tctx = context.WithValue(ctx, daemon.CtxKey(p.Name), false)\n\t\t\t\t\t\tlog.Warnln(fmt.Errorf(\"error starting %s: %w\", p.Name, err))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t}\n\n\t// check if inotify is running\n\tif conf.MountINotify {\n\t\tif inotifyEnabled, _ := ctx.Value(ctxKeyInotify).(bool); !inotifyEnabled {\n\t\t\tlog.Warnln(\"error occurred enabling inotify daemon\")\n\t\t}\n\t}\n\n\t// preserve vmnet context\n\tif vmnetEnabled, _ := ctx.Value(ctxKeyVmnet).(bool); vmnetEnabled {\n\t\t// env var for subprocess to detect vmnet\n\t\tl.host = l.host.WithEnv(vmnet.SubProcessEnvVar + \"=1\")\n\t}\n\n\treturn ctx, nil\n}\n"
  },
  {
    "path": "environment/vm/lima/disk.go",
    "content": "package lima\n\nimport (\n\t\"context\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/config/configmanager\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/environment/container/containerd\"\n\t\"github.com/abiosoft/colima/environment/container/docker\"\n\t\"github.com/abiosoft/colima/environment/container/incus\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limaconfig\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n\t\"github.com/abiosoft/colima/store\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/downloader\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n//go:embed disk.sh\nvar diskScript string\n\nfunc (l *limaVM) createRuntimeDisk(conf config.Config) error {\n\tif environment.IsNoneRuntime(conf.Runtime) {\n\t\t// runtime disk is not required when no runtime is in use\n\t\treturn nil\n\t}\n\n\tdisk := dataDisk(conf.Runtime)\n\n\ts, _ := store.Load()\n\tformat := !s.DiskFormatted // only format if not previously formatted\n\n\tif !limautil.HasDisk() {\n\t\tif err := limautil.CreateDisk(conf.Disk); err != nil {\n\t\t\treturn fmt.Errorf(\"error creating runtime disk: %w\", err)\n\t\t}\n\t\tformat = true // new disk should be formated\n\t}\n\n\t// when disk is formatted for the wrong runtime, prevent use\n\tif s.DiskFormatted && s.DiskRuntime != \"\" && s.DiskRuntime != conf.Runtime {\n\t\treturn fmt.Errorf(\"runtime disk provisioned for %s runtime. Delete container data with 'colima delete --data' before using another runtime\", s.DiskRuntime)\n\t}\n\n\tl.limaConf.Disk = config.Disk(conf.RootDisk).GiB()\n\tl.limaConf.AdditionalDisks = append(l.limaConf.AdditionalDisks, limaconfig.Disk{\n\t\tName:   config.CurrentProfile().ID,\n\t\tFormat: format,\n\t\tFSType: disk.FSType,\n\t})\n\n\tl.mountRuntimeDisk(conf, format)\n\treturn nil\n}\n\nfunc (l *limaVM) useRuntimeDisk(conf config.Config) {\n\tif !limautil.HasDisk() {\n\t\tl.limaConf.Disk = config.Disk(conf.Disk).GiB()\n\t\treturn\n\t}\n\n\tdisk := dataDisk(conf.Runtime)\n\n\ts, _ := store.Load()\n\tformat := !s.DiskFormatted // only format if not previously formatted\n\n\tl.limaConf.Disk = config.Disk(conf.RootDisk).GiB()\n\tl.limaConf.AdditionalDisks = append(l.limaConf.AdditionalDisks, limaconfig.Disk{\n\t\tName:   config.CurrentProfile().ID,\n\t\tFormat: format,\n\t\tFSType: disk.FSType,\n\t})\n\n\tl.mountRuntimeDisk(conf, format)\n}\n\nfunc dataDisk(runtime string) environment.DataDisk {\n\tswitch runtime {\n\tcase docker.Name:\n\t\treturn docker.DataDisk()\n\tcase containerd.Name:\n\t\treturn containerd.DataDisk()\n\tcase incus.Name:\n\t\treturn incus.DataDisk()\n\t}\n\n\treturn environment.DataDisk{}\n}\n\nfunc diskMountScript(format bool) string {\n\tvar values = struct {\n\t\tFormat     bool\n\t\tInstanceId string\n\t}{\n\t\tFormat:     format,\n\t\tInstanceId: config.CurrentProfile().ID,\n\t}\n\n\tb, err := util.ParseTemplate(diskScript, values)\n\tif err != nil {\n\t\t// must never happen\n\t\tpanic(fmt.Sprintf(\"error parsing disk mount script template: %v\", err))\n\t}\n\treturn string(b)\n}\n\nfunc (l *limaVM) mountRuntimeDisk(conf config.Config, format bool) {\n\t// provision script to prepare disk\n\tl.limaConf.Provision = append(l.limaConf.Provision, limaconfig.Provision{\n\t\tMode:   \"dependency\",\n\t\tScript: diskMountScript(format),\n\t})\n\n\t// handle disk mounts\n\tdisk := dataDisk(conf.Runtime)\n\n\t// pre mount script\n\tfor _, script := range disk.PreMount {\n\t\tl.limaConf.Provision = append(l.limaConf.Provision, limaconfig.Provision{\n\t\t\tMode:   \"dependency\",\n\t\t\tScript: script,\n\t\t})\n\t}\n\n\tmountPoint := limautil.MountPoint()\n\tfor _, dir := range disk.Dirs {\n\t\tscript := strings.NewReplacer(\n\t\t\t\"{mount_point}\", mountPoint,\n\t\t\t\"{name}\", dir.Name,\n\t\t\t\"{data_path}\", dir.Path,\n\t\t).Replace(\"[ -d {mount_point} ] && mkdir -p {mount_point}/{name} {data_path} && mount --bind {mount_point}/{name} {data_path}\")\n\n\t\tl.limaConf.Provision = append(l.limaConf.Provision, limaconfig.Provision{\n\t\t\tMode:   \"dependency\",\n\t\t\tScript: script,\n\t\t})\n\t}\n}\n\nfunc (l *limaVM) downloadDiskImage(ctx context.Context, conf config.Config) error {\n\tlog := l.Logger(ctx)\n\n\t// use a user specified disk image\n\tif conf.DiskImage != \"\" {\n\t\tif _, err := os.Stat(conf.DiskImage); err != nil {\n\t\t\treturn fmt.Errorf(\"invalid disk image: %w\", err)\n\t\t}\n\n\t\timage, err := limautil.Image(l.limaConf.Arch, conf.Runtime)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error getting disk image details: %w\", err)\n\t\t}\n\n\t\tsha := downloader.SHA{Size: 512, Digest: image.Digest}\n\t\tif err := sha.ValidateFile(l.host, conf.DiskImage); err != nil {\n\t\t\treturn fmt.Errorf(\"disk image must be downloaded from '%s', hash failure: %w\", image.Location, err)\n\t\t}\n\n\t\timage.Location = conf.DiskImage\n\t\tl.limaConf.Images = []limaconfig.File{image}\n\t\treturn nil\n\t}\n\n\t// use a previously cached image\n\tif image, ok := limautil.ImageCached(l.limaConf.Arch, conf.Runtime); ok {\n\t\tl.limaConf.Images = []limaconfig.File{image}\n\t\treturn nil\n\t}\n\n\t// download image\n\tlog.Infoln(\"downloading disk image ...\")\n\timage, err := limautil.DownloadImage(l.limaConf.Arch, conf.Runtime)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting qcow image: %w\", err)\n\t}\n\n\tl.limaConf.Images = []limaconfig.File{image}\n\treturn nil\n}\n\nfunc (l *limaVM) setDiskImage() error {\n\tvar c limaconfig.Config\n\tb, err := os.ReadFile(config.CurrentProfile().LimaFile())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := yaml.Unmarshal(b, &c); err != nil {\n\t\treturn err\n\t}\n\n\tl.limaConf.Images = c.Images\n\treturn nil\n}\n\nfunc (l *limaVM) syncDiskSize(ctx context.Context, conf config.Config) config.Config {\n\tlog := l.Logger(ctx)\n\tinstance, err := configmanager.LoadInstance()\n\tif err != nil {\n\t\t// instance config missing, ignore\n\t\treturn conf\n\t}\n\n\tresized := func() bool {\n\t\tif instance.Disk == conf.Disk {\n\t\t\t// nothing to do\n\t\t\treturn false\n\t\t}\n\n\t\tsize := conf.Disk - instance.Disk\n\t\tif size < 0 {\n\t\t\tlog.Warnln(\"disk size cannot be reduced, ignoring...\")\n\t\t\treturn false\n\t\t}\n\n\t\tif err := limautil.ResizeDisk(conf.Disk); err != nil {\n\t\t\tlog.Warnln(fmt.Errorf(\"unable to resize disk: %w\", err))\n\t\t\treturn false\n\t\t}\n\n\t\tlog.Printf(\"resizing disk to %dGiB...\", conf.Disk)\n\t\treturn true\n\t}()\n\n\tif !resized {\n\t\tconf.Disk = instance.Disk\n\t}\n\n\treturn conf\n}\n"
  },
  {
    "path": "environment/vm/lima/disk.sh",
    "content": "#!/usr/bin/env sh\n\n# Steps:\n# 1. Check if directory is already mounted, if yes, skip setup\n# 2. Idenify disk e.g. /dev/vdb or /dev/vdc\n# 3. Format disk with ext4 if not already formatted\n# 4. Label disk with instance id\n# 5. Mount disk\n\nDISK_LABEL=\"lima-{{ .InstanceId }}\"\nMOUNT_POINT=\"/mnt/${DISK_LABEL}\"\n\n# Directory already mounted, skip setup\nif [ -d \"$MOUNT_POINT\" ]; then\n    if [ -n \"$(find \"$DIR\" -mindepth 1 -print -quit 2>/dev/null)\" ]; then\n        echo \"Disk already mounted, skipping setup.\"\n        exit 0\n    fi\nfi\n\n# Detect the disk to use e.g. /dev/vdb or /dev/vdc\nDISK=\"/dev/vdb\"\nif df -h /mnt/lima-cidata/ | tail -n +2 | grep '^/dev/vdb'; then\n    DISK=\"/dev/vdc\"\nfi\nDISK_PART=\"${DISK}1\"\n\n{{ if .Format }}\necho 'type=83' | sudo sfdisk \"$DISK\"\nmkfs.ext4 \"$DISK_PART\"\ne2label \"$DISK_PART\" \"$DISK_LABEL\"\n{{ end }}\n\n# mount disk\nmkdir -p \"$MOUNT_POINT\"\nmount \"$DISK_PART\" \"$MOUNT_POINT\"\n\n"
  },
  {
    "path": "environment/vm/lima/dns.go",
    "content": "package lima\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n)\n\nconst (\n\tlocalhostAddr         = \"127.0.0.1\"\n\tdefaultGatewayAddress = \"192.168.5.2\"\n)\n\nfunc hasDnsmasq(l *limaVM) bool {\n\t// check if dnsmasq is installed\n\treturn l.RunQuiet(\"sh\", \"-c\", `apt list | grep 'dnsmasq\\/' | grep '\\[installed'`) == nil\n}\n\nfunc (l *limaVM) setupDNS(conf config.Config) error {\n\tif !hasDnsmasq(l) {\n\t\t// older image still using systemd-resolved\n\t\t// ignore\n\t\treturn nil\n\t}\n\n\t// use custom gateway address\n\tvar gatewayAddr = defaultGatewayAddress\n\tcustomGatewayAddress := conf.Network.GatewayAddress\n\tif customGatewayAddress != nil {\n\t\tgatewayAddr = customGatewayAddress.String()\n\t}\n\n\tvar dnsHosts = map[string]string{\n\t\t\"host.docker.internal\": gatewayAddr,\n\t\t\"host.lima.internal\":   gatewayAddr,\n\t}\n\n\tinternalIP := limautil.InternalIPAddress(config.CurrentProfile().ID)\n\n\t// extra dns entries\n\tdnsHosts[\"colima.internal\"] = internalIP\n\tif (conf.Hostname) != \"\" {\n\t\tdnsHosts[conf.Hostname] = localhostAddr\n\t}\n\n\tvar buf bytes.Buffer\n\n\t// generate dns hosts\n\tfmt.Fprintln(&buf, \"# Generated by Colima\")\n\tfmt.Fprintln(&buf, \"# Do not edit this file manually\")\n\tfmt.Fprintln(&buf)\n\tfor k, v := range dnsHosts {\n\t\tfmt.Fprintf(&buf, \"address=/%s/%s\", k, v)\n\t\tfmt.Fprintln(&buf)\n\t}\n\tfmt.Fprintln(&buf) // for cleaner output\n\n\t// generate dns servers\n\tdnsServers := []string{gatewayAddr}\n\tif len(conf.Network.DNSResolvers) > 0 {\n\t\tdnsServers = nil\n\t\tfor _, dns := range conf.Network.DNSResolvers {\n\t\t\tdnsServers = append(dnsServers, dns.String())\n\t\t}\n\t}\n\tfor _, dns := range dnsServers {\n\t\tfmt.Fprintf(&buf, \"server=%s\", dns)\n\t\tfmt.Fprintln(&buf)\n\t}\n\tfmt.Fprintln(&buf) // for cleaner output\n\n\t// set dnsmasq listening interface and address\n\tfmt.Fprintln(&buf, \"interface=eth0\")\n\tfmt.Fprintln(&buf, \"listen-address=\"+internalIP)\n\tfmt.Fprintln(&buf, \"bind-interfaces\")\n\n\t// ensure dnsmasq config directory exists\n\tif err := l.RunQuiet(\"sudo\", \"mkdir\", \"-p\", \"/etc/dnsmasq.d\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to create dnsmasq config directory: %w\", err)\n\t}\n\n\t// write config to dnsmasq directory\n\tif err := l.Write(\"/etc/dnsmasq.d/01-colima.conf\", buf.Bytes()); err != nil {\n\t\treturn fmt.Errorf(\"failed to write dnsmasq config: %w\", err)\n\t}\n\n\t// remove existing resolv.conf file\n\tif err := l.RunQuiet(\"sudo\", \"rm\", \"-f\", \"/etc/resolv.conf\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to remove existing resolv.conf: %w\", err)\n\t}\n\n\t// replace resolv.conf with a custom one\n\tresolvConf := fmt.Sprintf(\"# Generated by Colima\\n\\nnameserver %s\\n\", internalIP)\n\tif err := l.Write(\"/etc/resolv.conf\", []byte(resolvConf)); err != nil {\n\t\treturn fmt.Errorf(\"failed to write resolv.conf: %w\", err)\n\t}\n\n\t// restart dnsmasq service to apply changes\n\tif err := l.RunQuiet(\"sudo\", \"systemctl\", \"restart\", \"dnsmasq\"); err != nil {\n\t\treturn fmt.Errorf(\"failed to restart dnsmasq service: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "environment/vm/lima/file.go",
    "content": "package lima\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/environment\"\n)\n\nfunc (l limaVM) Read(fileName string) (string, error) {\n\ts, err := l.RunOutput(\"sudo\", \"cat\", fileName)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"cannot read file '%s': %w\", fileName, err)\n\t}\n\treturn s, err\n}\n\nfunc (l *limaVM) Write(fileName string, body []byte) error {\n\tvar stdin = bytes.NewReader(body)\n\tdir := filepath.Dir(fileName)\n\tif err := l.RunQuiet(\"sudo\", \"mkdir\", \"-p\", dir); err != nil {\n\t\treturn fmt.Errorf(\"error creating directory '%s': %w\", dir, err)\n\t}\n\treturn l.RunWith(stdin, nil, \"sudo\", \"sh\", \"-c\", \"cat > \"+fileName)\n}\n\nfunc (l *limaVM) Stat(fileName string) (os.FileInfo, error) {\n\treturn newFileInfo(l, fileName)\n}\n\nvar _ os.FileInfo = (*fileInfo)(nil)\n\ntype fileInfo struct {\n\tisDir   bool\n\tmodTime time.Time\n\tmode    fs.FileMode\n\tname    string\n\tsize    int64\n}\n\nfunc newFileInfo(guest environment.GuestActions, filename string) (fileInfo, error) {\n\tinfo := fileInfo{}\n\t// \"%s,%a,%Y,%F\" -> size, permission, modified time, type\n\tstat, err := guest.RunOutput(\"sudo\", \"stat\", \"-c\", \"%s,%a,%Y,%F\", filename)\n\tif err != nil {\n\t\treturn info, statError(filename, err)\n\t}\n\tstats := strings.Split(stat, \",\")\n\tif len(stats) < 4 {\n\t\treturn info, statError(filename, err)\n\t}\n\tinfo.name = filename\n\tinfo.size, _ = strconv.ParseInt(stats[0], 10, 64)\n\tinfo.mode = func() fs.FileMode {\n\t\tmode, _ := strconv.ParseUint(stats[1], 10, 32)\n\t\treturn fs.FileMode(mode)\n\t}()\n\tinfo.modTime = func() time.Time {\n\t\tunix, _ := strconv.ParseInt(stats[2], 10, 64)\n\t\treturn time.Unix(unix, 0)\n\t}()\n\tinfo.isDir = stats[3] == \"directory\"\n\n\treturn info, nil\n}\n\nfunc statError(filename string, err error) error {\n\treturn fmt.Errorf(\"cannot stat file '%s': %w\", filename, err)\n}\n\n// IsDir implements fs.FileInfo\nfunc (f fileInfo) IsDir() bool { return f.isDir }\n\n// ModTime implements fs.FileInfo\nfunc (f fileInfo) ModTime() time.Time { return f.modTime }\n\n// Mode implements fs.FileInfo\nfunc (f fileInfo) Mode() fs.FileMode { return f.mode }\n\n// Name implements fs.FileInfo\nfunc (f fileInfo) Name() string { return f.name }\n\n// Size implements fs.FileInfo\nfunc (f fileInfo) Size() int64 { return f.size }\n\n// Sys implements fs.FileInfo\nfunc (fileInfo) Sys() any { return nil }\n"
  },
  {
    "path": "environment/vm/lima/lima.go",
    "content": "package lima\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/config/configmanager\"\n\t\"github.com/abiosoft/colima/core\"\n\t\"github.com/abiosoft/colima/daemon\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limaconfig\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n\t\"github.com/abiosoft/colima/store\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/osutil\"\n\t\"github.com/abiosoft/colima/util/yamlutil\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// New creates a new virtual machine.\nfunc New(host environment.HostActions) environment.VM {\n\t// lima config directory\n\tlimaHome := config.LimaDir()\n\n\t// environment variables for the subprocesses\n\tvar envs []string\n\tenvHome := limautil.EnvLimaHome + \"=\" + limaHome\n\tenvLimaInstance := envLimaInstance + \"=\" + config.CurrentProfile().ID\n\tenvBinary := osutil.EnvColimaBinary + \"=\" + osutil.Executable()\n\tenvs = append(envs, envHome, envLimaInstance, envBinary)\n\n\t// consider making this truly flexible to support other VMs\n\treturn &limaVM{\n\t\thost:         host.WithEnv(envs...),\n\t\tlimaHome:     limaHome,\n\t\tCommandChain: cli.New(\"vm\"),\n\t\tdaemon:       daemon.NewManager(host),\n\t}\n}\n\nconst (\n\tenvLimaInstance = \"LIMA_INSTANCE\"\n\tlima            = \"lima\"\n\tlimactl         = limautil.LimactlCommand\n)\n\nvar _ environment.VM = (*limaVM)(nil)\n\ntype limaVM struct {\n\thost environment.HostActions\n\tcli.CommandChain\n\n\t// keep config in case of restart\n\tconf config.Config\n\n\t// lima config\n\tlimaConf limaconfig.Config\n\n\t// lima config directory\n\tlimaHome string\n\n\t// network between host and the vm\n\tdaemon daemon.Manager\n}\n\nfunc (l limaVM) Dependencies() []string {\n\treturn []string{\n\t\t\"lima\",\n\t}\n}\n\nfunc (l *limaVM) Start(ctx context.Context, conf config.Config) error {\n\ta := l.Init(ctx)\n\n\tl.prepareHost(conf)\n\n\tif l.Created() {\n\t\treturn l.resume(ctx, conf)\n\t}\n\n\ta.Add(func() (err error) {\n\t\tctx, err = l.startDaemon(ctx, conf)\n\t\treturn err\n\t})\n\n\ta.Stage(\"creating and starting\")\n\tconfFile := filepath.Join(os.TempDir(), config.CurrentProfile().ID+\".yaml\")\n\n\ta.Add(func() (err error) {\n\t\tl.limaConf, err = newConf(ctx, conf)\n\t\treturn err\n\t})\n\n\ta.Add(l.assertQemu)\n\n\ta.Add(func() error {\n\t\treturn l.createRuntimeDisk(conf)\n\t})\n\n\ta.Add(func() error {\n\t\treturn l.downloadDiskImage(ctx, conf)\n\t})\n\n\ta.Add(func() error {\n\t\treturn yamlutil.WriteYAML(l.limaConf, confFile)\n\t})\n\n\ta.Add(func() error { return l.writeNetworkFile(conf) })\n\n\ta.Add(func() error {\n\t\treturn l.host.Run(limactl, \"start\", \"--tty=false\", confFile)\n\t})\n\ta.Add(func() error {\n\t\treturn os.Remove(confFile)\n\t})\n\n\t// adding it to command chain to execute only after successful startup.\n\ta.Add(func() error {\n\t\tl.conf = conf\n\t\treturn nil\n\t})\n\n\tl.addPostStartActions(a, conf)\n\n\treturn a.Exec()\n}\n\nfunc (l *limaVM) resume(ctx context.Context, conf config.Config) error {\n\tlog := l.Logger(ctx)\n\ta := l.Init(ctx)\n\n\tif l.Running(ctx) {\n\t\tlog.Println(\"already running\")\n\t\treturn nil\n\t}\n\n\ta.Add(func() (err error) {\n\t\tctx, err = l.startDaemon(ctx, conf)\n\t\treturn err\n\t})\n\n\ta.Add(func() (err error) {\n\t\t// disk must be resized before starting\n\t\tconf = l.syncDiskSize(ctx, conf)\n\n\t\tl.limaConf, err = newConf(ctx, conf)\n\t\treturn err\n\t})\n\n\ta.Add(l.assertQemu)\n\n\ta.Add(func() error {\n\t\tl.useRuntimeDisk(conf)\n\t\treturn nil\n\t})\n\n\ta.Add(l.setDiskImage)\n\n\ta.Add(func() error {\n\t\terr := yamlutil.WriteYAML(l.limaConf, config.CurrentProfile().LimaFile())\n\t\treturn err\n\t})\n\n\ta.Add(func() error { return l.writeNetworkFile(conf) })\n\n\ta.Stage(\"starting\")\n\ta.Add(func() error {\n\t\treturn l.host.Run(limactl, \"start\", config.CurrentProfile().ID)\n\t})\n\n\tl.addPostStartActions(a, conf)\n\n\treturn a.Exec()\n}\n\nfunc (l limaVM) Running(_ context.Context) bool {\n\ti, err := limautil.Instance()\n\tif err != nil {\n\t\tlogrus.Trace(fmt.Errorf(\"error retrieving running instance: %w\", err))\n\t\treturn false\n\t}\n\treturn i.Running()\n}\n\nfunc (l limaVM) Stop(ctx context.Context, force bool) error {\n\tlog := l.Logger(ctx)\n\ta := l.Init(ctx)\n\tif !l.Running(ctx) && !force {\n\t\tlog.Println(\"not running\")\n\t\treturn nil\n\t}\n\n\ta.Stage(\"stopping\")\n\n\tif util.MacOS() {\n\t\tconf, _ := configmanager.LoadInstance()\n\t\ta.Retry(\"\", time.Second*1, 10, func(retryCount int) error {\n\t\t\terr := l.daemon.Stop(ctx, conf)\n\t\t\tif err != nil {\n\t\t\t\terr = cli.ErrNonFatal(err)\n\t\t\t}\n\t\t\treturn err\n\t\t})\n\t}\n\n\ta.Add(func() error { l.removeHostAddresses(); return nil })\n\n\ta.Add(func() error { l.removeIncusContainerRoute(); return nil })\n\n\ta.Add(func() error {\n\t\tif force {\n\t\t\treturn l.host.Run(limactl, \"stop\", \"--force\", config.CurrentProfile().ID)\n\t\t}\n\t\treturn l.host.Run(limactl, \"stop\", config.CurrentProfile().ID)\n\t})\n\n\treturn a.Exec()\n}\n\nfunc (l limaVM) Teardown(ctx context.Context) error {\n\ta := l.Init(ctx)\n\n\tif util.MacOS() {\n\t\tconf, _ := configmanager.LoadInstance()\n\t\ta.Retry(\"\", time.Second*1, 10, func(retryCount int) error {\n\t\t\treturn l.daemon.Stop(ctx, conf)\n\t\t})\n\t}\n\n\ta.Add(func() error {\n\t\treturn l.host.Run(limactl, \"delete\", \"--force\", config.CurrentProfile().ID)\n\t})\n\n\treturn a.Exec()\n}\n\nfunc (l limaVM) Restart(ctx context.Context) error {\n\tif l.conf.Empty() {\n\t\treturn fmt.Errorf(\"cannot restart, VM not previously started\")\n\t}\n\n\tif err := l.Stop(ctx, false); err != nil {\n\t\treturn err\n\t}\n\n\t// minor delay to prevent possible race condition.\n\ttime.Sleep(time.Second * 2)\n\n\tif err := l.Start(ctx, l.conf); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (l limaVM) Host() environment.HostActions {\n\treturn l.host\n}\n\nfunc (l limaVM) Env(s string) (string, error) {\n\tctx := context.Background()\n\tif !l.Running(ctx) {\n\t\treturn \"\", fmt.Errorf(\"not running\")\n\t}\n\treturn l.RunOutput(\"echo\", \"$\"+s)\n}\n\nfunc (l limaVM) Created() bool {\n\tstat, err := os.Stat(config.CurrentProfile().LimaFile())\n\treturn err == nil && !stat.IsDir()\n}\n\nfunc (l limaVM) User() (string, error) {\n\treturn l.RunOutput(\"whoami\")\n}\n\nfunc (l limaVM) Arch() environment.Arch {\n\ta, _ := l.RunOutput(\"uname\", \"-m\")\n\treturn environment.Arch(a)\n}\n\nfunc (l *limaVM) addPostStartActions(a *cli.ActiveCommandChain, conf config.Config) {\n\t// setup dns\n\ta.Add(func() error {\n\t\tif err := l.setupDNS(conf); err != nil {\n\t\t\treturn fmt.Errorf(\"error setting up DNS: %w\", err)\n\t\t}\n\t\treturn nil\n\t})\n\n\t// registry certs\n\ta.Add(l.copyCerts)\n\n\t// cross-platform emulation\n\ta.Add(func() error {\n\t\t// use binfmt when emulation is disabled i.e. host arch\n\t\tif conf.Binfmt != nil && *conf.Binfmt {\n\t\t\tif arch := environment.HostArch(); arch == environment.Arch(conf.Arch).Value() {\n\t\t\t\tif err := core.SetupBinfmt(l.host, l, environment.Arch(conf.Arch)); err != nil {\n\t\t\t\t\tlogrus.Warn(fmt.Errorf(\"unable to enable qemu %s emulation: %w\", arch, err))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif l.limaConf.VMOpts.VZOpts.Rosetta.Enabled {\n\t\t\t// enable rosetta\n\t\t\terr := 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`)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Warn(fmt.Errorf(\"unable to enable rosetta: %w\", err))\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// disable qemu\n\t\t\tif err := l.RunQuiet(\"stat\", \"/proc/sys/fs/binfmt_misc/qemu-x86_64\"); err == nil {\n\t\t\t\terr = l.Run(\"sudo\", \"sh\", \"-c\", `echo 0 > /proc/sys/fs/binfmt_misc/qemu-x86_64`)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogrus.Warn(fmt.Errorf(\"unable to disable qemu x86_84 emulation: %w\", err))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\t// replicate addresses when network address is disabled\n\ta.Add(func() error {\n\t\tif err := l.replicateHostAddresses(conf); err != nil {\n\t\t\tlogrus.Warnln(fmt.Errorf(\"unable to assign host IP addresses to the VM: %w\", err))\n\t\t}\n\t\treturn nil\n\t})\n\n\t// preserve state\n\ta.Add(func() error {\n\t\tif err := configmanager.SaveToFile(conf, config.CurrentProfile().StateFile()); err != nil {\n\t\t\tlogrus.Warnln(fmt.Errorf(\"error persisting Colima state: %w\", err))\n\t\t}\n\t\treturn nil\n\t})\n\n\t// save store settings\n\ta.Add(func() error {\n\t\tif len(l.limaConf.AdditionalDisks) == 0 {\n\t\t\treturn nil\n\t\t}\n\n\t\t// startup is successful\n\t\t// if additional disk is present, then it must've been formatted correctly.\n\t\tif err := store.Set(func(s *store.Store) {\n\t\t\ts.DiskFormatted = true\n\t\t}); err != nil {\n\t\t\t// not fatal, but should be logged\n\t\t\tlogrus.Warnln(fmt.Errorf(\"error persisting store settings: %w\", err))\n\t\t}\n\n\t\treturn nil\n\t})\n\n}\n\nfunc (l *limaVM) assertQemu() error {\n\t// assert qemu requirement\n\tsameArchitecture := environment.HostArch() == l.limaConf.Arch\n\tif err := util.AssertQemuImg(); err != nil && l.limaConf.VMType == limaconfig.QEMU {\n\t\tif !sameArchitecture {\n\t\t\treturn fmt.Errorf(\"qemu is required to emulate %s: %w\", l.limaConf.Arch, err)\n\t\t}\n\t\treturn err\n\t}\n\treturn nil\n}\n\nconst envLimaSSHPortForwarder = \"LIMA_SSH_PORT_FORWARDER\"\n\nfunc (l *limaVM) prepareHost(conf config.Config) {\n\tuseSSHPortForwarder := conf.PortForwarder != \"grpc\"\n\n\tl.host = l.host.WithEnv(envLimaSSHPortForwarder + \"=\" + strconv.FormatBool(useSSHPortForwarder))\n}\n"
  },
  {
    "path": "environment/vm/lima/limaconfig/config.go",
    "content": "package limaconfig\n\nimport (\n\t\"net\"\n\n\t\"github.com/abiosoft/colima/environment\"\n)\n\ntype Arch = environment.Arch\n\n// Config is lima config. Code copied from lima and modified.\ntype Config struct {\n\tVMType               VMType            `yaml:\"vmType,omitempty\" json:\"vmType,omitempty\"`\n\tVMOpts               VMOpts            `yaml:\"vmOpts,omitempty\" json:\"vmOpts,omitempty\"`\n\tArch                 Arch              `yaml:\"arch,omitempty\"`\n\tImages               []File            `yaml:\"images\"`\n\tCPUs                 *int              `yaml:\"cpus,omitempty\"`\n\tMemory               string            `yaml:\"memory,omitempty\"`\n\tDisk                 string            `yaml:\"disk,omitempty\"`\n\tAdditionalDisks      []Disk            `yaml:\"additionalDisks,omitempty\" json:\"additionalDisks,omitempty\"`\n\tMounts               []Mount           `yaml:\"mounts,omitempty\"`\n\tMountType            MountType         `yaml:\"mountType,omitempty\" json:\"mountType,omitempty\"`\n\tSSH                  SSH               `yaml:\"ssh\"`\n\tContainerd           Containerd        `yaml:\"containerd\"`\n\tEnv                  map[string]string `yaml:\"env,omitempty\"`\n\tDNS                  []net.IP          `yaml:\"dns\"`\n\tFirmware             Firmware          `yaml:\"firmware\"`\n\tHostResolver         HostResolver      `yaml:\"hostResolver\"`\n\tPortForwards         []PortForward     `yaml:\"portForwards,omitempty\"`\n\tNetworks             []Network         `yaml:\"networks,omitempty\"`\n\tProvision            []Provision       `yaml:\"provision,omitempty\" json:\"provision,omitempty\"`\n\tNestedVirtualization bool              `yaml:\"nestedVirtualization,omitempty\" json:\"nestedVirtualization,omitempty\"`\n}\n\ntype File struct {\n\tLocation string `yaml:\"location\"` // REQUIRED\n\tArch     Arch   `yaml:\"arch,omitempty\"`\n\tDigest   string `yaml:\"digest,omitempty\"`\n}\n\ntype Mount struct {\n\tLocation   string `yaml:\"location\"` // REQUIRED\n\tMountPoint string `yaml:\"mountPoint,omitempty\"`\n\tWritable   bool   `yaml:\"writable\"`\n\tNineP      NineP  `yaml:\"9p,omitempty\" json:\"9p,omitempty\"`\n}\n\ntype Disk struct {\n\tName   string   `yaml:\"name\" json:\"name\"` // REQUIRED\n\tFormat bool     `yaml:\"format\" json:\"format\"`\n\tFSType string   `yaml:\"fsType,omitempty\" json:\"fsType,omitempty\"`\n\tFSArgs []string `yaml:\"fsArgs,omitempty\" json:\"fsArgs,omitempty\"`\n}\n\ntype SSH struct {\n\tLocalPort         int  `yaml:\"localPort,omitempty\"`\n\tLoadDotSSHPubKeys bool `yaml:\"loadDotSSHPubKeys\"`\n\tForwardAgent      bool `yaml:\"forwardAgent\"` // default: false\n}\n\ntype Containerd struct {\n\tSystem bool `yaml:\"system\"` // default: false\n\tUser   bool `yaml:\"user\"`   // default: true\n}\n\ntype Firmware struct {\n\t// LegacyBIOS disables UEFI if set.\n\t// LegacyBIOS is ignored for aarch64.\n\tLegacyBIOS bool `yaml:\"legacyBIOS\"`\n}\n\ntype (\n\tProto     = string\n\tMountType = string\n\tVMType    = string\n)\n\nconst (\n\tTCP Proto = \"tcp\"\n\tUDP Proto = \"udp\"\n\n\tREVSSHFS MountType = \"reverse-sshfs\"\n\tNINEP    MountType = \"9p\"\n\tVIRTIOFS MountType = \"virtiofs\"\n\n\tKrunkit VMType = \"krunkit\"\n\tQEMU    VMType = \"qemu\"\n\tVZ      VMType = \"vz\"\n)\n\ntype PortForward struct {\n\tGuestIPMustBeZero bool   `yaml:\"guestIPMustBeZero,omitempty\" json:\"guestIPMustBeZero,omitempty\"`\n\tGuestIP           net.IP `yaml:\"guestIP,omitempty\" json:\"guestIP,omitempty\"`\n\tGuestPort         int    `yaml:\"guestPort,omitempty\" json:\"guestPort,omitempty\"`\n\tGuestPortRange    [2]int `yaml:\"guestPortRange,omitempty\" json:\"guestPortRange,omitempty\"`\n\tGuestSocket       string `yaml:\"guestSocket,omitempty\" json:\"guestSocket,omitempty\"`\n\tHostIP            net.IP `yaml:\"hostIP,omitempty\" json:\"hostIP,omitempty\"`\n\tHostPort          int    `yaml:\"hostPort,omitempty\" json:\"hostPort,omitempty\"`\n\tHostPortRange     [2]int `yaml:\"hostPortRange,omitempty\" json:\"hostPortRange,omitempty\"`\n\tHostSocket        string `yaml:\"hostSocket,omitempty\" json:\"hostSocket,omitempty\"`\n\tProto             Proto  `yaml:\"proto,omitempty\" json:\"proto,omitempty\"`\n\tIgnore            bool   `yaml:\"ignore,omitempty\" json:\"ignore,omitempty\"`\n}\n\ntype HostResolver struct {\n\tEnabled bool              `yaml:\"enabled\" json:\"enabled\"`\n\tIPv6    bool              `yaml:\"ipv6,omitempty\" json:\"ipv6,omitempty\"`\n\tHosts   map[string]string `yaml:\"hosts,omitempty\" json:\"hosts,omitempty\"`\n}\n\ntype Network struct {\n\t// `Lima`, `Socket`, and `VNL` are mutually exclusive; exactly one is required\n\tLima string `yaml:\"lima,omitempty\" json:\"lima,omitempty\"`\n\t// Socket is a QEMU-compatible socket\n\tSocket string `yaml:\"socket,omitempty\" json:\"socket,omitempty\"`\n\t// VZNAT uses VZNATNetworkDeviceAttachment. Needs VZ. No root privilege is required.\n\tVZNAT bool `yaml:\"vzNAT,omitempty\" json:\"vzNAT,omitempty\"`\n\n\t// VNLDeprecated is a Virtual Network Locator (https://github.com/rd235/vdeplug4/commit/089984200f447abb0e825eb45548b781ba1ebccd).\n\t// On macOS, only VDE2-compatible form (optionally with vde:// prefix) is supported.\n\t// VNLDeprecated is deprecated. Use Socket.\n\tVNLDeprecated        string `yaml:\"vnl,omitempty\" json:\"vnl,omitempty\"`\n\tSwitchPortDeprecated uint16 `yaml:\"switchPort,omitempty\" json:\"switchPort,omitempty\"` // VDE Switch port, not TCP/UDP port (only used by VDE networking)\n\tMACAddress           string `yaml:\"macAddress,omitempty\" json:\"macAddress,omitempty\"`\n\tInterface            string `yaml:\"interface,omitempty\" json:\"interface,omitempty\"`\n\tMetric               uint32 `yaml:\"metric,omitempty\" json:\"metric,omitempty\"`\n}\n\ntype ProvisionMode = string\n\nconst (\n\tProvisionModeSystem     ProvisionMode = \"system\"\n\tProvisionModeUser       ProvisionMode = \"user\"\n\tProvisionModeBoot       ProvisionMode = \"boot\"\n\tProvisionModeDependency ProvisionMode = \"dependency\"\n)\n\ntype Provision struct {\n\tMode           ProvisionMode `yaml:\"mode\" json:\"mode\"` // default: \"system\"\n\tScript         string        `yaml:\"script\" json:\"script\"`\n\tSkipResolution bool          `yaml:\"skipDefaultDependencyResolution,omitempty\" json:\"skipDefaultDependencyResolution,omitempty\"`\n}\n\ntype NineP struct {\n\tSecurityModel   string `yaml:\"securityModel,omitempty\" json:\"securityModel,omitempty\"`\n\tProtocolVersion string `yaml:\"protocolVersion,omitempty\" json:\"protocolVersion,omitempty\"`\n\tMsize           string `yaml:\"msize,omitempty\" json:\"msize,omitempty\"`\n\tCache           string `yaml:\"cache,omitempty\" json:\"cache,omitempty\"`\n}\n\ntype VMOpts struct {\n\tQEMU   QEMUOpts `yaml:\"qemu,omitempty\" json:\"qemu,omitempty\"`\n\tVZOpts VZOpts   `yaml:\"vz,omitempty\" json:\"vz,omitempty\"`\n}\n\ntype QEMUOpts struct {\n\tMinimumVersion *string         `yaml:\"minimumVersion,omitempty\" json:\"minimumVersion,omitempty\"`\n\tCPUType        map[Arch]string `yaml:\"cpuType,omitempty\" json:\"cpuType,omitempty\"`\n}\n\ntype VZOpts struct {\n\tRosetta Rosetta `yaml:\"rosetta,omitempty\" json:\"rosetta,omitempty\"`\n}\n\ntype Rosetta struct {\n\tEnabled bool `yaml:\"enabled\" json:\"enabled\"`\n\tBinFmt  bool `yaml:\"binfmt\" json:\"binfmt\"`\n}\n"
  },
  {
    "path": "environment/vm/lima/limautil/disk.go",
    "content": "package limautil\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/store\"\n)\n\n// HasDisk checks if a lima disk exists for the current instance.\nfunc HasDisk() bool {\n\tname := config.CurrentProfile().ID\n\n\tvar resp struct {\n\t\tName string `json:\"name\"`\n\t}\n\n\tcmd := Limactl(\"disk\", \"list\", \"--json\", name)\n\tvar buf bytes.Buffer\n\tcmd.Stdout = &buf\n\tcmd.Stderr = nil\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn false\n\t}\n\n\tif err := json.NewDecoder(&buf).Decode(&resp); err != nil {\n\t\treturn false\n\t}\n\n\treturn resp.Name == name\n}\n\n// CreateDisk creates a lima disk with size in GiB.\nfunc CreateDisk(size int) error {\n\tname := config.CurrentProfile().ID\n\n\tvar buf bytes.Buffer\n\tcmd := Limactl(\"disk\", \"create\", name, \"--size\", fmt.Sprintf(\"%dGiB\", size))\n\tcmd.Stderr = &buf\n\tcmd.Stdout = &buf\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"error creating lima disk: %w, output: %s\", err, buf.String())\n\t}\n\n\treturn nil\n}\n\n// ResizeDisk resizes disk to new size\nfunc ResizeDisk(size int) error {\n\tname := config.CurrentProfile().ID\n\n\tvar buf bytes.Buffer\n\tcmd := Limactl(\"disk\", \"resize\", name, \"--size\", fmt.Sprintf(\"%dGiB\", size))\n\tcmd.Stderr = &buf\n\tcmd.Stdout = &buf\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"error resizing disk: %w, output: %s\", err, buf.String())\n\t}\n\n\treturn nil\n}\n\n// DeleteDisk deletes lima disk for the current instance.\nfunc DeleteDisk() error {\n\tname := config.CurrentProfile().ID\n\n\tvar buf bytes.Buffer\n\tcmd := Limactl(\"disk\", \"delete\", name)\n\tcmd.Stderr = &buf\n\tcmd.Stdout = &buf\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"error deleting lima disk: %w, output: %s\", err, buf.String())\n\t}\n\n\treturn nil\n}\n\n// MountPoint returns the lima disk mount point for the current instance.\nfunc MountPoint() string { return fmt.Sprintf(\"/mnt/lima-%s\", config.CurrentProfile().ID) }\n\n// DiskPrivisioned returns if the disk exists and has been provisioned for the specified runtime.\nfunc DiskProvisioned(runtime string) bool {\n\tif !HasDisk() {\n\t\treturn false\n\t}\n\n\ts, _ := store.Load()\n\treturn s.DiskFormatted && s.DiskRuntime == runtime\n}\n"
  },
  {
    "path": "environment/vm/lima/limautil/files.go",
    "content": "package limautil\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/abiosoft/colima/config\"\n)\n\nconst colimaDiffDiskFile = \"diffdisk\"\n\n// ColimaDiffDisk returns path to the diffdisk for the colima VM.\nfunc ColimaDiffDisk(profileID string) string {\n\treturn filepath.Join(config.ProfileFromName(profileID).LimaInstanceDir(), colimaDiffDiskFile)\n}\n\nconst networkFile = \"networks.yaml\"\n\n// NetworkFile returns path to the network file.\nfunc NetworkFile() string {\n\treturn filepath.Join(config.LimaDir(), \"_config\", networkFile)\n}\n\n// NetworkAssetsDirecotry returns the directory for the generated network assets.\nfunc NetworkAssetsDirectory() string {\n\treturn filepath.Join(config.LimaDir(), \"_networks\")\n}\n"
  },
  {
    "path": "environment/vm/lima/limautil/image.go",
    "content": "package limautil\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/embedded\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/environment/host\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limaconfig\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/downloader\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc init() {\n\tif err := loadImages(); err != nil {\n\t\tlogrus.Fatal(err)\n\t}\n}\n\n// ImageCached returns if the image for architecture and runtime\n// has been previously downloaded and cached.\nfunc ImageCached(arch environment.Arch, runtime string) (limaconfig.File, bool) {\n\timg, err := findImage(arch, runtime)\n\tif err != nil {\n\t\treturn img, false\n\t}\n\n\timage := diskImageFile(downloader.CacheFilename(img.Location))\n\n\timg.Location = image.Location()\n\timg.Digest = \"\"\n\n\treturn img, image.Generated()\n}\n\nfunc findImage(arch environment.Arch, runtime string) (f limaconfig.File, err error) {\n\terr = fmt.Errorf(\"cannot find %s image for %s runtime\", arch, runtime)\n\n\timgFile, ok := diskImageMap[runtime]\n\tif !ok {\n\t\treturn\n\t}\n\timg, ok := imgFile[arch.GoArch()]\n\tif !ok {\n\t\treturn\n\t}\n\treturn img, nil\n}\n\n// Image returns the details of the disk image to download for the arch and runtime.\nfunc Image(arch environment.Arch, runtime string) (limaconfig.File, error) {\n\treturn findImage(arch, runtime)\n}\n\n// DownloadImage downloads the image for arch and runtime.\nfunc DownloadImage(arch environment.Arch, runtime string) (f limaconfig.File, err error) {\n\timg, err := findImage(arch, runtime)\n\tif err != nil {\n\t\treturn img, err\n\t}\n\n\thost := host.New()\n\t// download image\n\tqcow2, err := downloadImage(host, img)\n\tif err != nil {\n\t\treturn f, err\n\t}\n\n\tdiskImage := diskImageFile(qcow2)\n\n\t// if qemu-img is missing, ignore raw conversion\n\tif err := util.AssertQemuImg(); err != nil {\n\t\timg.Location = diskImage.String()\n\t\timg.Digest = \"\" // remove digest\n\t\treturn img, nil\n\t}\n\n\t// convert from qcow2 to raw\n\traw, err := qcow2ToRaw(host, diskImage)\n\tif err != nil {\n\t\treturn f, err\n\t}\n\n\timg.Location = raw\n\timg.Digest = \"\" // remove digest\n\treturn img, nil\n}\n\n// map of runtime to disk images.\nvar diskImageMap = map[string]diskImages{}\n\n// map of architecture to disk image\ntype diskImages map[string]limaconfig.File\n\nfunc loadImages() error {\n\tfilename := \"images/images.txt\"\n\tb, err := embedded.Read(filename)\n\tif err != nil {\n\t\tlogrus.Fatalf(\"error reading embedded file: %s\", filename)\n\t}\n\treturn loadImagesFromBytes(b)\n}\n\nfunc loadImagesFromBytes(b []byte) error {\n\tscanner := bufio.NewScanner(bytes.NewReader(b))\n\tfor scanner.Scan() {\n\t\tline := scanner.Bytes()\n\t\tvar arch environment.Arch\n\t\tvar runtime, url, sha string\n\t\t_, err := fmt.Fscan(bytes.NewReader(line), &arch, &runtime, &url, &sha)\n\t\tif err != nil && err != io.EOF {\n\t\t\treturn err\n\t\t}\n\n\t\t// sanitise the value\n\t\tarch = arch.Value()\n\n\t\tfile := limaconfig.File{Location: url, Arch: arch}\n\t\tif sha != \"\" {\n\t\t\tfile.Digest = \"sha512:\" + sha\n\t\t}\n\n\t\tvar files = diskImages{}\n\t\tif m, ok := diskImageMap[runtime]; ok {\n\t\t\tfiles = m\n\t\t}\n\t\tfiles[arch.GoArch()] = file\n\n\t\tdiskImageMap[runtime] = files\n\t}\n\n\treturn nil\n}\n\n// downloadImage downloads the file and returns the location of the downloaded file.\nfunc downloadImage(host environment.HostActions, file limaconfig.File) (string, error) {\n\t// download image\n\trequest := downloader.Request{URL: file.Location}\n\tif file.Digest != \"\" {\n\t\trequest.SHA = &downloader.SHA{Size: 512, Digest: file.Digest}\n\t}\n\tlocation, err := downloader.Download(host, request)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error during image download: %w\", err)\n\t}\n\n\treturn location, nil\n}\n\n// qcow2ToRaw uses qemu-img to conver the image from qcow to raw.\n// Returns the filename of the raw file and an error (if any).\nfunc qcow2ToRaw(host environment.Host, image diskImageFile) (string, error) {\n\tif _, err := os.Stat(image.Raw()); err == nil {\n\t\t// already exists, return\n\t\treturn image.Raw(), nil\n\t}\n\n\terr := host.Run(\"qemu-img\", \"convert\", \"-f\", \"qcow2\", \"-O\", \"raw\", image.String(), image.Raw())\n\tif err != nil {\n\t\t// remove the incomplete raw file\n\t\t_ = host.RunQuiet(\"rm\", \"-f\", image.Raw())\n\t\treturn \"\", err\n\t}\n\n\treturn image.Raw(), err\n}\n\ntype diskImageFile string\n\nfunc (d diskImageFile) String() string { return strings.TrimSuffix(string(d), \".raw\") }\nfunc (d diskImageFile) Raw() string    { return d.String() + \".raw\" }\nfunc (d diskImageFile) Generated() bool {\n\tstat, err := os.Stat(d.Location())\n\treturn err == nil && !stat.IsDir()\n}\n\n// Location returns the expected location of the image based on availability of qemu.\nfunc (d diskImageFile) Location() string {\n\tif err := util.AssertQemuImg(); err == nil {\n\t\treturn d.Raw()\n\t}\n\treturn d.String()\n}\n"
  },
  {
    "path": "environment/vm/lima/limautil/instance.go",
    "content": "package limautil\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/config/configmanager\"\n)\n\n// Instance returns current instance.\nfunc Instance() (InstanceInfo, error) {\n\treturn getInstance(config.CurrentProfile().ID)\n}\n\n// InstanceInfo is the information about a Lima instance\ntype InstanceInfo struct {\n\tName    string `json:\"name,omitempty\"`\n\tStatus  string `json:\"status,omitempty\"`\n\tArch    string `json:\"arch,omitempty\"`\n\tCPU     int    `json:\"cpus,omitempty\"`\n\tMemory  int64  `json:\"memory,omitempty\"`\n\tDisk    int64  `json:\"disk,omitempty\"`\n\tDir     string `json:\"dir,omitempty\"`\n\tNetwork []struct {\n\t\tVNL       string `json:\"vnl,omitempty\"`\n\t\tInterface string `json:\"interface,omitempty\"`\n\t} `json:\"network,omitempty\"`\n\tIPAddress string `json:\"address,omitempty\"`\n\tRuntime   string `json:\"runtime,omitempty\"`\n}\n\n// Running checks if the instance is running.\nfunc (i InstanceInfo) Running() bool { return i.Status == limaStatusRunning }\n\n// Config returns the current Colima config\nfunc (i InstanceInfo) Config() (config.Config, error) {\n\treturn configmanager.LoadFrom(config.ProfileFromName(i.Name).StateFile())\n}\n\n// Lima statuses\nconst (\n\tlimaStatusRunning = \"Running\"\n)\n\nfunc getInstance(profileID string) (InstanceInfo, error) {\n\tvar i InstanceInfo\n\tvar buf bytes.Buffer\n\tcmd := Limactl(\"list\", profileID, \"--json\")\n\tcmd.Stderr = nil\n\tcmd.Stdout = &buf\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn i, fmt.Errorf(\"error retrieving instance: %w\", err)\n\t}\n\n\tif buf.Len() == 0 {\n\t\treturn i, fmt.Errorf(\"instance '%s' does not exist\", config.ProfileFromName(profileID).DisplayName)\n\t}\n\n\tif err := json.Unmarshal(buf.Bytes(), &i); err != nil {\n\t\treturn i, fmt.Errorf(\"error retrieving instance: %w\", err)\n\t}\n\n\tif conf, err := i.Config(); err == nil {\n\t\tif conf.Disk > 0 {\n\t\t\ti.Disk = config.Disk(conf.Disk).Int()\n\t\t}\n\t}\n\treturn i, nil\n}\n\n// Instances returns Lima instances created by colima.\nfunc Instances(ids ...string) ([]InstanceInfo, error) {\n\tlimaIDs := make([]string, len(ids))\n\tfor i := range ids {\n\t\tlimaIDs[i] = config.ProfileFromName(ids[i]).ID\n\t}\n\targs := append([]string{\"list\", \"--json\"}, limaIDs...)\n\n\tvar buf bytes.Buffer\n\tcmd := Limactl(args...)\n\tcmd.Stderr = nil\n\tcmd.Stdout = &buf\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, fmt.Errorf(\"error retrieving instances: %w\", err)\n\t}\n\n\tvar instances []InstanceInfo\n\tscanner := bufio.NewScanner(&buf)\n\tfor scanner.Scan() {\n\t\tvar i InstanceInfo\n\t\tline := scanner.Bytes()\n\t\tif err := json.Unmarshal(line, &i); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error retrieving instances: %w\", err)\n\t\t}\n\n\t\t// limit to colima instances\n\t\tif !strings.HasPrefix(i.Name, \"colima\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tconf, _ := i.Config()\n\t\tif i.Running() {\n\t\t\tfor _, n := range i.Network {\n\t\t\t\tif n.Interface == NetInterface {\n\t\t\t\t\ti.IPAddress = getIPAddress(i.Name, NetInterface)\n\t\t\t\t}\n\t\t\t}\n\t\t\ti.Runtime = getRuntime(conf)\n\t\t}\n\n\t\t// rename to local friendly names\n\t\ti.Name = config.ProfileFromName(i.Name).ShortName\n\n\t\t// network is low level, remove\n\t\ti.Network = nil\n\n\t\t// report correct disk usage\n\t\tif conf.Disk > 0 {\n\t\t\ti.Disk = config.Disk(conf.Disk).Int()\n\t\t}\n\n\t\tinstances = append(instances, i)\n\t}\n\n\treturn instances, nil\n}\n\n// RunningInstances return Lima instances that are has a running status.\nfunc RunningInstances() ([]InstanceInfo, error) {\n\tallInstances, err := Instances()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar runningInstances []InstanceInfo\n\tfor _, instance := range allInstances {\n\t\tif instance.Running() {\n\t\t\trunningInstances = append(runningInstances, instance)\n\t\t}\n\t}\n\n\treturn runningInstances, nil\n}\n\nfunc getRuntime(conf config.Config) string {\n\tvar runtime string\n\n\tswitch conf.Runtime {\n\tcase \"docker\", \"containerd\", \"incus\":\n\t\truntime = conf.Runtime\n\tcase \"none\":\n\t\treturn \"none\"\n\tdefault:\n\t\treturn \"\"\n\t}\n\n\tif conf.Kubernetes.Enabled {\n\t\truntime += \"+k3s\"\n\t}\n\treturn runtime\n}\n"
  },
  {
    "path": "environment/vm/lima/limautil/limautil.go",
    "content": "package limautil\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/config\"\n)\n\n// EnvLimaHome is the environment variable for the Lima directory.\nconst EnvLimaHome = \"LIMA_HOME\"\n\n// EnvLimaDrivers is the environment variable for the path to external Lima drivers.\nconst EnvLimaDrivers = \"LIMA_DRIVERS_PATH\"\n\n// LimactlCommand is the limactl command.\nconst LimactlCommand = \"limactl\"\n\n// Limactl prepares a limactl command.\nfunc Limactl(args ...string) *exec.Cmd {\n\tcmd := cli.Command(LimactlCommand, args...)\n\tcmd.Env = append(cmd.Env, os.Environ()...)\n\tcmd.Env = append(cmd.Env, EnvLimaHome+\"=\"+config.LimaDir())\n\treturn cmd\n}\n"
  },
  {
    "path": "environment/vm/lima/limautil/network.go",
    "content": "package limautil\n\nimport (\n\t\"bytes\"\n\t\"net\"\n\t\"strings\"\n)\n\n// network interface for shared network in the virtual machine.\nconst NetInterface = \"col0\"\n\n// network metric for the route\nconst NetMetric uint32 = 300\nconst NetMetricPreferred uint32 = 100\n\n// IPAddress returns the ip address for profile.\n// It returns the PTP address if networking is enabled or falls back to 127.0.0.1.\n// It is guaranteed to return a value.\n//\n// TODO: unnecessary round-trip is done to get instance details from Lima.\nfunc IPAddress(profileID string) string {\n\tconst fallback = \"127.0.0.1\"\n\tinstance, err := getInstance(profileID)\n\tif err != nil {\n\t\treturn fallback\n\t}\n\n\tif len(instance.Network) > 0 {\n\t\tfor _, n := range instance.Network {\n\t\t\tif n.Interface == NetInterface {\n\t\t\t\treturn getIPAddress(profileID, n.Interface)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fallback\n}\n\n// InternalIPAddress returns the internal IP address for the profile.\nfunc InternalIPAddress(profileID string) string {\n\treturn getIPAddress(profileID, \"eth0\")\n}\n\nfunc getIPAddress(profileID, interfaceName string) string {\n\tvar buf bytes.Buffer\n\t// TODO: this should be less hacky\n\tcmd := Limactl(\"shell\", profileID, \"sh\", \"-c\",\n\t\t`ip -4 addr show `+interfaceName+` | grep inet | awk -F' ' '{print $2 }' | cut -d/ -f1`)\n\tcmd.Stderr = nil\n\tcmd.Stdout = &buf\n\n\t_ = cmd.Run()\n\treturn strings.TrimSpace(buf.String())\n}\n\ntype LimaNetworkConfig struct {\n\tMode    string `yaml:\"mode\"`\n\tGateway net.IP `yaml:\"gateway\"`\n\tNetmask string `yaml:\"netmask\"`\n}\n\ntype LimaNetwork struct {\n\tNetworks struct {\n\t\tUserV2 LimaNetworkConfig `yaml:\"user-v2\"`\n\t} `yaml:\"networks\"`\n}\n"
  },
  {
    "path": "environment/vm/lima/limautil/ssh.go",
    "content": "package limautil\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/config\"\n)\n\n// ShowSSH runs the show-ssh command in Lima.\n// returns the ssh output, if in layer, and an error if any\nfunc ShowSSH(profileID string) (resp struct {\n\tOutput string\n\tFile   struct {\n\t\tLima   string\n\t\tColima string\n\t}\n}, err error) {\n\tssh := sshConfig(profileID)\n\tsshConf, err := ssh.Contents()\n\tif err != nil {\n\t\treturn resp, fmt.Errorf(\"error retrieving ssh config: %w\", err)\n\t}\n\n\tresp.Output = replaceSSHConfig(sshConf, profileID)\n\tresp.File.Lima = ssh.File()\n\tresp.File.Colima = config.SSHConfigFile()\n\treturn resp, nil\n}\n\nfunc replaceSSHConfig(conf string, profileID string) string {\n\tprofileID = config.ProfileFromName(profileID).ID\n\n\tvar out bytes.Buffer\n\tscanner := bufio.NewScanner(strings.NewReader(conf))\n\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif strings.HasPrefix(line, \"Host \") {\n\t\t\tline = \"Host \" + profileID\n\t\t}\n\n\t\t_, _ = fmt.Fprintln(&out, line)\n\t}\n\treturn out.String()\n}\n\nconst sshConfigFile = \"ssh.config\"\n\n// sshConfig is the ssh configuration file for a Colima profile.\ntype sshConfig string\n\n// Contents returns the content of the SSH config file.\nfunc (s sshConfig) Contents() (string, error) {\n\tprofile := config.ProfileFromName(string(s))\n\tb, err := os.ReadFile(s.File())\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error retrieving Lima SSH config file for profile '%s': %w\", strings.TrimPrefix(profile.DisplayName, \"lima\"), err)\n\t}\n\treturn string(b), nil\n}\n\n// File returns the path to the SSH config file.\nfunc (s sshConfig) File() string {\n\tprofile := config.ProfileFromName(string(s))\n\treturn filepath.Join(profile.LimaInstanceDir(), sshConfigFile)\n}\n"
  },
  {
    "path": "environment/vm/lima/network.go",
    "content": "package lima\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/config/configmanager\"\n\t\"github.com/abiosoft/colima/environment/container/incus\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nvar defaultLimaNetworkConfig = limautil.LimaNetwork{\n\tNetworks: struct {\n\t\tUserV2 limautil.LimaNetworkConfig `yaml:\"user-v2\"`\n\t}{\n\t\tUserV2: limautil.LimaNetworkConfig{\n\t\t\tMode:    \"user-v2\",\n\t\t\tGateway: net.ParseIP(\"192.168.5.2\"),\n\t\t\tNetmask: \"255.255.255.0\",\n\t\t},\n\t},\n}\n\nfunc (l *limaVM) writeNetworkFile(conf config.Config) error {\n\tnetworkFile := limautil.NetworkFile()\n\n\t// use custom gateway address\n\tgatewayAddress := conf.Network.GatewayAddress\n\tif gatewayAddress != nil {\n\t\tdefaultLimaNetworkConfig.Networks.UserV2.Gateway = gatewayAddress\n\t}\n\n\t// if there are no running instances, clear network directory\n\tif instances, err := limautil.RunningInstances(); err == nil && len(instances) == 0 {\n\t\tif err := os.RemoveAll(limautil.NetworkAssetsDirectory()); err != nil {\n\t\t\tlogrus.Warnln(fmt.Errorf(\"could not clear network assets directory: %w\", err))\n\t\t}\n\t}\n\n\tif err := os.MkdirAll(filepath.Dir(networkFile), 0755); err != nil {\n\t\treturn fmt.Errorf(\"error creating Lima config directory: %w\", err)\n\t}\n\n\tnetworkFileMarshalled, err := yaml.Marshal(&defaultLimaNetworkConfig)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshalling Lima network config file: %w\", err)\n\t}\n\n\tif err := os.WriteFile(networkFile, networkFileMarshalled, 0755); err != nil {\n\t\treturn fmt.Errorf(\"error writing Lima network config file: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (l *limaVM) replicateHostAddresses(conf config.Config) error {\n\tif !conf.Network.Address && conf.Network.HostAddresses {\n\t\tfor _, ip := range util.HostIPAddresses() {\n\t\t\tif err := l.RunQuiet(\"sudo\", \"ip\", \"address\", \"add\", ip.String()+\"/24\", \"dev\", \"lo\"); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (l *limaVM) removeHostAddresses() {\n\tconf, _ := configmanager.LoadInstance()\n\tif !conf.Network.Address && conf.Network.HostAddresses {\n\t\tfor _, ip := range util.HostIPAddresses() {\n\t\t\t_ = l.RunQuiet(\"sudo\", \"ip\", \"address\", \"del\", ip.String()+\"/24\", \"dev\", \"lo\")\n\t\t}\n\t}\n}\n\n// removeIncusContainerRoute is a safety net for force-stop,\n// where the Incus container Stop() is skipped.\nfunc (l *limaVM) removeIncusContainerRoute() {\n\tif !util.MacOS() {\n\t\treturn\n\t}\n\n\tif l.conf.Runtime != incus.Name {\n\t\treturn\n\t}\n\n\tif !util.RouteExists(incus.BridgeSubnet) {\n\t\treturn\n\t}\n\n\t_ = l.host.RunQuiet(\"sudo\", \"/sbin/route\", \"delete\", \"-net\", incus.BridgeSubnet)\n}\n"
  },
  {
    "path": "environment/vm/lima/shell.go",
    "content": "package lima\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\t\"github.com/abiosoft/colima/config\"\n)\n\nfunc (l limaVM) Run(args ...string) error {\n\targs = append([]string{lima}, args...)\n\n\ta := l.Init(context.Background())\n\n\ta.Add(func() error {\n\t\treturn l.host.Run(args...)\n\t})\n\n\treturn a.Exec()\n}\n\nfunc (l limaVM) SSH(workingDir string, args ...string) error {\n\tif workingDir == \"\" {\n\t\targs = append([]string{limactl, \"shell\", config.CurrentProfile().ID}, args...)\n\t} else {\n\t\targs = append([]string{limactl, \"shell\", \"--workdir\", workingDir, config.CurrentProfile().ID}, args...)\n\t}\n\n\ta := l.Init(context.Background())\n\n\ta.Add(func() error {\n\t\treturn l.host.RunInteractive(args...)\n\t})\n\n\treturn a.Exec()\n}\n\nfunc (l limaVM) RunInteractive(args ...string) error {\n\targs = append([]string{lima}, args...)\n\n\ta := l.Init(context.Background())\n\n\ta.Add(func() error {\n\t\treturn l.host.RunInteractive(args...)\n\t})\n\n\treturn a.Exec()\n}\n\nfunc (l limaVM) RunWith(stdin io.Reader, stdout io.Writer, args ...string) error {\n\targs = append([]string{lima}, args...)\n\n\ta := l.Init(context.Background())\n\n\ta.Add(func() error {\n\t\treturn l.host.RunWith(stdin, stdout, args...)\n\t})\n\n\treturn a.Exec()\n}\n\nfunc (l limaVM) RunOutput(args ...string) (out string, err error) {\n\targs = append([]string{lima}, args...)\n\n\ta := l.Init(context.Background())\n\n\ta.Add(func() (err error) {\n\t\tout, err = l.host.RunOutput(args...)\n\t\treturn\n\t})\n\n\terr = a.Exec()\n\treturn\n}\n\nfunc (l limaVM) RunQuiet(args ...string) (err error) {\n\targs = append([]string{lima}, args...)\n\n\ta := l.Init(context.Background())\n\n\ta.Add(func() (err error) {\n\t\treturn l.host.RunQuiet(args...)\n\t})\n\n\terr = a.Exec()\n\treturn\n}\n"
  },
  {
    "path": "environment/vm/lima/yaml.go",
    "content": "package lima\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/daemon\"\n\t\"github.com/abiosoft/colima/daemon/process/vmnet\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/environment/container/containerd\"\n\t\"github.com/abiosoft/colima/environment/container/docker\"\n\t\"github.com/abiosoft/colima/environment/container/incus\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limaconfig\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limautil\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc newConf(ctx context.Context, conf config.Config) (l limaconfig.Config, err error) {\n\tl.Arch = environment.Arch(conf.Arch).Value()\n\n\t// VM type is qemu except in few scenarios\n\tl.VMType = limaconfig.QEMU\n\n\tsameArchitecture := environment.HostArch() == l.Arch\n\n\t// when vz is chosen and OS version supports it\n\tif util.MacOS13OrNewer() && conf.VMType == limaconfig.VZ && sameArchitecture {\n\t\tl.VMType = limaconfig.VZ\n\n\t\t// Rosetta is only available on Apple Silicon\n\t\tif conf.VZRosetta && util.MacOS13OrNewerOnArm() {\n\t\t\tif util.RosettaRunning() {\n\t\t\t\tl.VMOpts.VZOpts.Rosetta.Enabled = true\n\t\t\t\tl.VMOpts.VZOpts.Rosetta.BinFmt = true\n\t\t\t} else {\n\t\t\t\tlogrus.Warnln(\"Unable to enable Rosetta: Rosetta2 is not installed\")\n\t\t\t\tlogrus.Warnln(\"Run 'softwareupdate --install-rosetta' to install Rosetta2\")\n\t\t\t}\n\t\t}\n\n\t\tif util.MacOSNestedVirtualizationSupported() {\n\t\t\tl.NestedVirtualization = conf.NestedVirtualization\n\t\t}\n\t}\n\n\t// when krunkit is chosen and OS version supports it\n\tif util.MacOS13OrNewerOnArm() && conf.VMType == limaconfig.Krunkit && sameArchitecture {\n\t\tl.VMType = limaconfig.Krunkit\n\n\t\tif util.MacOSNestedVirtualizationSupported() {\n\t\t\tl.NestedVirtualization = conf.NestedVirtualization\n\t\t}\n\t}\n\n\tif conf.CPUType != \"\" && conf.CPUType != \"host\" {\n\t\tl.VMOpts.QEMU.CPUType = map[environment.Arch]string{\n\t\t\tl.Arch: conf.CPUType,\n\t\t}\n\t}\n\n\tif conf.CPU > 0 {\n\t\tl.CPUs = &conf.CPU\n\t}\n\tif conf.Memory > 0 {\n\t\tl.Memory = fmt.Sprintf(\"%dMiB\", uint32(conf.Memory*1024))\n\t}\n\tif conf.RootDisk > 0 {\n\t\tl.Disk = fmt.Sprintf(\"%dGiB\", conf.RootDisk)\n\t}\n\tl.SSH = limaconfig.SSH{LocalPort: conf.SSHPort, LoadDotSSHPubKeys: false, ForwardAgent: conf.ForwardAgent}\n\tl.Containerd = limaconfig.Containerd{System: false, User: false}\n\n\tl.DNS = conf.Network.DNSResolvers\n\tl.HostResolver.Enabled = len(conf.Network.DNSResolvers) == 0\n\tl.HostResolver.Hosts = conf.Network.DNSHosts\n\tif l.HostResolver.Hosts == nil {\n\t\tl.HostResolver.Hosts = make(map[string]string)\n\t}\n\n\tif _, ok := l.HostResolver.Hosts[\"host.docker.internal\"]; !ok {\n\t\tl.HostResolver.Hosts[\"host.docker.internal\"] = \"host.lima.internal\"\n\t}\n\n\tl.Env = conf.Env\n\tif l.Env == nil {\n\t\tl.Env = make(map[string]string)\n\t}\n\n\t// extra required provision commands\n\t{\n\t\t// fix inotify\n\t\tl.Provision = append(l.Provision, limaconfig.Provision{\n\t\t\tMode:   limaconfig.ProvisionModeSystem,\n\t\t\tScript: \"sysctl -w fs.inotify.max_user_watches=1048576\",\n\t\t})\n\n\t\t// add user to docker group\n\t\t// \"sudo\", \"usermod\", \"-aG\", \"docker\", user\n\t\tif conf.Runtime == docker.Name {\n\t\t\tl.Provision = append(l.Provision, limaconfig.Provision{\n\t\t\t\tMode:   limaconfig.ProvisionModeDependency,\n\t\t\t\tScript: \"groupadd -f docker && usermod -aG docker {{ .User }}\",\n\t\t\t})\n\t\t}\n\n\t\t// add user to incus-admin group\n\t\t// \"sudo\", \"usermod\", \"-aG\", \"incus-admin\", user\n\t\tif conf.Runtime == incus.Name {\n\t\t\tl.Provision = append(l.Provision, limaconfig.Provision{\n\t\t\t\tMode:   limaconfig.ProvisionModeDependency,\n\t\t\t\tScript: \"groupadd -f incus-admin && usermod -aG incus-admin {{ .User }}\",\n\t\t\t})\n\t\t}\n\n\t\t// set hostname\n\t\thostname := config.CurrentProfile().ID\n\t\tif conf.Hostname != \"\" {\n\t\t\thostname = conf.Hostname\n\t\t}\n\t\tl.Provision = append(l.Provision, limaconfig.Provision{\n\t\t\tMode:   limaconfig.ProvisionModeSystem,\n\t\t\tScript: \"grep '127.0.0.1 \" + hostname + \"' /etc/hosts || echo '127.0.0.1 \" + hostname + \"' >> /etc/hosts\",\n\t\t})\n\t\tl.Provision = append(l.Provision, limaconfig.Provision{\n\t\t\tMode:   limaconfig.ProvisionModeSystem,\n\t\t\tScript: \"hostnamectl set-hostname \" + hostname,\n\t\t})\n\n\t}\n\n\t// network setup\n\t{\n\t\tl.Networks = append(l.Networks, limaconfig.Network{\n\t\t\tLima: \"user-v2\",\n\t\t})\n\n\t\treachableIPAddress := true\n\t\tif conf.Network.Address {\n\t\t\tmetric := limautil.NetMetric\n\t\t\tif conf.Network.PreferredRoute {\n\t\t\t\tmetric = limautil.NetMetricPreferred\n\t\t\t}\n\t\t\t// vmnet is used for bridged mode, otherwise VZ uses VZNAT\n\t\t\tif l.VMType == limaconfig.VZ && conf.Network.Mode != \"bridged\" {\n\t\t\t\tl.Networks = append(l.Networks, limaconfig.Network{\n\t\t\t\t\tVZNAT:     true,\n\t\t\t\t\tInterface: limautil.NetInterface,\n\t\t\t\t\tMetric:    metric,\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\treachableIPAddress, _ = ctx.Value(daemon.CtxKey(vmnet.Name)).(bool)\n\n\t\t\t\t// network is currently limited to macOS.\n\t\t\t\tif util.MacOS() && reachableIPAddress {\n\t\t\t\t\tif err := func() error {\n\t\t\t\t\t\tsocketFile := vmnet.Info().Socket.File()\n\t\t\t\t\t\t// ensure the socket file exists\n\t\t\t\t\t\tif _, err := os.Stat(socketFile); err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"vmnet socket file not found: %w\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tl.Networks = append(l.Networks, limaconfig.Network{\n\t\t\t\t\t\t\tSocket:    socketFile,\n\t\t\t\t\t\t\tInterface: limautil.NetInterface,\n\t\t\t\t\t\t\tMetric:    metric,\n\t\t\t\t\t\t})\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}(); err != nil {\n\t\t\t\t\t\treachableIPAddress = false\n\t\t\t\t\t\tlogrus.Warn(fmt.Errorf(\"error setting up reachable IP address: %w\", err))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// disable ports 80 and 443 when k8s is enabled and there is a reachable IP address\n\t\t\t// to prevent ingress (traefik) from occupying relevant host ports.\n\t\t\tif reachableIPAddress && conf.Kubernetes.Enabled && !ingressDisabled(conf.Kubernetes.K3sArgs) {\n\t\t\t\tl.PortForwards = append(l.PortForwards,\n\t\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\t\tGuestIP:           net.IPv4zero,\n\t\t\t\t\t\tGuestPort:         80,\n\t\t\t\t\t\tGuestIPMustBeZero: true,\n\t\t\t\t\t\tIgnore:            true,\n\t\t\t\t\t\tProto:             limaconfig.TCP,\n\t\t\t\t\t},\n\t\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\t\tGuestIP:           net.IPv4zero,\n\t\t\t\t\t\tGuestPort:         443,\n\t\t\t\t\t\tGuestIPMustBeZero: true,\n\t\t\t\t\t\tIgnore:            true,\n\t\t\t\t\t\tProto:             limaconfig.TCP,\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t}\n\n\t\t\t// disable port forwarding for Incus when there is a reachable IP address for consistent behaviour\n\t\t\tif reachableIPAddress && conf.Runtime == incus.Name {\n\t\t\t\tl.PortForwards = append(l.PortForwards,\n\t\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\t\tGuestIP:           net.IPv4zero,\n\t\t\t\t\t\tGuestIPMustBeZero: true,\n\t\t\t\t\t\tGuestPortRange:    [2]int{1, 65535},\n\t\t\t\t\t\tHostPortRange:     [2]int{1, 65535},\n\t\t\t\t\t\tIgnore:            true,\n\t\t\t\t\t\tProto:             limaconfig.TCP,\n\t\t\t\t\t},\n\t\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\t\tGuestIP:        net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\t\tGuestPortRange: [2]int{1, 65535},\n\t\t\t\t\t\tHostPortRange:  [2]int{1, 65535},\n\t\t\t\t\t\tIgnore:         true,\n\t\t\t\t\t\tProto:          limaconfig.TCP,\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\t// ports and sockets\n\t{\n\t\t// docker socket\n\t\tif conf.Runtime == docker.Name {\n\t\t\tl.PortForwards = append(l.PortForwards,\n\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\tGuestSocket: \"/var/run/docker.sock\",\n\t\t\t\t\tHostSocket:  docker.HostSocketFile(),\n\t\t\t\t\tProto:       limaconfig.TCP,\n\t\t\t\t},\n\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\tGuestSocket: \"/var/run/containerd/containerd.sock\",\n\t\t\t\t\tHostSocket:  containerd.HostSocketFiles().Containerd,\n\t\t\t\t\tProto:       limaconfig.TCP,\n\t\t\t\t})\n\n\t\t\tif config.CurrentProfile().ShortName == \"default\" {\n\t\t\t\t// for backward compatibility, will be removed in future releases\n\t\t\t\tl.PortForwards = append(l.PortForwards,\n\t\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\t\tGuestSocket: \"/var/run/docker.sock\",\n\t\t\t\t\t\tHostSocket:  docker.LegacyDefaultHostSocketFile(),\n\t\t\t\t\t\tProto:       limaconfig.TCP,\n\t\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\t// containerd socket\n\t\tif conf.Runtime == containerd.Name {\n\t\t\tl.PortForwards = append(l.PortForwards,\n\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\tGuestSocket: \"/var/run/containerd/containerd.sock\",\n\t\t\t\t\tHostSocket:  containerd.HostSocketFiles().Containerd,\n\t\t\t\t\tProto:       limaconfig.TCP,\n\t\t\t\t},\n\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\tGuestSocket: \"/var/run/buildkit/buildkitd.sock\",\n\t\t\t\t\tHostSocket:  containerd.HostSocketFiles().Buildkitd,\n\t\t\t\t\tProto:       limaconfig.TCP,\n\t\t\t\t})\n\t\t}\n\n\t\t// incus socket\n\t\tif conf.Runtime == incus.Name {\n\t\t\tl.PortForwards = append(l.PortForwards,\n\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\tGuestSocket: \"/var/lib/incus/unix.socket\",\n\t\t\t\t\tHostSocket:  incus.HostSocketFile(),\n\t\t\t\t\tProto:       limaconfig.TCP,\n\t\t\t\t})\n\t\t}\n\n\t\tif conf.PortForwarder == \"none\" {\n\t\t\t// disable port forwarding\n\t\t\tl.PortForwards = append(l.PortForwards,\n\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\tGuestIP: net.IPv4zero,\n\t\t\t\t\tProto:   \"any\",\n\t\t\t\t\tIgnore:  true,\n\t\t\t\t})\n\t\t} else {\n\t\t\t// handle port forwarding to allow listening on 0.0.0.0\n\t\t\t// bind 0.0.0.0\n\t\t\tl.PortForwards = append(l.PortForwards,\n\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\tGuestIPMustBeZero: true,\n\t\t\t\t\tGuestIP:           net.IPv4zero,\n\t\t\t\t\tGuestPortRange:    [2]int{1, 65535},\n\t\t\t\t\tHostIP:            net.IPv4zero,\n\t\t\t\t\tHostPortRange:     [2]int{1, 65535},\n\t\t\t\t\tProto:             limaconfig.TCP,\n\t\t\t\t},\n\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\tGuestIPMustBeZero: true,\n\t\t\t\t\tGuestIP:           net.IPv4zero,\n\t\t\t\t\tGuestPortRange:    [2]int{1, 65535},\n\t\t\t\t\tHostIP:            net.IPv4zero,\n\t\t\t\t\tHostPortRange:     [2]int{1, 65535},\n\t\t\t\t\tProto:             limaconfig.UDP,\n\t\t\t\t},\n\t\t\t)\n\t\t\t// bind 127.0.0.1\n\t\t\tl.PortForwards = append(l.PortForwards,\n\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\tGuestIP:        net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\tGuestPortRange: [2]int{1, 65535},\n\t\t\t\t\tHostIP:         net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\tHostPortRange:  [2]int{1, 65535},\n\t\t\t\t\tProto:          limaconfig.TCP,\n\t\t\t\t},\n\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\tGuestIP:        net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\tGuestPortRange: [2]int{1, 65535},\n\t\t\t\t\tHostIP:         net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t\tHostPortRange:  [2]int{1, 65535},\n\t\t\t\t\tProto:          limaconfig.UDP,\n\t\t\t\t},\n\t\t\t)\n\n\t\t\t// bind all host addresses when network address is not enabled\n\t\t\tif !conf.Network.Address && conf.Network.HostAddresses {\n\t\t\t\tfor _, ip := range util.HostIPAddresses() {\n\t\t\t\t\tl.PortForwards = append(l.PortForwards,\n\t\t\t\t\t\tlimaconfig.PortForward{\n\t\t\t\t\t\t\tGuestIP:        ip,\n\t\t\t\t\t\t\tGuestPortRange: [2]int{1, 65535},\n\t\t\t\t\t\t\tHostIP:         ip,\n\t\t\t\t\t\t\tHostPortRange:  [2]int{1, 65535},\n\t\t\t\t\t\t\tProto:          limaconfig.TCP,\n\t\t\t\t\t\t},\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tswitch strings.ToLower(conf.MountType) {\n\tcase \"ssh\", \"sshfs\", \"reversessh\", \"reverse-ssh\", \"reversesshfs\", limaconfig.REVSSHFS:\n\t\tl.MountType = limaconfig.REVSSHFS\n\tdefault:\n\t\tif l.VMType == limaconfig.VZ {\n\t\t\tl.MountType = limaconfig.VIRTIOFS\n\t\t} else { // qemu\n\t\t\tl.MountType = limaconfig.NINEP\n\t\t}\n\t}\n\n\t/*\n\t\tprovision scripts for disk actions\n\t*/\n\n\t// ensure all volumes are mounted.\n\tl.Provision = append(l.Provision, limaconfig.Provision{\n\t\tMode:   limaconfig.ProvisionModeSystem,\n\t\tScript: \"mount -a\",\n\t})\n\n\t// trim mounted drive to recover disk space\n\t// however problematic for incus\n\tif conf.Runtime != incus.Name {\n\t\tl.Provision = append(l.Provision, limaconfig.Provision{\n\t\t\tMode:   limaconfig.ProvisionModeSystem,\n\t\t\tScript: `readlink /usr/sbin/fstrim || fstrim -a`,\n\t\t})\n\t}\n\n\t// grow partition in case disk size has increased\n\tl.Provision = append(l.Provision, limaconfig.Provision{\n\t\tMode:   limaconfig.ProvisionModeSystem,\n\t\tScript: \"resize2fs \" + diskByLabelPath(config.CurrentProfile().ID) + \" || true\",\n\t})\n\n\t/* end */\n\n\tif conf.Mounts != nil && len(conf.Mounts) == 0 {\n\t\tl.Mounts = append(l.Mounts,\n\t\t\tlimaconfig.Mount{Location: \"~\", Writable: true},\n\t\t)\n\t} else {\n\t\t// overlapping mounts are problematic in Lima https://github.com/lima-vm/lima/issues/302\n\t\tif err = checkOverlappingMounts(conf.Mounts); err != nil {\n\t\t\terr = fmt.Errorf(\"overlapping mounts not supported: %w\", err)\n\t\t\treturn\n\t\t}\n\n\t\tfor _, m := range conf.Mounts {\n\t\t\tvar location, mountPoint string\n\t\t\tlocation, err = util.CleanPath(m.Location)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmountPoint, err = util.CleanPath(m.MountPoint)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmount := limaconfig.Mount{Location: location, MountPoint: mountPoint, Writable: m.Writable}\n\n\t\t\tl.Mounts = append(l.Mounts, mount)\n\t\t}\n\t}\n\n\t// provision scripts (only pass Lima-managed modes)\n\tfor _, script := range conf.Provision {\n\t\tif script.IsColimaMode() {\n\t\t\tcontinue\n\t\t}\n\t\tl.Provision = append(l.Provision, limaconfig.Provision{\n\t\t\tMode:   script.Mode,\n\t\t\tScript: script.Script,\n\t\t})\n\t}\n\n\treturn\n}\n\ntype Arch = environment.Arch\n\nfunc selectPath(m config.Mount) (string, error) {\n\tif m.MountPoint != \"\" {\n\t\treturn util.CleanPath(m.MountPoint)\n\t}\n\n\treturn util.CleanPath(m.Location)\n}\n\nfunc checkOverlappingMounts(mounts []config.Mount) error {\n\tfor i := 0; i < len(mounts)-1; i++ {\n\t\ta, err := selectPath(mounts[i])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfor j := i + 1; j < len(mounts); j++ {\n\t\t\tb, err := selectPath(mounts[j])\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif strings.HasPrefix(a, b) || strings.HasPrefix(b, a) {\n\t\t\t\treturn fmt.Errorf(\"'%s' overlaps '%s'\", a, b)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// disableHas checks if the provided feature is indeed found in the disable configuration slice.\nfunc ingressDisabled(disableFlags []string) bool {\n\tdisabled := func(s string) bool { return s == \"traefik\" || s == \"ingress\" }\n\tfor i, f := range disableFlags {\n\t\tif f == \"--disable\" {\n\t\t\tif len(disableFlags)-1 <= i {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif disabled(disableFlags[i+1]) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tstr := strings.SplitN(f, \"=\", 2)\n\t\tif len(str) < 2 || str[0] != \"--disable\" {\n\t\t\tcontinue\n\t\t}\n\t\tif disabled(str[1]) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nconst diskLabelMaxLength = 16 // https://tldp.org/HOWTO/Partition/labels.html\n\nfunc diskByLabelPath(instanceId string) string {\n\tname := \"lima-\" + instanceId\n\tif len(name) > diskLabelMaxLength {\n\t\tname = name[:diskLabelMaxLength]\n\t}\n\n\treturn \"/dev/disk/by-label/\" + name\n}\n"
  },
  {
    "path": "environment/vm/lima/yaml_test.go",
    "content": "package lima\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limaconfig\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/fsutil\"\n)\n\nfunc Test_checkOverlappingMounts(t *testing.T) {\n\ttype args struct {\n\t\tmounts []string\n\t}\n\ttests := []struct {\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{args: args{mounts: []string{\"/User\", \"/User/something\"}}, wantErr: true},\n\t\t{args: args{mounts: []string{\"/User/one\", \"/User/two\"}}, wantErr: false},\n\t\t{args: args{mounts: []string{\"/User/one\", \"/User/one_other\"}}, wantErr: false},\n\t\t{args: args{mounts: []string{\"/User/one_other\", \"/User/one\"}}, wantErr: false},\n\t\t{args: args{mounts: []string{\"/User/one\", \"/User/one/other\"}}, wantErr: true},\n\t\t{args: args{mounts: []string{\"/User/one/\", \"/User/one\"}}, wantErr: true},\n\t\t{args: args{mounts: []string{\"/User/one/\", \"/User/two\", \"User/one\"}}, wantErr: true},\n\t\t{args: args{mounts: []string{\"/home/a/b/c\", \"/home/b/c/a\", \"/home/c/a/b\"}}, wantErr: false},\n\t}\n\tfor i, tt := range tests {\n\t\tt.Run(fmt.Sprint(i), func(t *testing.T) {\n\t\t\tmounts := func(mounts []string) (mnts []config.Mount) {\n\t\t\t\tfor _, m := range mounts {\n\t\t\t\t\tmnts = append(mnts, config.Mount{Location: m})\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}(tt.args.mounts)\n\t\t\tif err := checkOverlappingMounts(mounts); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"checkOverlappingMounts() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_config_Mounts(t *testing.T) {\n\tfsutil.FS = fsutil.FakeFS\n\ttests := []struct {\n\t\tmounts    []string\n\t\tisDefault bool\n\t}{\n\t\t{mounts: []string{\"/User/user\", \"/tmp/another\"}},\n\t\t{mounts: []string{\"/User/another\", \"/User/something\", \"/User/else\"}},\n\t\t{mounts: []string{}, isDefault: true},\n\t\t{mounts: nil},\n\t\t{mounts: []string{util.HomeDir()}},\n\t}\n\tfor i, tt := range tests {\n\t\tt.Run(fmt.Sprint(i), func(t *testing.T) {\n\t\t\tmounts := func(mounts []string) (mnts []config.Mount) {\n\t\t\t\tif mounts != nil {\n\t\t\t\t\tmnts = []config.Mount{}\n\t\t\t\t}\n\n\t\t\t\tfor _, m := range mounts {\n\t\t\t\t\tmnts = append(mnts, config.Mount{Location: m})\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}(tt.mounts)\n\t\t\tconf, err := newConf(context.Background(), config.Config{Mounts: mounts})\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\texpectedLocations := tt.mounts\n\t\t\tif tt.isDefault {\n\t\t\t\texpectedLocations = []string{\"~\"}\n\t\t\t}\n\n\t\t\tsameMounts := func(expectedLocations []string, mounts []limaconfig.Mount) bool {\n\t\t\t\tsanitize := func(s string) string { return strings.TrimSuffix(s, \"/\") + \"/\" }\n\t\t\t\tfor i, m := range mounts {\n\t\t\t\t\tif sanitize(m.Location) != sanitize(expectedLocations[i]) {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t}(expectedLocations, conf.Mounts)\n\t\t\tif !sameMounts {\n\t\t\t\tfoundLocations := func() (locations []string) {\n\t\t\t\t\tfor _, m := range conf.Mounts {\n\t\t\t\t\t\tlocations = append(locations, m.Location)\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}()\n\t\t\t\tt.Errorf(\"got: %+v, want: %v\", foundLocations, expectedLocations)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_ingressDisabled(t *testing.T) {\n\ttests := []struct {\n\t\targs []string\n\t\twant bool\n\t}{\n\t\t{args: []string{\"--flag=f\", \"--another\", \"flag\"}, want: false},\n\t\t{args: []string{\"--disable=traefik\", \"--version=3\"}, want: true},\n\t\t{args: []string{}, want: false},\n\t\t{args: []string{\"--disable\", \"traefik\", \"--one=two\"}, want: true},\n\t}\n\tfor i, tt := range tests {\n\t\tt.Run(strconv.Itoa(i+1), func(t *testing.T) {\n\t\t\tif got := ingressDisabled(tt.args); got != tt.want {\n\t\t\t\tt.Errorf(\"ingressDisabled() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "environment/vm.go",
    "content": "package environment\n\nimport (\n\t\"context\"\n\t\"runtime\"\n\n\t\"github.com/abiosoft/colima/util\"\n)\n\n// VM is virtual machine.\ntype VM interface {\n\tGuestActions\n\tDependencies\n\tHost() HostActions\n\tTeardown(ctx context.Context) error\n}\n\n// VM configurations\nconst (\n\t// ContainerRuntimeKey is the settings key for container runtime.\n\tContainerRuntimeKey = \"runtime\"\n)\n\n// Arch is the CPU architecture of the VM.\ntype Arch string\n\nconst (\n\tX8664   Arch = \"x86_64\"\n\tAARCH64 Arch = \"aarch64\"\n)\n\n// HostArch returns the host CPU architecture.\nfunc HostArch() Arch {\n\treturn Arch(runtime.GOARCH).Value()\n}\n\n// GoArch returns the GOARCH equivalent value for the architecture.\nfunc (a Arch) GoArch() string {\n\tswitch a {\n\tcase X8664:\n\t\treturn \"amd64\"\n\tcase AARCH64:\n\t\treturn \"arm64\"\n\t}\n\n\treturn runtime.GOARCH\n}\n\n// Value converts the underlying architecture alias value to one of X8664 or AARCH64.\nfunc (a Arch) Value() Arch {\n\tswitch a {\n\tcase X8664, AARCH64:\n\t\treturn a\n\t// accept amd, amd64, x86, x64, arm, arm64 and m1 values\n\tcase \"amd\", \"amd64\", \"x86\", \"x64\":\n\t\treturn X8664\n\tcase \"arm\", \"arm64\", \"m1\":\n\t\treturn AARCH64\n\t}\n\n\treturn Arch(runtime.GOARCH).Value()\n}\n\n// DefaultVMType returns the default virtual machine type based on the operation\n// system and availability of Qemu.\nfunc DefaultVMType() string {\n\tif util.MacOS13OrNewer() {\n\t\treturn \"vz\"\n\t}\n\n\treturn \"qemu\"\n}\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"Container runtimes on macOS (and Linux) with minimal setup\";\n\n  # Last revision with go_1_23\n  inputs.nixpkgs.url = \"github:NixOS/nixpkgs/25.05\";\n\n  outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem\n    (system:\n      let\n        pkgs = nixpkgs.legacyPackages.${system};\n      in\n      {\n        packages.default = import ./colima.nix { inherit pkgs; };\n        devShell = import ./shell.nix { inherit pkgs; };\n        apps.default = {\n          type = \"app\";\n          program = \"${self.packages.${system}.default}/bin/colima\";\n        };\n      }\n    );\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/abiosoft/colima\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/coreos/go-semver v0.3.1\n\tgithub.com/docker/go-units v0.5.0\n\tgithub.com/fatih/color v1.18.0\n\tgithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510\n\tgithub.com/rjeczalik/notify v0.9.3\n\tgithub.com/schollz/progressbar/v3 v3.19.0\n\tgithub.com/sevlyar/go-daemon v0.1.6\n\tgithub.com/sirupsen/logrus v1.9.4\n\tgithub.com/spf13/cobra v1.10.2\n\tgolang.org/x/term v0.41.0\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/spf13/pflag v1.0.9 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=\ngithub.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=\ngithub.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=\ngithub.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=\ngithub.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=\ngithub.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=\ngithub.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY=\ngithub.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=\ngithub.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=\ngithub.com/sevlyar/go-daemon v0.1.6 h1:EUh1MDjEM4BI109Jign0EaknA2izkOyi0LV3ro3QQGs=\ngithub.com/sevlyar/go-daemon v0.1.6/go.mod h1:6dJpPatBT9eUwM5VCw9Bt6CdX9Tk6UWvhW3MebLDRKE=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "integration/Dockerfile",
    "content": "# sample dockerfile to test image building\n# without pulling from docker hub\nFROM scratch\n\nCOPY . /files"
  },
  {
    "path": "model/docker.go",
    "content": "package model\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/environment/host\"\n\t\"github.com/abiosoft/colima/environment/vm/lima\"\n\t\"github.com/abiosoft/colima/util/terminal\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// DockerModelInfo represents the output of docker model inspect.\ntype DockerModelInfo struct {\n\tID     string   `json:\"id\"`\n\tTags   []string `json:\"tags\"`\n\tConfig struct {\n\t\tFormat       string `json:\"format\"`\n\t\tQuantization string `json:\"quantization\"`\n\t\tParameters   string `json:\"parameters\"`\n\t\tArchitecture string `json:\"architecture\"`\n\t\tSize         string `json:\"size\"`\n\t} `json:\"config\"`\n}\n\n// Hash returns the model's hash (without the \"sha256:\" prefix).\nfunc (m *DockerModelInfo) Hash() string {\n\tif hash, ok := strings.CutPrefix(m.ID, \"sha256:\"); ok {\n\t\treturn hash\n\t}\n\treturn \"\"\n}\n\n// ociManifest represents the OCI manifest structure for Docker models.\ntype ociManifest struct {\n\tLayers []struct {\n\t\tMediaType string `json:\"mediaType\"`\n\t\tDigest    string `json:\"digest\"`\n\t} `json:\"layers\"`\n}\n\n// findGGUFPath finds the GGUF file path for a model inside the docker-model-runner container.\n// It handles both Docker registry models (bundle path) and HuggingFace models (blob path via manifest).\n// For models without a bundle, it creates the bundle structure by hard-linking the blob.\nfunc findGGUFPath(guest environment.VM, modelHash string) (string, error) {\n\t// Standard bundle path used by Docker Model Runner for all models\n\tbundlePath := fmt.Sprintf(\"/models/bundles/sha256/%s/model/model.gguf\", modelHash)\n\n\t// Check if bundle already exists\n\tif err := guest.RunQuiet(\"docker\", \"exec\", \"docker-model-runner\", \"test\", \"-f\", bundlePath); err == nil {\n\t\treturn bundlePath, nil\n\t}\n\n\t// Bundle doesn't exist - read manifest to find the GGUF blob and create the bundle\n\tmanifestPath := fmt.Sprintf(\"/models/manifests/sha256/%s\", modelHash)\n\toutput, err := guest.RunOutput(\"docker\", \"exec\", \"docker-model-runner\", \"cat\", manifestPath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read model manifest: %w\", err)\n\t}\n\n\tvar manifest ociManifest\n\tif err := json.Unmarshal([]byte(output), &manifest); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to parse model manifest: %w\", err)\n\t}\n\n\t// Find the GGUF layer (mediaType contains \"gguf\")\n\tvar blobPath string\n\tfor _, layer := range manifest.Layers {\n\t\tif strings.Contains(layer.MediaType, \"gguf\") {\n\t\t\tif blobHash, ok := strings.CutPrefix(layer.Digest, \"sha256:\"); ok {\n\t\t\t\tblobPath = fmt.Sprintf(\"/models/blobs/sha256/%s\", blobHash)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif blobPath == \"\" {\n\t\treturn \"\", fmt.Errorf(\"no GGUF layer found in model manifest\")\n\t}\n\n\t// Create bundle directory and hard-link the blob (same approach as Docker Model Runner)\n\tbundleDir := fmt.Sprintf(\"/models/bundles/sha256/%s/model\", modelHash)\n\tif err := guest.RunQuiet(\"docker\", \"exec\", \"docker-model-runner\", \"mkdir\", \"-p\", bundleDir); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create bundle directory: %w\", err)\n\t}\n\n\tif err := guest.RunQuiet(\"docker\", \"exec\", \"docker-model-runner\", \"ln\", blobPath, bundlePath); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to link model file: %w\", err)\n\t}\n\n\treturn bundlePath, nil\n}\n\n// InspectDockerModel returns information about a Docker model.\nfunc InspectDockerModel(modelName string) (*DockerModelInfo, error) {\n\tguest := lima.New(host.New())\n\toutput, err := guest.RunOutput(\"docker\", \"model\", \"inspect\", modelName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error inspecting model %q: %w\", modelName, err)\n\t}\n\n\tvar info DockerModelInfo\n\tif err := json.Unmarshal([]byte(strings.TrimSpace(output)), &info); err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing model info: %w\", err)\n\t}\n\n\treturn &info, nil\n}\n\n// SetupOrUpdateDocker reinstalls Docker Model Runner in the VM.\nfunc SetupOrUpdateDocker() error {\n\tguest := lima.New(host.New())\n\n\tlog.Println(\"reinstalling Docker Model Runner...\")\n\n\tif err := guest.RunInteractive(\"docker\", \"model\", \"reinstall-runner\"); err != nil {\n\t\treturn fmt.Errorf(\"error reinstalling Docker Model Runner: %w\", err)\n\t}\n\n\tlog.Println(\"Docker Model Runner reinstalled\")\n\n\t// Print installed version\n\tif version := GetDockerModelVersion(); version != \"\" {\n\t\tfmt.Println(\"Docker Model Runner\")\n\t\tfmt.Printf(\"version: %s\", version)\n\t\tfmt.Println()\n\t}\n\n\treturn nil\n}\n\n// GetDockerModelVersion returns the Docker Model Runner version in the VM.\n// Returns empty string if version cannot be determined.\nfunc GetDockerModelVersion() string {\n\tguest := lima.New(host.New())\n\toutput, err := guest.RunOutput(\"docker\", \"model\", \"version\")\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(output)\n}\n\n// EnsureDockerModel ensures a Docker model is available, pulling if necessary.\n// Returns the normalized model name (resolving aliases like hf.co → huggingface.co).\nfunc EnsureDockerModel(modelName string) (string, error) {\n\tguest := lima.New(host.New())\n\n\t// Try to inspect the model first\n\tmodelInfo, err := InspectDockerModel(modelName)\n\tif err != nil {\n\t\t// Model not found locally, try to pull it\n\t\tif pullErr := guest.RunInteractive(\"docker\", \"model\", \"pull\", modelName); pullErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to pull model %q: %w\", modelName, pullErr)\n\t\t}\n\t\t// Retry inspect after pull\n\t\tmodelInfo, err = InspectDockerModel(modelName)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to inspect model %q after pull: %w\", modelName, err)\n\t\t}\n\t}\n\n\t// Return the first tag as the normalized name (e.g., \"docker.io/ai/smollm2:latest\")\n\tif len(modelInfo.Tags) > 0 {\n\t\treturn modelInfo.Tags[0], nil\n\t}\n\treturn modelName, nil\n}\n\n// DockerModelServeConfig holds configuration for serving a Docker model.\ntype DockerModelServeConfig struct {\n\tModelName string // Model name (e.g., \"smollm2\")\n\tPort      int    // Host port to expose the model on\n\tThreads   int    // Number of CPU threads (default: 2)\n\tGPULayers int    // Number of GPU layers (default: 999 = all)\n}\n\n// ServeDockerModel serves a Docker model with llama-server.\n// It runs llama-server interactively (with visible output) and uses socat to forward the port.\n// The function blocks until interrupted (Ctrl-C) or llama-server exits.\n// Note: Call EnsureDockerModel first to ensure the model is available.\nfunc ServeDockerModel(cfg DockerModelServeConfig) error {\n\tguest := lima.New(host.New())\n\n\t// Set defaults\n\tif cfg.Threads <= 0 {\n\t\tcfg.Threads = 2\n\t}\n\tif cfg.GPULayers <= 0 {\n\t\tcfg.GPULayers = 999\n\t}\n\n\t// Get the model info (model should already be available via EnsureDockerModel)\n\tmodelInfo, err := InspectDockerModel(cfg.ModelName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to inspect model %q: %w\", cfg.ModelName, err)\n\t}\n\n\t// Check model format - only GGUF models are supported\n\tif modelInfo.Config.Format != \"gguf\" {\n\t\treturn fmt.Errorf(\"model %q has format %q, only GGUF models are supported\\n\"+\n\t\t\t\"Try a GGUF version of this model (e.g., from TheBloke on HuggingFace)\",\n\t\t\tcfg.ModelName, modelInfo.Config.Format)\n\t}\n\n\tmodelHash := modelInfo.Hash()\n\tif modelHash == \"\" {\n\t\treturn fmt.Errorf(\"could not determine hash for model %q\", cfg.ModelName)\n\t}\n\n\t// Ensure docker-model-runner container is running (needed to find GGUF path)\n\tif err := ensureDockerModelRunner(guest); err != nil {\n\t\treturn err\n\t}\n\n\t// Find the GGUF file path (handles both Docker registry and HuggingFace models)\n\tggufPath, err := findGGUFPath(guest, modelHash)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not find GGUF file for model %q: %w\", cfg.ModelName, err)\n\t}\n\n\t// Get container IP\n\tcontainerIP, err := getDockerModelRunnerIP(guest)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Kill any existing socat on this port\n\tstopSocat(guest, cfg.Port)\n\n\t// Start socat in background to forward localhost:port → container_ip:port\n\tif err := startSocat(guest, cfg.Port, containerIP); err != nil {\n\t\treturn fmt.Errorf(\"failed to start port forwarder: %w\", err)\n\t}\n\n\t// Run llama-server interactively (blocking, with visible output)\n\t// Ctrl-C will be received by the interactive process directly\n\t// Use -it for TTY, -i for non-TTY (e.g., piped or CI environments)\n\texecFlag := \"-i\"\n\tif terminal.IsTerminal() {\n\t\texecFlag = \"-it\"\n\t}\n\n\terr = guest.RunInteractive(\"docker\", \"exec\", execFlag, \"docker-model-runner\",\n\t\t\"/app/bin/com.docker.llama-server\",\n\t\t\"-ngl\", fmt.Sprintf(\"%d\", cfg.GPULayers),\n\t\t\"--metrics\",\n\t\t\"--threads\", fmt.Sprintf(\"%d\", cfg.Threads),\n\t\t\"--model\", ggufPath,\n\t\t\"--alias\", cfg.ModelName,\n\t\t\"--host\", \"0.0.0.0\",\n\t\t\"--port\", fmt.Sprintf(\"%d\", cfg.Port),\n\t\t\"--jinja\",\n\t)\n\n\t// Cleanup socat on exit (whether normal exit or Ctrl-C)\n\tstopSocat(guest, cfg.Port)\n\n\treturn err\n}\n\n// ensureDockerModelRunner ensures the docker-model-runner container is running.\n// Attempts to start it up to 3 times if not found.\nfunc ensureDockerModelRunner(guest environment.VM) error {\n\tfor attempt := 1; attempt <= 3; attempt++ {\n\t\t// Check if container exists\n\t\tif err := guest.RunQuiet(\"docker\", \"inspect\", \"docker-model-runner\"); err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tlog.Infof(\"docker-model-runner not found, starting it (attempt %d/3)...\", attempt)\n\t\t_ = guest.Run(\"docker\", \"model\", \"start-runner\")\n\t\ttime.Sleep(2 * time.Second)\n\t}\n\n\treturn fmt.Errorf(\"could not start docker-model-runner after 3 attempts\")\n}\n\n// getDockerModelRunnerIP returns the IP address of the docker-model-runner container.\nfunc getDockerModelRunnerIP(guest environment.VM) (string, error) {\n\toutput, err := guest.RunOutput(\"docker\", \"inspect\", \"docker-model-runner\",\n\t\t\"--format\", \"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get container IP: %w\", err)\n\t}\n\n\tip := strings.TrimSpace(output)\n\tif ip == \"\" {\n\t\treturn \"\", fmt.Errorf(\"container IP is empty\")\n\t}\n\n\treturn ip, nil\n}\n\n// startSocat starts socat in the background to forward a port to the container.\nfunc startSocat(guest environment.VM, port int, containerIP string) error {\n\tcmd := fmt.Sprintf(\"nohup socat TCP-LISTEN:%d,fork,reuseaddr TCP:%s:%d > /dev/null 2>&1 &\",\n\t\tport, containerIP, port)\n\treturn guest.Run(\"sh\", \"-c\", cmd)\n}\n\n// stopSocat stops the socat process for a given port.\nfunc stopSocat(guest environment.VM, port int) {\n\tcmd := fmt.Sprintf(\"pkill -f 'socat.*TCP-LISTEN:%d' 2>/dev/null || true\", port)\n\t_ = guest.Run(\"sh\", \"-c\", cmd)\n}\n\n// StopDockerModelServe stops a Docker model serve instance.\nfunc StopDockerModelServe(port int) error {\n\tguest := lima.New(host.New())\n\n\t// Stop the socat proxy on the VM\n\tstopCmd := fmt.Sprintf(\"pkill -f 'socat.*TCP-LISTEN:%d' 2>/dev/null || true\", port)\n\tif err := guest.Run(\"sh\", \"-c\", stopCmd); err != nil {\n\t\tlog.Debugf(\"error stopping socat: %v\", err)\n\t}\n\n\t// Note: llama-server processes inside docker-model-runner are harder to clean up\n\t// since they run in the same container. For now, we just stop the socat proxy.\n\t// The llama-server process will remain running but be inaccessible.\n\n\treturn nil\n}\n\n// IsDockerModelServeRunning checks if a serve instance is running on the given port.\nfunc IsDockerModelServeRunning(port int) bool {\n\tguest := lima.New(host.New())\n\n\t// Check if socat is running for this port\n\tcheckCmd := fmt.Sprintf(\"pgrep -f 'socat.*TCP-LISTEN:%d' > /dev/null 2>&1\", port)\n\terr := guest.Run(\"sh\", \"-c\", checkCmd)\n\treturn err == nil\n}\n"
  },
  {
    "path": "model/ramalama.go",
    "content": "package model\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/environment/host\"\n\t\"github.com/abiosoft/colima/environment/vm/lima\"\n\t\"github.com/abiosoft/colima/store\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nconst ramalamaReleasesURL = \"https://api.github.com/repos/containers/ramalama/releases/latest\"\n\n// SetupOrUpdateRamalama installs or updates ramalama.\n// Call CheckSetup() first to determine if setup is needed and display version info.\nfunc SetupOrUpdateRamalama() error {\n\treturn ProvisionRamalama()\n}\n\n// GetRamalamaVersion returns the currently installed ramalama version in the VM.\n// Returns empty string if ramalama is not installed or version cannot be determined.\nfunc GetRamalamaVersion() string {\n\tguest := lima.New(host.New())\n\toutput, err := guest.RunOutput(\"sh\", \"-c\", `export PATH=\"$HOME/.local/bin:$PATH\"; ramalama version 2>/dev/null`)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\t// Output format: \"ramalama version 0.17.1\"\n\toutput = strings.TrimSpace(output)\n\tif version, ok := strings.CutPrefix(output, \"ramalama version \"); ok {\n\t\treturn version\n\t}\n\treturn \"\"\n}\n\n// getLatestRamalamaVersion fetches the latest release version from GitHub.\nfunc getLatestRamalamaVersion() (string, error) {\n\tclient := &http.Client{Timeout: 10 * time.Second}\n\tresp, err := client.Get(ramalamaReleasesURL)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to fetch releases: %w\", err)\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"unexpected status code: %d\", resp.StatusCode)\n\t}\n\n\tvar release struct {\n\t\tTagName string `json:\"tag_name\"`\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&release); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to decode response: %w\", err)\n\t}\n\n\t// Tag might be \"v0.17.1\" or \"0.17.1\"\n\tversion := strings.TrimPrefix(release.TagName, \"v\")\n\treturn version, nil\n}\n\n// ramalamaModel represents a model from ramalama ls --json output.\ntype ramalamaModel struct {\n\tName     string `json:\"name\"`\n\tModified string `json:\"modified\"`\n\tSize     int64  `json:\"size\"`\n}\n\n// listRamalamaModels returns all locally available ramalama models.\nfunc listRamalamaModels() ([]ramalamaModel, error) {\n\tguest := lima.New(host.New())\n\toutput, err := guest.RunOutput(\"sh\", \"-c\", `export PATH=\"$HOME/.local/bin:$PATH\"; ramalama ls --json 2>/dev/null`)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error listing models: %w\", err)\n\t}\n\n\toutput = strings.TrimSpace(output)\n\tif output == \"\" || output == \"[]\" {\n\t\treturn nil, nil\n\t}\n\n\tvar models []ramalamaModel\n\tif err := json.Unmarshal([]byte(output), &models); err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing model list: %w\", err)\n\t}\n\n\treturn models, nil\n}\n\n// ramalamaModelExists checks if a model exists locally in ramalama.\nfunc ramalamaModelExists(modelName string) bool {\n\tmodels, err := listRamalamaModels()\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// Normalize the input model name\n\tnormalizedInput := normalizeRamalamaModelName(modelName)\n\n\tfor _, m := range models {\n\t\t// Model names in ramalama have format like \"hf://TheBloke/...\" or \"ollama://library/...\"\n\t\tnormalizedStored := normalizeRamalamaModelName(m.Name)\n\t\tif normalizedInput == normalizedStored {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// normalizeRamalamaModelName normalizes a ramalama model name for comparison.\nfunc normalizeRamalamaModelName(name string) string {\n\tname = strings.ToLower(strings.TrimSpace(name))\n\n\t// Normalize different URL formats to a common form\n\t// \"hf.co/...\" -> \"hf://...\"\n\t// \"huggingface.co/...\" -> \"hf://...\"\n\tname = strings.ReplaceAll(name, \"hf.co/\", \"hf://\")\n\tname = strings.ReplaceAll(name, \"huggingface.co/\", \"hf://\")\n\n\treturn name\n}\n\n// EnsureRamalamaModel ensures a ramalama model is available, pulling if necessary.\nfunc EnsureRamalamaModel(modelName string) error {\n\tif ramalamaModelExists(modelName) {\n\t\treturn nil\n\t}\n\n\t// Model not found locally, pull it\n\tguest := lima.New(host.New())\n\tshellCmd := fmt.Sprintf(\n\t\t`export RAMALAMA_CONTAINER_ENGINE=docker PATH=\"$HOME/.local/bin:$PATH\"; ramalama pull %s`,\n\t\tmodelName,\n\t)\n\n\tif err := guest.RunInteractive(\"sh\", \"-c\", shellCmd); err != nil {\n\t\treturn fmt.Errorf(\"failed to pull model %q: %w\", modelName, err)\n\t}\n\n\treturn nil\n}\n\n// ProvisionRamalama installs ramalama and its dependencies in the VM.\nfunc ProvisionRamalama() error {\n\tguest := lima.New(host.New())\n\n\tscript := `set -e\nexport PATH=\"$HOME/.local/bin:$PATH\"\n\n# ensure pipx is available\nsudo apt-get update -y && sudo apt-get install -y pipx\n\n# install ramalama via pipx; upgrade if ramalama is already installed\nif command -v ramalama >/dev/null 2>&1; then\n  pipx upgrade ramalama\nelse\n  pipx install ramalama\nfi\n\n# pull ramalama container images\ndocker pull quay.io/ramalama/ramalama\ndocker pull quay.io/ramalama/ramalama-rag\n\n# fix ownership of persistent data dir and symlink to expected location\nsudo chown -R $(id -u):$(id -g) /var/lib/ramalama\nmkdir -p \"$HOME/.local/share\"\nln -sfn /var/lib/ramalama \"$HOME/.local/share/ramalama\"\n`\n\n\tlog.Println(\"installing AI model runner...\")\n\n\tif err := guest.RunInteractive(\"sh\", \"-c\", script); err != nil {\n\t\treturn fmt.Errorf(\"error setting up AI model runner: %w\", err)\n\t}\n\n\tlog.Println(\"AI model runner installed\")\n\n\t// mark as provisioned\n\tif err := store.Set(func(s *store.Store) {\n\t\ts.RamalamaProvisioned = true\n\t}); err != nil {\n\t\treturn fmt.Errorf(\"error saving provisioning state: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "model/runner.go",
    "content": "package model\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/app\"\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/config/configmanager\"\n\t\"github.com/abiosoft/colima/environment/container/docker\"\n\t\"github.com/abiosoft/colima/environment/host\"\n\t\"github.com/abiosoft/colima/environment/vm/lima\"\n\t\"github.com/abiosoft/colima/environment/vm/lima/limaconfig\"\n\t\"github.com/abiosoft/colima/store\"\n\t\"github.com/abiosoft/colima/util\"\n\t\"github.com/abiosoft/colima/util/terminal\"\n\t\"github.com/coreos/go-semver/semver\"\n\tlog \"github.com/sirupsen/logrus\"\n)\n\n// RunnerType represents the type of AI model runner.\ntype RunnerType string\n\nconst (\n\tRunnerDocker   RunnerType = \"docker\"\n\tRunnerRamalama RunnerType = \"ramalama\"\n)\n\n// SetupStatus contains the result of checking if setup is needed.\ntype SetupStatus struct {\n\t// NeedsSetup indicates whether setup/update is required.\n\tNeedsSetup bool\n\t// CurrentVersion is the currently installed version (empty if not installed).\n\tCurrentVersion string\n\t// LatestVersion is the latest available version (empty if not checked).\n\tLatestVersion string\n}\n\n// Runner defines the interface for AI model runners.\ntype Runner interface {\n\t// Name returns the runner type name.\n\tName() RunnerType\n\t// DisplayName returns a human-readable name for the runner.\n\tDisplayName() string\n\t// ValidatePrerequisites checks runner-specific requirements.\n\tValidatePrerequisites(a app.App) error\n\t// EnsureProvisioned ensures the runner is set up (no-op for docker).\n\tEnsureProvisioned() error\n\t// BuildArgs constructs the command arguments for the runner.\n\t// Returns an error if the command is not supported.\n\tBuildArgs(args []string) ([]string, error)\n\t// EnsureModel ensures a model is available (pulls if necessary).\n\t// Returns the normalized model name.\n\tEnsureModel(model string) (string, error)\n\t// Serve starts serving a model on the given port.\n\t// This is a blocking call that runs until interrupted.\n\t// The model should already be available (call EnsureModel first).\n\tServe(model string, port int) error\n\t// CheckSetup checks if setup/update is needed and returns version info.\n\t// This should be called before Setup() to display version info on primary screen.\n\tCheckSetup() (SetupStatus, error)\n\t// Setup installs or updates the runner.\n\t// Call CheckSetup() first to determine if setup is needed.\n\tSetup() error\n\t// GetCurrentVersion returns the currently installed version.\n\tGetCurrentVersion() string\n}\n\n// GetRunner returns the appropriate Runner based on type.\nfunc GetRunner(runnerType RunnerType) (Runner, error) {\n\tswitch runnerType {\n\tcase RunnerDocker:\n\t\treturn &dockerRunner{}, nil\n\tcase RunnerRamalama:\n\t\treturn &ramalamaRunner{}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown runner type: %s (valid options: docker, ramalama)\", runnerType)\n\t}\n}\n\n// validateCommonPrerequisites checks prerequisites common to all runners.\nfunc validateCommonPrerequisites(a app.App) error {\n\t// VM must be running\n\tif !a.Active() {\n\t\treturn fmt.Errorf(\"%s is not running\", config.CurrentProfile().DisplayName)\n\t}\n\n\t// check runtime is docker\n\tr, err := a.Runtime()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif r != docker.Name {\n\t\treturn fmt.Errorf(\"'colima model' requires docker runtime, current runtime is %s\\n\"+\n\t\t\t\"Start colima with: colima start --runtime docker --vm-type krunkit\", r)\n\t}\n\n\t// check VM type is krunkit (required for GPU access)\n\tconf, err := configmanager.LoadInstance()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error loading instance config: %w\", err)\n\t}\n\tif conf.VMType != limaconfig.Krunkit {\n\t\treturn fmt.Errorf(\"'colima model' requires krunkit VM type for GPU access, current VM type is %s\\n\"+\n\t\t\t\"Start colima with: colima start --runtime docker --vm-type krunkit\", conf.VMType)\n\t}\n\n\t// check krunkit binary exists on host\n\tif err := util.AssertKrunkit(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// dockerRunner implements Runner for Docker Model Runner.\ntype dockerRunner struct{}\n\nfunc (d *dockerRunner) Name() RunnerType {\n\treturn RunnerDocker\n}\n\nfunc (d *dockerRunner) DisplayName() string {\n\treturn \"Docker Model Runner\"\n}\n\nfunc (d *dockerRunner) ValidatePrerequisites(a app.App) error {\n\treturn validateCommonPrerequisites(a)\n}\n\nfunc (d *dockerRunner) EnsureProvisioned() error {\n\t// Docker Model Runner requires no provisioning\n\treturn nil\n}\n\nfunc (d *dockerRunner) BuildArgs(args []string) ([]string, error) {\n\t// docker model <subcommand> [args...]\n\treturn append([]string{\"docker\", \"model\"}, args...), nil\n}\n\n// EnsureModel ensures a Docker model is available, pulling if necessary.\n// Returns the normalized model name (resolving aliases like hf.co → huggingface.co).\nfunc (d *dockerRunner) EnsureModel(modelName string) (string, error) {\n\treturn EnsureDockerModel(modelName)\n}\n\n// Serve starts serving a Docker model using llama-server.\nfunc (d *dockerRunner) Serve(modelName string, port int) error {\n\treturn ServeDockerModel(DockerModelServeConfig{\n\t\tModelName: modelName,\n\t\tPort:      port,\n\t})\n}\n\n// dockerModel represents a model from docker model list --json output.\ntype dockerModel struct {\n\tID   string   `json:\"id\"`\n\tTags []string `json:\"tags\"`\n}\n\n// GetFirstModel returns the first available model from docker model list.\n// Returns empty string if no models are available.\nfunc GetFirstModel() (string, error) {\n\tmodels, err := listDockerModels()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(models) == 0 {\n\t\treturn \"\", nil\n\t}\n\t// Return the first tag of the first model\n\tif len(models[0].Tags) > 0 {\n\t\treturn models[0].Tags[0], nil\n\t}\n\treturn \"\", nil\n}\n\n// listDockerModels returns all available models from docker model list.\nfunc listDockerModels() ([]dockerModel, error) {\n\tguest := lima.New(host.New())\n\toutput, err := guest.RunOutput(\"docker\", \"model\", \"list\", \"--json\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error listing models: %w\", err)\n\t}\n\n\toutput = strings.TrimSpace(output)\n\tif output == \"\" || output == \"[]\" {\n\t\treturn nil, nil\n\t}\n\n\tvar models []dockerModel\n\tif err := json.Unmarshal([]byte(output), &models); err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing model list: %w\", err)\n\t}\n\n\treturn models, nil\n}\n\n// ResolveModelName resolves a short model name to its full tag.\n// Supports flexible matching:\n//   - \"smollm2\" resolves to \"docker.io/ai/smollm2:latest\"\n//   - \"ai/smollm2\" resolves to \"docker.io/ai/smollm2:latest\"\n//   - \"hf.co/...\" resolves to \"huggingface.co/...\"\n//\n// Returns the original name if no match is found (for new models to be pulled).\nfunc ResolveModelName(name string) (string, error) {\n\tmodels, err := listDockerModels()\n\tif err != nil {\n\t\treturn name, err\n\t}\n\n\tfor _, m := range models {\n\t\tfor _, tag := range m.Tags {\n\t\t\tif matchesModel(name, tag) {\n\t\t\t\treturn tag, nil\n\t\t\t}\n\t\t}\n\t}\n\t// Return original name if not found (will be pulled)\n\treturn name, nil\n}\n\n// matchesModel checks if a user-provided name matches a full model tag.\nfunc matchesModel(name, tag string) bool {\n\t// Normalize both for comparison\n\tnormName := normalizeModelName(name)\n\tnormTag := normalizeModelName(tag)\n\n\t// Exact match after normalization\n\tif normName == normTag {\n\t\treturn true\n\t}\n\n\t// Check if name is a suffix of tag (e.g., \"smollm2\" matches \"ai/smollm2\")\n\t// Strip the tag version suffix for matching\n\ttagParts := strings.Split(normTag, \":\")\n\ttagWithoutVersion := tagParts[0]\n\ttagVersion := \"\"\n\tif len(tagParts) > 1 {\n\t\ttagVersion = tagParts[1]\n\t}\n\n\tnameParts := strings.Split(normName, \":\")\n\tnameWithoutVersion := nameParts[0]\n\tnameHasVersion := len(nameParts) > 1\n\n\t// If input has no version, only match :latest tags\n\tif !nameHasVersion && tagVersion != \"\" && tagVersion != \"latest\" {\n\t\treturn false\n\t}\n\n\t// \"smollm2\" should match \"ai/smollm2:latest\"\n\tif strings.HasSuffix(tagWithoutVersion, \"/\"+normName) {\n\t\treturn true\n\t}\n\n\t// \"ai/smollm2\" should match \"docker.io/ai/smollm2\" or just \"ai/smollm2\"\n\tif strings.HasSuffix(tagWithoutVersion, \"/\"+nameWithoutVersion) {\n\t\treturn true\n\t}\n\n\t// Direct suffix match (handles cases like \"tinyllama/tinyllama-1.1b-chat-v1.0\")\n\tif strings.HasSuffix(tagWithoutVersion, nameWithoutVersion) {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// normalizeModelName normalizes a model name for comparison.\nfunc normalizeModelName(name string) string {\n\tname = strings.ToLower(strings.TrimSpace(name))\n\n\t// Normalize registry prefixes\n\tname = strings.TrimPrefix(name, \"docker.io/\")\n\tname = strings.ReplaceAll(name, \"hf.co/\", \"huggingface.co/\")\n\n\treturn name\n}\n\nfunc (d *dockerRunner) CheckSetup() (SetupStatus, error) {\n\t// Docker Model Runner always reinstalls; no version comparison\n\treturn SetupStatus{\n\t\tNeedsSetup:     true,\n\t\tCurrentVersion: GetDockerModelVersion(),\n\t}, nil\n}\n\nfunc (d *dockerRunner) Setup() error {\n\treturn SetupOrUpdateDocker()\n}\n\nfunc (d *dockerRunner) GetCurrentVersion() string {\n\treturn GetDockerModelVersion()\n}\n\n// gpuSubcommands are ramalama subcommands that need GPU device passthrough.\nvar gpuSubcommands = map[string]bool{\n\t\"run\":        true,\n\t\"serve\":      true,\n\t\"bench\":      true,\n\t\"chat\":       true,\n\t\"perplexity\": true,\n}\n\n// ramalamaRunner implements Runner for Ramalama.\ntype ramalamaRunner struct{}\n\nfunc (r *ramalamaRunner) Name() RunnerType {\n\treturn RunnerRamalama\n}\n\nfunc (r *ramalamaRunner) DisplayName() string {\n\treturn \"Ramalama\"\n}\n\nfunc (r *ramalamaRunner) ValidatePrerequisites(a app.App) error {\n\treturn validateCommonPrerequisites(a)\n}\n\nfunc (r *ramalamaRunner) EnsureProvisioned() error {\n\ts, _ := store.Load()\n\tif s.RamalamaProvisioned {\n\t\treturn nil\n\t}\n\n\tprompt := fmt.Sprintf(\"%s requires initial setup (this may take a few minutes depending on internet connection speed). Continue\", r.DisplayName())\n\tif !cli.Prompt(prompt) {\n\t\treturn fmt.Errorf(\"setup cancelled\")\n\t}\n\n\tseparator := \"────────────────────────────────────────\"\n\theader := fmt.Sprintf(\"Colima - %s Setup\\n%s\", r.DisplayName(), separator)\n\n\treturn terminal.WithAltScreen(ProvisionRamalama, header)\n}\n\nfunc (r *ramalamaRunner) BuildArgs(args []string) ([]string, error) {\n\treturn r.buildRamalamaArgs(args), nil\n}\n\n// EnsureModel ensures a ramalama model is available, pulling if necessary.\nfunc (r *ramalamaRunner) EnsureModel(modelName string) (string, error) {\n\tif err := EnsureRamalamaModel(modelName); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn modelName, nil\n}\n\n// Serve starts serving a model using ramalama.\nfunc (r *ramalamaRunner) Serve(modelName string, port int) error {\n\tguest := lima.New(host.New())\n\n\t// ramalama serve <model> with GPU support and custom port\n\tshellCmd := fmt.Sprintf(\n\t\t`export RAMALAMA_CONTAINER_ENGINE=docker PATH=\"$HOME/.local/bin:$PATH\"; exec ramalama serve --device=/dev/dri -p %d %s`,\n\t\tport, modelName,\n\t)\n\n\treturn guest.RunInteractive(\"sh\", \"-c\", shellCmd)\n}\n\nfunc (r *ramalamaRunner) buildRamalamaArgs(args []string) []string {\n\tshellCmd := `export RAMALAMA_CONTAINER_ENGINE=docker PATH=\"$HOME/.local/bin:$PATH\"; exec ramalama \"$@\"`\n\n\tramalamaArgs := []string{\"sh\", \"-c\", shellCmd, \"--\"}\n\n\t// for GPU subcommands, inject --device=/dev/dri after the subcommand name\n\tif len(args) > 0 && gpuSubcommands[args[0]] {\n\t\tramalamaArgs = append(ramalamaArgs, args[0], \"--device=/dev/dri\")\n\t\tramalamaArgs = append(ramalamaArgs, args[1:]...)\n\t} else {\n\t\tramalamaArgs = append(ramalamaArgs, args...)\n\t}\n\n\treturn ramalamaArgs\n}\n\nfunc (r *ramalamaRunner) CheckSetup() (SetupStatus, error) {\n\ts, _ := store.Load()\n\n\t// Fresh install - no version check needed\n\tif !s.RamalamaProvisioned {\n\t\treturn SetupStatus{NeedsSetup: true}, nil\n\t}\n\n\t// Get current version\n\tcurrentVersion := GetRamalamaVersion()\n\tif currentVersion == \"\" {\n\t\t// Can't determine current version, proceed with update\n\t\tlog.Debug(\"could not determine current ramalama version, proceeding with update\")\n\t\treturn SetupStatus{NeedsSetup: true}, nil\n\t}\n\n\t// Fetch latest version\n\tlatestVersion, err := getLatestRamalamaVersion()\n\tif err != nil {\n\t\tlog.Debugf(\"could not fetch latest ramalama version: %v\", err)\n\t\treturn SetupStatus{}, fmt.Errorf(\"could not check for updates: %w\", err)\n\t}\n\n\t// Compare versions\n\tcurrent, err := semver.NewVersion(currentVersion)\n\tif err != nil {\n\t\tlog.Debugf(\"could not parse current version %q: %v\", currentVersion, err)\n\t\treturn SetupStatus{\n\t\t\tNeedsSetup:     true,\n\t\t\tCurrentVersion: currentVersion,\n\t\t\tLatestVersion:  latestVersion,\n\t\t}, nil\n\t}\n\n\tlatest, err := semver.NewVersion(latestVersion)\n\tif err != nil {\n\t\tlog.Debugf(\"could not parse latest version %q: %v\", latestVersion, err)\n\t\treturn SetupStatus{\n\t\t\tNeedsSetup:     true,\n\t\t\tCurrentVersion: currentVersion,\n\t\t\tLatestVersion:  latestVersion,\n\t\t}, nil\n\t}\n\n\tneedsSetup := current.Compare(*latest) < 0\n\n\treturn SetupStatus{\n\t\tNeedsSetup:     needsSetup,\n\t\tCurrentVersion: currentVersion,\n\t\tLatestVersion:  latestVersion,\n\t}, nil\n}\n\nfunc (r *ramalamaRunner) Setup() error {\n\treturn SetupOrUpdateRamalama()\n}\n\nfunc (r *ramalamaRunner) GetCurrentVersion() string {\n\treturn GetRamalamaVersion()\n}\n"
  },
  {
    "path": "model/runner_test.go",
    "content": "package model\n\nimport (\n\t\"testing\"\n)\n\nfunc TestNormalizeModelName(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"lowercase conversion\",\n\t\t\tinput:    \"AI/SmollM2\",\n\t\t\texpected: \"ai/smollm2\",\n\t\t},\n\t\t{\n\t\t\tname:     \"trim whitespace\",\n\t\t\tinput:    \"  ai/smollm2  \",\n\t\t\texpected: \"ai/smollm2\",\n\t\t},\n\t\t{\n\t\t\tname:     \"strip docker.io prefix\",\n\t\t\tinput:    \"docker.io/ai/smollm2\",\n\t\t\texpected: \"ai/smollm2\",\n\t\t},\n\t\t{\n\t\t\tname:     \"convert hf.co to huggingface.co\",\n\t\t\tinput:    \"hf.co/tinyllama/model\",\n\t\t\texpected: \"huggingface.co/tinyllama/model\",\n\t\t},\n\t\t{\n\t\t\tname:     \"already normalized\",\n\t\t\tinput:    \"ai/smollm2:latest\",\n\t\t\texpected: \"ai/smollm2:latest\",\n\t\t},\n\t\t{\n\t\t\tname:     \"huggingface.co unchanged\",\n\t\t\tinput:    \"huggingface.co/tinyllama/model:latest\",\n\t\t\texpected: \"huggingface.co/tinyllama/model:latest\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := normalizeModelName(tt.input)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"normalizeModelName(%q) = %q, want %q\", tt.input, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMatchesModel(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\ttag      string\n\t\texpected bool\n\t}{\n\t\t// Exact matches\n\t\t{\n\t\t\tname:     \"exact match with full tag\",\n\t\t\tinput:    \"docker.io/ai/smollm2:latest\",\n\t\t\ttag:      \"docker.io/ai/smollm2:latest\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"exact match after normalization\",\n\t\t\tinput:    \"ai/smollm2:latest\",\n\t\t\ttag:      \"docker.io/ai/smollm2:latest\",\n\t\t\texpected: true,\n\t\t},\n\n\t\t// Short name matches\n\t\t{\n\t\t\tname:     \"short name matches full tag\",\n\t\t\tinput:    \"smollm2\",\n\t\t\ttag:      \"docker.io/ai/smollm2:latest\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"short name with ai prefix\",\n\t\t\tinput:    \"ai/smollm2\",\n\t\t\ttag:      \"docker.io/ai/smollm2:latest\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"short name case insensitive\",\n\t\t\tinput:    \"SmollM2\",\n\t\t\ttag:      \"docker.io/ai/smollm2:latest\",\n\t\t\texpected: true,\n\t\t},\n\n\t\t// Huggingface models\n\t\t{\n\t\t\tname:     \"hf.co prefix matches huggingface.co\",\n\t\t\tinput:    \"hf.co/tinyllama/tinyllama-1.1b-chat-v1.0\",\n\t\t\ttag:      \"huggingface.co/tinyllama/tinyllama-1.1b-chat-v1.0:latest\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"huggingface short name\",\n\t\t\tinput:    \"tinyllama/tinyllama-1.1b-chat-v1.0\",\n\t\t\ttag:      \"huggingface.co/tinyllama/tinyllama-1.1b-chat-v1.0:latest\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"huggingface model name only\",\n\t\t\tinput:    \"tinyllama-1.1b-chat-v1.0\",\n\t\t\ttag:      \"huggingface.co/tinyllama/tinyllama-1.1b-chat-v1.0:latest\",\n\t\t\texpected: true,\n\t\t},\n\n\t\t// Version tag handling\n\t\t{\n\t\t\tname:     \"input without version matches tag with version\",\n\t\t\tinput:    \"ai/smollm2\",\n\t\t\ttag:      \"docker.io/ai/smollm2:latest\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"input with version matches tag with same version\",\n\t\t\tinput:    \"ai/smollm2:latest\",\n\t\t\ttag:      \"docker.io/ai/smollm2:latest\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"input with specific version matches tag with same version\",\n\t\t\tinput:    \"ai/smollm2:v1.0\",\n\t\t\ttag:      \"docker.io/ai/smollm2:v1.0\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"input without version does NOT match tag with specific version\",\n\t\t\tinput:    \"smollm2\",\n\t\t\ttag:      \"docker.io/ai/smollm2:v1.0\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"short name does NOT match tag with specific version\",\n\t\t\tinput:    \"gemma3\",\n\t\t\ttag:      \"docker.io/ai/gemma3:4b-it-qat-q4_0\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"input without version matches tag with latest\",\n\t\t\tinput:    \"smollm2\",\n\t\t\ttag:      \"docker.io/ai/smollm2:latest\",\n\t\t\texpected: true,\n\t\t},\n\n\t\t// Non-matches\n\t\t{\n\t\t\tname:     \"different model names\",\n\t\t\tinput:    \"smollm2\",\n\t\t\ttag:      \"docker.io/ai/gemma3:latest\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"partial name should not match\",\n\t\t\tinput:    \"smoll\",\n\t\t\ttag:      \"docker.io/ai/smollm2:latest\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"different registry\",\n\t\t\tinput:    \"ollama/smollm2\",\n\t\t\ttag:      \"docker.io/ai/smollm2:latest\",\n\t\t\texpected: false,\n\t\t},\n\n\t\t// Edge cases\n\t\t{\n\t\t\tname:     \"gemma3 short name\",\n\t\t\tinput:    \"gemma3\",\n\t\t\ttag:      \"docker.io/ai/gemma3:latest\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"ai/gemma3\",\n\t\t\tinput:    \"ai/gemma3\",\n\t\t\ttag:      \"docker.io/ai/gemma3:latest\",\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := matchesModel(tt.input, tt.tag)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"matchesModel(%q, %q) = %v, want %v\", tt.input, tt.tag, result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestResolveModelNameWithMockData(t *testing.T) {\n\t// Test the resolution logic by testing matchesModel with various inputs\n\t// against a set of mock tags that would come from docker model list\n\n\tmockTags := []string{\n\t\t\"docker.io/ai/smollm2:latest\",\n\t\t\"huggingface.co/tinyllama/tinyllama-1.1b-chat-v1.0:latest\",\n\t\t\"docker.io/ai/gemma3:latest\",\n\t}\n\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\tshouldMatch string // empty if no match expected\n\t}{\n\t\t{\n\t\t\tname:        \"smollm2 resolves to full tag\",\n\t\t\tinput:       \"smollm2\",\n\t\t\tshouldMatch: \"docker.io/ai/smollm2:latest\",\n\t\t},\n\t\t{\n\t\t\tname:        \"ai/smollm2 resolves to full tag\",\n\t\t\tinput:       \"ai/smollm2\",\n\t\t\tshouldMatch: \"docker.io/ai/smollm2:latest\",\n\t\t},\n\t\t{\n\t\t\tname:        \"gemma3 resolves to full tag\",\n\t\t\tinput:       \"gemma3\",\n\t\t\tshouldMatch: \"docker.io/ai/gemma3:latest\",\n\t\t},\n\t\t{\n\t\t\tname:        \"tinyllama model resolves\",\n\t\t\tinput:       \"tinyllama/tinyllama-1.1b-chat-v1.0\",\n\t\t\tshouldMatch: \"huggingface.co/tinyllama/tinyllama-1.1b-chat-v1.0:latest\",\n\t\t},\n\t\t{\n\t\t\tname:        \"hf.co prefix resolves\",\n\t\t\tinput:       \"hf.co/tinyllama/tinyllama-1.1b-chat-v1.0\",\n\t\t\tshouldMatch: \"huggingface.co/tinyllama/tinyllama-1.1b-chat-v1.0:latest\",\n\t\t},\n\t\t{\n\t\t\tname:        \"unknown model returns no match\",\n\t\t\tinput:       \"unknown-model\",\n\t\t\tshouldMatch: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar matched string\n\t\t\tfor _, tag := range mockTags {\n\t\t\t\tif matchesModel(tt.input, tag) {\n\t\t\t\t\tmatched = tag\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif matched != tt.shouldMatch {\n\t\t\t\tt.Errorf(\"resolving %q: got %q, want %q\", tt.input, matched, tt.shouldMatch)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "scripts/build_vmnet.sh",
    "content": "#!/usr/bin/env sh\n\nset -ex\n\nexport DIR_BUILD=$PWD/_build/network\nexport DIR_VMNET=$DIR_BUILD/socket_vmnet\nexport EMBED_DIR=$PWD/embedded/network\n\nclone() (\n    if [ ! -d \"$2\" ]; then\n        git clone \"$1\" \"$2\"\n    fi\n)\n\nmkdir -p \"$DIR_BUILD\"\nclone https://github.com/lima-vm/socket_vmnet.git \"$DIR_VMNET\"\n\nmove_to_embed_dir() (\n    mkdir -p \"$EMBED_DIR\"/vmnet/bin\n    cp \"$DIR_VMNET\"/socket_vmnet \"$DIR_VMNET\"/socket_vmnet_client \"$EMBED_DIR\"/vmnet/bin\n    cd \"$EMBED_DIR\"/vmnet && tar cvfz \"$EMBED_DIR\"/vmnet_\"${1}\".tar.gz bin/socket_vmnet bin/socket_vmnet_client\n    rm -rf \"$EMBED_DIR\"/vmnet\n)\n\nbuild_x86_64() (\n    cd \"$DIR_VMNET\"\n\n    # pinning to a commit for consistency\n    git checkout v1.1.5\n    make ARCH=x86_64\n\n    move_to_embed_dir x86_64\n\n    # cleanup\n    make clean\n)\n\nbuild_arm64() (\n    cd \"$DIR_VMNET\"\n\n    # pinning to a commit for consistency\n    git checkout v1.1.5\n    make ARCH=arm64\n    move_to_embed_dir arm64\n\n    # cleanup\n    make clean\n)\n\ntest_archives() (\n    TEMP_DIR=/tmp/colima-test-archives\n    rm -rf $TEMP_DIR\n    mkdir -p $TEMP_DIR/x86 $TEMP_DIR/arm\n    (\n        cp \"$EMBED_DIR\"/vmnet_x86_64.tar.gz $TEMP_DIR/x86\n        cd $TEMP_DIR/x86 && tar xvfz vmnet_x86_64.tar.gz\n    )\n    (\n        cp \"$EMBED_DIR\"/vmnet_arm64.tar.gz $TEMP_DIR/arm\n        cd $TEMP_DIR/arm && tar xvfz vmnet_arm64.tar.gz\n    )\n\n    assert_not_equal() (\n        if diff $TEMP_DIR/x86/\"$1\" $TEMP_DIR/arm/\"$1\"; then\n            echo \"$1\" is same for both arch\n            exit 1\n        fi\n    )\n\n    assert_not_equal bin/socket_vmnet\n    assert_not_equal bin/socket_vmnet_client\n)\n\nbuild_x86_64\nbuild_arm64\ntest_archives\n"
  },
  {
    "path": "scripts/integration.sh",
    "content": "#!/usr/bin/env bash\n\nset -ex\n\nalias colima=\"$COLIMA_BINARY\"\nDOCKER_CONTEXT=\"$(docker info -f '{{json .}}' | jq -r '.ClientInfo.Context')\"\n\nOTHER_ARCH=\"amd64\"\nif [ \"$GOARCH\" == \"amd64\" ]; then\n    OTHER_ARCH=\"arm64\"\nfi\n\nstage() (\n    set +x\n    echo\n    echo \"######################################\"\n    echo \"$@\"\n    echo \"######################################\"\n    echo\n    set -x\n)\n\ntest_runtime() (\n    stage \"runtime: $2, arch: $1\"\n\n    NAME=\"itest-$2\"\n    COLIMA=\"$COLIMA_BINARY -p $NAME\"\n\n    COMMAND=\"docker\"\n    if [ \"$2\" == \"containerd\" ]; then\n       COMMAND=\"$COLIMA nerdctl --\" \n    fi\n\n    # reset\n    $COLIMA delete -f\n\n    # start\n    $COLIMA start --arch \"$1\" --runtime \"$2\"\n\n    # validate\n    $COMMAND ps && $COMMAND info\n\n    # validate DNS\n    $COLIMA ssh -- nslookup host.docker.internal\n\n    # valid building image\n    $COMMAND build integration\n\n    # teardown\n    $COLIMA delete -f \n)\n\ntest_kubernetes() (\n    stage \"k8s runtime: $2, arch: $1\"\n\n    NAME=\"itest-$2-k8s\"\n    COLIMA=\"$COLIMA_BINARY -p $NAME\"\n\n    # reset\n    $COLIMA delete -f\n\n    # start\n    $COLIMA start --arch \"$1\" --runtime \"$2\" --kubernetes\n\n    # short delay\n    sleep 5\n\n    # validate\n    kubectl cluster-info && kubectl version && kubectl get nodes -o wide\n\n    # teardown\n    $COLIMA delete -f\n)\n\ntest_runtime $GOARCH docker\ntest_runtime $GOARCH containerd\ntest_kubernetes $GOARCH docker\ntest_kubernetes $GOARCH containerd\ntest_runtime $OTHER_ARCH docker\ntest_runtime $OTHER_ARCH containerd\n\nif [ -n \"$DOCKER_CONTEXT\" ]; then\n    docker context use \"$DOCKER_CONTEXT\" || echo # prevent error\nfi\n"
  },
  {
    "path": "shell.nix",
    "content": "{ pkgs ? import <nixpkgs> { } }:\n\npkgs.mkShell {\n  # nativeBuildInputs is usually what you want -- tools you need to run\n  nativeBuildInputs = with pkgs.buildPackages; [\n    go_1_23\n    gotools\n    git\n    lima\n    qemu\n  ];\n  shellHook = ''\n    echo Nix Shell with $(go version)\n    echo\n\n    COLIMA_BIN=\"$PWD/$(make print-binary-name)\"\n    if [ ! -f \"$COLIMA_BIN\" ]; then\n        echo \"Run 'make' to build Colima.\"\n        echo\n    fi\n\n    set -x\n    set -x\n    alias colima=\"$COLIMA_BIN\"\n    set +x\n  '';\n}\n"
  },
  {
    "path": "store/store.go",
    "content": "package store\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// Store stores internal Colima configuration for an instance\ntype Store struct {\n\t// if the runtime disk has been formatted.\n\tDiskFormatted bool `json:\"disk_formatted\"`\n\t// the container runtime the disk is provisioned for\n\tDiskRuntime string `json:\"disk_runtime\"`\n\t// if ramalama has been provisioned in the VM\n\tRamalamaProvisioned bool `json:\"ramalama_provisioned\"`\n}\n\nfunc storeFile() string { return config.CurrentProfile().StoreFile() }\n\n// Load loads the store from the json file.\nfunc Load() (s Store, err error) {\n\tb, err := os.ReadFile(storeFile())\n\tif err != nil {\n\t\treturn s, fmt.Errorf(\"cannot read store file: %w\", err)\n\t}\n\n\tif err := json.Unmarshal(b, &s); err != nil {\n\t\treturn s, fmt.Errorf(\"error unmarshaling store file: %w\", err)\n\t}\n\n\treturn s, nil\n}\n\n// save persists the store.\nfunc save(s Store) error {\n\tb, err := json.MarshalIndent(s, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error marshaling store: %w\", err)\n\t}\n\n\tif err := os.WriteFile(storeFile(), b, 0o644); err != nil {\n\t\treturn fmt.Errorf(\"error writing store file: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Set provides an easy way to set a value in the store.\nfunc Set(f func(*Store)) error {\n\ts, err := Load()\n\tif err != nil {\n\t\tlogrus.Debug(\"error loading store: %w\", err)\n\t}\n\n\tf(&s)\n\n\tif err := save(s); err != nil {\n\t\treturn fmt.Errorf(\"error saving store: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Reset resets the values in the store to the defaults.\nfunc Reset() error {\n\t// first attempt to remove store file\n\tif err := os.Remove(storeFile()); err != nil {\n\t\t// if it fails\n\t\t// then attempt to set it to empty value\n\t\treturn Set(func(s *Store) { *s = Store{} })\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "util/debutil/debutil.go",
    "content": "package debutil\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/abiosoft/colima/environment\"\n)\n\n// packages is list of deb package names.\ntype packages []string\n\n// Upgradable returns the shell command to check if the packages are upgradable with apt.\n// The returned command should be passed to 'sh -c' or equivalent.\nfunc (p packages) Upgradable() string {\n\tcmd := \"sudo apt list --upgradable | grep\"\n\tfor _, v := range p {\n\t\tcmd += fmt.Sprintf(\" -e '^%s/'\", v)\n\t}\n\treturn cmd\n}\n\n// Install returns the shell command to install the packages with apt.\n// The returned command should be passed to 'sh -c' or equivalent.\nfunc (p packages) Install() string {\n\treturn \"sudo apt-get install -y --allow-change-held-packages \" + strings.Join(p, \" \")\n}\n\nfunc UpdateRuntime(\n\tctx context.Context,\n\tguest environment.GuestActions,\n\tchain cli.CommandChain,\n\tpackageNames ...string,\n) (bool, error) {\n\ta := chain.Init(ctx)\n\tlog := a.Logger()\n\n\tpackages := packages(packageNames)\n\n\thasUpdates := false\n\tupdated := false\n\n\ta.Stage(\"refreshing package manager\")\n\ta.Add(func() error {\n\t\treturn guest.RunQuiet(\n\t\t\t\"sh\",\n\t\t\t\"-c\",\n\t\t\t\"sudo apt-get update -y\",\n\t\t)\n\t})\n\n\ta.Stage(\"checking for updates\")\n\ta.Add(func() error {\n\t\terr := guest.RunQuiet(\n\t\t\t\"sh\",\n\t\t\t\"-c\",\n\t\t\tpackages.Upgradable(),\n\t\t)\n\t\thasUpdates = err == nil\n\t\treturn nil\n\t})\n\n\ta.Add(func() (err error) {\n\t\tif !hasUpdates {\n\t\t\tlog.Warnln(\"no updates available\")\n\t\t\treturn\n\t\t}\n\n\t\tlog.Println(\"updating packages ...\")\n\t\terr = guest.RunQuiet(\n\t\t\t\"sh\",\n\t\t\t\"-c\",\n\t\t\tpackages.Install(),\n\t\t)\n\t\tif err == nil {\n\t\t\tupdated = true\n\t\t\tlog.Println(\"done\")\n\t\t}\n\t\treturn\n\t})\n\n\t// it is necessary to execute the chain here to get the correct value for `updated`.\n\terr := a.Exec()\n\treturn updated, err\n}\n"
  },
  {
    "path": "util/downloader/curl.go",
    "content": "package downloader\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/util/terminal\"\n)\n\nconst (\n\t// DownloaderNative uses Go's native HTTP client\n\tDownloaderNative = \"native\"\n\t// DownloaderCurl uses the curl command (honors .curlrc)\n\tDownloaderCurl = \"curl\"\n\n\tenvDownloader = \"COLIMA_DOWNLOADER\"\n)\n\n// ValidateDownloader validates the downloader value (case-insensitive).\n// Returns the normalized value or an error if invalid.\nfunc ValidateDownloader(v string) (string, error) {\n\tswitch strings.ToLower(v) {\n\tcase DownloaderNative:\n\t\treturn DownloaderNative, nil\n\tcase DownloaderCurl:\n\t\treturn DownloaderCurl, nil\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"invalid downloader %q: must be one of %s, %s\", v, DownloaderNative, DownloaderCurl)\n\t}\n}\n\n// curlDownloader handles downloads using the curl command\ntype curlDownloader struct{}\n\n// Download downloads a file using curl\nfunc (c *curlDownloader) Download(r Request, destPath string) error {\n\t// check if curl is available\n\tif _, err := exec.LookPath(\"curl\"); err != nil {\n\t\treturn fmt.Errorf(\"curl not found in PATH: %w\", err)\n\t}\n\n\targs := []string{\n\t\t\"-fSL\",    // fail on HTTP errors, show errors, follow redirects\n\t\t\"-C\", \"-\", // resume if possible (auto-detect offset)\n\t\t\"--progress-bar\", // show progress bar\n\t\t\"-o\", destPath,   // output file\n\t\tr.URL,\n\t}\n\n\tcmd := exec.Command(\"curl\", args...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn fmt.Errorf(\"curl download failed for '%s': %w\", path.Base(r.URL), err)\n\t}\n\n\tterminal.ClearLine()\n\treturn nil\n}\n"
  },
  {
    "path": "util/downloader/download.go",
    "content": "package downloader\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/environment\"\n\t\"github.com/abiosoft/colima/util/osutil\"\n\t\"github.com/abiosoft/colima/util/shautil\"\n)\n\ntype (\n\thostActions  = environment.HostActions\n\tguestActions = environment.GuestActions\n)\n\n// Request is download request\ntype Request struct {\n\tURL string // request URL\n\tSHA *SHA   // shasum url\n}\n\n// FileDownloader is the interface for downloading files\ntype FileDownloader interface {\n\tDownload(r Request, destPath string) error\n}\n\n// fileDownloader is the configured downloader implementation\nvar fileDownloader FileDownloader = &nativeDownloader{}\n\n// SetDownloader sets the downloader implementation based on the provided type.\n// The value should be validated before calling this function.\nfunc SetDownloader(v string) {\n\tif v == DownloaderCurl {\n\t\tfileDownloader = &curlDownloader{}\n\t} else {\n\t\tfileDownloader = &nativeDownloader{}\n\t}\n}\n\nfunc init() {\n\t// check environment variable for default downloader\n\tif v := osutil.EnvVar(envDownloader).Val(); v != \"\" {\n\t\tif d, err := ValidateDownloader(v); err == nil {\n\t\t\tSetDownloader(d)\n\t\t}\n\t}\n}\n\n// DownloadToGuest downloads file at url and saves it in the destination.\n//\n// In the implementation, the file is downloaded (and cached) on the host,\n// then copied to the guest using limactl copy.\n// filename must be an absolute path and a directory on the guest that does not require root access.\nfunc DownloadToGuest(host hostActions, guest guestActions, r Request, filename string) error {\n\t// if file is on the filesystem, no need for download. A copy suffices\n\tif strings.HasPrefix(r.URL, \"/\") {\n\t\treturn CopyToGuest(host, r.URL, filename)\n\t}\n\n\tcacheFile, err := Download(host, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn CopyToGuest(host, cacheFile, filename)\n}\n\n// CopyToGuest copies a file or directory from the host to the guest VM using limactl copy.\nfunc CopyToGuest(host hostActions, src, dest string) error {\n\tinstanceName := config.CurrentProfile().ID\n\treturn host.RunQuiet(\"limactl\", \"copy\", \"-r\", src, instanceName+\":\"+dest)\n}\n\n// Download downloads file at url and returns the location of the downloaded file.\nfunc Download(host hostActions, r Request) (string, error) {\n\td := downloader{}\n\n\tif !d.hasCache(r.URL) {\n\t\tif err := d.downloadFile(r); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\treturn CacheFilename(r.URL), nil\n}\n\ntype downloader struct{}\n\n// CacheFilename returns the computed filename for the url.\nfunc CacheFilename(url string) string {\n\treturn filepath.Join(config.CacheDir(), \"caches\", shautil.SHA256(url).String())\n}\n\nfunc (d downloader) cacheDownloadingFileName(url string) string {\n\treturn CacheFilename(url) + \".downloading\"\n}\n\nfunc (d downloader) resumeInfoPath(url string) string {\n\treturn CacheFilename(url) + \".resume\"\n}\n\nfunc (d downloader) downloadFile(r Request) (err error) {\n\tcacheDownloadingFilename := d.cacheDownloadingFileName(r.URL)\n\n\t// create cache directory\n\tcacheDir := filepath.Dir(cacheDownloadingFilename)\n\tif err := os.MkdirAll(cacheDir, 0755); err != nil {\n\t\treturn fmt.Errorf(\"error preparing cache dir: %w\", err)\n\t}\n\n\tif err := fileDownloader.Download(r, cacheDownloadingFilename); err != nil {\n\t\treturn err\n\t}\n\n\t// validate download if SHA is present\n\tif r.SHA != nil {\n\t\tif err := r.SHA.validateDownload(r.URL, cacheDownloadingFilename); err != nil {\n\t\t\t// move file to allow subsequent re-download\n\t\t\t_ = os.Rename(cacheDownloadingFilename, cacheDownloadingFilename+\".invalid\")\n\t\t\treturn fmt.Errorf(\"error validating SHA sum for '%s': %w\", path.Base(r.URL), err)\n\t\t}\n\t}\n\n\t// move completed download to final location\n\tif err := os.Rename(cacheDownloadingFilename, CacheFilename(r.URL)); err != nil {\n\t\treturn fmt.Errorf(\"error finalizing download: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d downloader) saveResumeInfo(url, etag string, bytesWritten int64) {\n\tinfo := ResumeInfo{ETag: etag, BytesWritten: bytesWritten}\n\tdata, _ := json.Marshal(info)\n\t_ = os.WriteFile(d.resumeInfoPath(url), data, 0644)\n}\n\nfunc (d downloader) hasCache(url string) bool {\n\t_, err := os.Stat(CacheFilename(url))\n\treturn err == nil\n}\n"
  },
  {
    "path": "util/downloader/errors.go",
    "content": "package downloader\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"path\"\n\t\"syscall\"\n)\n\n// Sentinel errors for type checking\nvar (\n\tErrNetworkConnection = errors.New(\"network connection error\")\n\tErrHTTPStatus        = errors.New(\"HTTP error\")\n\tErrResumeFailed      = errors.New(\"resume failed\")\n\tErrSHAValidation     = errors.New(\"SHA validation failed\")\n)\n\n// NetworkError wraps network-related errors with user-friendly messages\ntype NetworkError struct {\n\tOp  string // \"connect\", \"resolve\", \"download\"\n\tURL string\n\tErr error\n}\n\nfunc (e *NetworkError) Error() string {\n\treturn fmt.Sprintf(\"%s failed for '%s': %s\", e.Op, e.URL, e.friendlyMessage())\n}\n\nfunc (e *NetworkError) Unwrap() error {\n\treturn e.Err\n}\n\nfunc (e *NetworkError) friendlyMessage() string {\n\t// check for DNS resolution errors\n\tvar dnsErr *net.DNSError\n\tif errors.As(e.Err, &dnsErr) {\n\t\treturn fmt.Sprintf(\"DNS lookup failed for host '%s'. Check your network connection or DNS settings\", dnsErr.Name)\n\t}\n\n\t// check for connection refused\n\tvar opErr *net.OpError\n\tif errors.As(e.Err, &opErr) {\n\t\tif errors.Is(opErr.Err, syscall.ECONNREFUSED) {\n\t\t\treturn \"connection refused. The server may be down or unreachable\"\n\t\t}\n\t\tif opErr.Timeout() {\n\t\t\treturn \"connection timed out. Check your network connection or try again later\"\n\t\t}\n\t}\n\n\t// check for URL parsing errors\n\tvar urlErr *url.Error\n\tif errors.As(e.Err, &urlErr) {\n\t\tif urlErr.Timeout() {\n\t\t\treturn \"request timed out. The server may be slow or overloaded\"\n\t\t}\n\t}\n\n\treturn e.Err.Error()\n}\n\n// HTTPStatusError represents HTTP error responses\ntype HTTPStatusError struct {\n\tStatusCode int\n\tStatus     string\n\tURL        string\n}\n\nfunc (e *HTTPStatusError) Error() string {\n\tswitch e.StatusCode {\n\tcase 404:\n\t\treturn fmt.Sprintf(\"file not found at '%s'. The URL may be incorrect or the file may have been removed\", e.URL)\n\tcase 403:\n\t\treturn fmt.Sprintf(\"access forbidden to '%s'. You may need authentication or the resource is restricted\", e.URL)\n\tcase 401:\n\t\treturn fmt.Sprintf(\"authentication required for '%s'\", e.URL)\n\tcase 500, 502, 503, 504:\n\t\treturn fmt.Sprintf(\"server error (%d) at '%s'. Try again later\", e.StatusCode, e.URL)\n\tcase 416: // Range Not Satisfiable\n\t\treturn fmt.Sprintf(\"resume failed for '%s'. The server does not support the requested byte range\", e.URL)\n\tdefault:\n\t\treturn fmt.Sprintf(\"HTTP %d (%s) for '%s'\", e.StatusCode, e.Status, e.URL)\n\t}\n}\n\nfunc (e *HTTPStatusError) Unwrap() error {\n\treturn ErrHTTPStatus\n}\n\n// ResumeError indicates a failed resume attempt\ntype ResumeError struct {\n\tReason string\n\tURL    string\n}\n\nfunc (e *ResumeError) Error() string {\n\treturn fmt.Sprintf(\"cannot resume download of '%s': %s. Starting fresh download\", path.Base(e.URL), e.Reason)\n}\n\nfunc (e *ResumeError) Unwrap() error {\n\treturn ErrResumeFailed\n}\n\n// SHAValidationError indicates checksum mismatch\ntype SHAValidationError struct {\n\tFile     string\n\tExpected string\n\tActual   string\n\tSize     int // 256 or 512\n}\n\nfunc (e *SHAValidationError) Error() string {\n\treturn 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\",\n\t\te.Size, e.File, e.Expected, e.Actual)\n}\n\nfunc (e *SHAValidationError) Unwrap() error {\n\treturn ErrSHAValidation\n}\n"
  },
  {
    "path": "util/downloader/http.go",
    "content": "package downloader\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/schollz/progressbar/v3\"\n\t\"golang.org/x/term\"\n)\n\n// HTTPClient encapsulates HTTP download operations\ntype HTTPClient struct {\n\tclient    *http.Client\n\tuserAgent string\n}\n\n// DownloadOptions configures a download operation\ntype DownloadOptions struct {\n\tURL            string\n\tDestPath       string\n\tExpectedETag   string // for resume validation\n\tResumeFromByte int64  // byte offset to resume from\n\tShowProgress   bool\n}\n\n// DownloadResult contains metadata about the completed download\ntype DownloadResult struct {\n\tFinalURL   string // After following redirects\n\tETag       string // For future resume validation\n\tTotalBytes int64\n\tWasResumed bool\n}\n\n// ResumeInfo stores metadata for resumable downloads\ntype ResumeInfo struct {\n\tETag         string `json:\"etag\"`\n\tBytesWritten int64  `json:\"bytes_written\"`\n}\n\n// NewHTTPClient creates a configured HTTP client\nfunc NewHTTPClient() *HTTPClient {\n\ttransport := &http.Transport{\n\t\t// use proxy from environment (HTTP_PROXY, HTTPS_PROXY, NO_PROXY)\n\t\tProxy: http.ProxyFromEnvironment,\n\t\tDialContext: (&net.Dialer{\n\t\t\tTimeout:   30 * time.Second,\n\t\t\tKeepAlive: 30 * time.Second,\n\t\t}).DialContext,\n\t\tTLSHandshakeTimeout:   10 * time.Second,\n\t\tResponseHeaderTimeout: 30 * time.Second,\n\t\tExpectContinueTimeout: 1 * time.Second,\n\t}\n\n\treturn &HTTPClient{\n\t\tclient: &http.Client{\n\t\t\tTransport: transport,\n\t\t\t// checkRedirect is left default - Go follows up to 10 redirects\n\t\t\t// and returns the final response\n\t\t},\n\t\tuserAgent: \"colima/\" + config.AppVersion().Version,\n\t}\n}\n\n// GetFinalURL follows redirects and returns the final URL\nfunc (h *HTTPClient) GetFinalURL(ctx context.Context, rawURL string) (string, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodHead, rawURL, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"invalid URL '%s': %w\", rawURL, err)\n\t}\n\treq.Header.Set(\"User-Agent\", h.userAgent)\n\n\tresp, err := h.client.Do(req)\n\tif err != nil {\n\t\treturn \"\", &NetworkError{Op: \"resolve redirect\", URL: rawURL, Err: err}\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\t// check for HTTP errors\n\tif resp.StatusCode >= 400 {\n\t\treturn \"\", &HTTPStatusError{\n\t\t\tStatusCode: resp.StatusCode,\n\t\t\tStatus:     resp.Status,\n\t\t\tURL:        rawURL,\n\t\t}\n\t}\n\n\t// resp.Request.URL contains the final URL after redirects\n\treturn resp.Request.URL.String(), nil\n}\n\n// Download performs a file download with optional resume support\nfunc (h *HTTPClient) Download(ctx context.Context, opts DownloadOptions) (*DownloadResult, error) {\n\tresult := &DownloadResult{}\n\n\t// open destination file for writing (or appending if resuming)\n\tvar file *os.File\n\tvar existingSize int64\n\tvar err error\n\n\tif opts.ResumeFromByte > 0 {\n\t\tfile, err = os.OpenFile(opts.DestPath, os.O_WRONLY|os.O_APPEND, 0644)\n\t\tif err != nil {\n\t\t\t// can't resume, start fresh\n\t\t\topts.ResumeFromByte = 0\n\t\t\topts.ExpectedETag = \"\"\n\t\t} else {\n\t\t\texistingSize = opts.ResumeFromByte\n\t\t}\n\t}\n\n\tif file == nil {\n\t\tfile, err = os.Create(opts.DestPath)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"cannot create file '%s': %w\", opts.DestPath, err)\n\t\t}\n\t}\n\tdefer func() { _ = file.Close() }()\n\n\t// build request\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, opts.URL, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid URL '%s': %w\", opts.URL, err)\n\t}\n\treq.Header.Set(\"User-Agent\", h.userAgent)\n\n\t// add Range header for resume\n\tif existingSize > 0 {\n\t\treq.Header.Set(\"Range\", fmt.Sprintf(\"bytes=%d-\", existingSize))\n\t\t// add If-Range with ETag if available for safe resume\n\t\tif opts.ExpectedETag != \"\" {\n\t\t\treq.Header.Set(\"If-Range\", opts.ExpectedETag)\n\t\t}\n\t}\n\n\t// execute request\n\tresp, err := h.client.Do(req)\n\tif err != nil {\n\t\treturn nil, &NetworkError{Op: \"download\", URL: opts.URL, Err: err}\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\t// store final URL after redirects\n\tresult.FinalURL = resp.Request.URL.String()\n\tresult.ETag = resp.Header.Get(\"ETag\")\n\n\t// handle response status\n\tswitch resp.StatusCode {\n\tcase http.StatusOK: // 200 - Full content (resume not supported or If-Range failed)\n\t\tif existingSize > 0 {\n\t\t\t// server sent full content, need to truncate and start over\n\t\t\tif err := file.Truncate(0); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"cannot truncate file for fresh download: %w\", err)\n\t\t\t}\n\t\t\tif _, err := file.Seek(0, 0); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"cannot seek to start of file: %w\", err)\n\t\t\t}\n\t\t\texistingSize = 0\n\t\t}\n\t\tresult.TotalBytes = resp.ContentLength\n\n\tcase http.StatusPartialContent: // 206 - Resume successful\n\t\tresult.WasResumed = true\n\t\t// Content-Range: bytes 21010-47021/47022\n\t\tcontentRange := resp.Header.Get(\"Content-Range\")\n\t\tif totalSize := parseContentRangeTotal(contentRange); totalSize > 0 {\n\t\t\tresult.TotalBytes = totalSize\n\t\t} else {\n\t\t\tresult.TotalBytes = existingSize + resp.ContentLength\n\t\t}\n\n\tcase http.StatusRequestedRangeNotSatisfiable: // 416\n\t\t// file is likely complete or server doesn't support range\n\t\treturn nil, &HTTPStatusError{\n\t\t\tStatusCode: resp.StatusCode,\n\t\t\tStatus:     resp.Status,\n\t\t\tURL:        opts.URL,\n\t\t}\n\n\tdefault:\n\t\tif resp.StatusCode >= 400 {\n\t\t\treturn nil, &HTTPStatusError{\n\t\t\t\tStatusCode: resp.StatusCode,\n\t\t\t\tStatus:     resp.Status,\n\t\t\t\tURL:        opts.URL,\n\t\t\t}\n\t\t}\n\t}\n\n\t// set up progress bar\n\tvar writer io.Writer = file\n\tvar bar *progressbar.ProgressBar\n\tif opts.ShowProgress && isTerminal() {\n\t\tbar = h.createProgressBar(result.TotalBytes, existingSize)\n\t\twriter = io.MultiWriter(file, bar)\n\t}\n\n\t// stream response body to file\n\twritten, err := io.Copy(writer, resp.Body)\n\tif err != nil {\n\t\treturn result, &NetworkError{Op: \"download\", URL: opts.URL, Err: err}\n\t}\n\n\t// finish progress bar\n\tif bar != nil {\n\t\t_ = bar.Finish()\n\t}\n\n\tresult.TotalBytes = existingSize + written\n\treturn result, nil\n}\n\n// Fetch downloads content from a URL and returns it as bytes (for small files like SHA checksums)\nfunc (h *HTTPClient) Fetch(ctx context.Context, url string) ([]byte, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid URL '%s': %w\", url, err)\n\t}\n\treq.Header.Set(\"User-Agent\", h.userAgent)\n\n\tresp, err := h.client.Do(req)\n\tif err != nil {\n\t\treturn nil, &NetworkError{Op: \"fetch\", URL: url, Err: err}\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, &HTTPStatusError{\n\t\t\tStatusCode: resp.StatusCode,\n\t\t\tStatus:     resp.Status,\n\t\t\tURL:        url,\n\t\t}\n\t}\n\n\t// limit read to 1MB for safety (SHA files should be tiny)\n\treturn io.ReadAll(io.LimitReader(resp.Body, 1<<20))\n}\n\n// createProgressBar creates a progress bar for download visualization\nfunc (h *HTTPClient) createProgressBar(totalBytes, startOffset int64) *progressbar.ProgressBar {\n\topts := []progressbar.Option{\n\t\tprogressbar.OptionSetDescription(\"    \"),\n\t\tprogressbar.OptionSetWriter(os.Stderr),\n\t\tprogressbar.OptionShowBytes(true),\n\t\tprogressbar.OptionSetWidth(30),\n\t\tprogressbar.OptionThrottle(100 * time.Millisecond),\n\t\tprogressbar.OptionClearOnFinish(),\n\t\tprogressbar.OptionSetPredictTime(true),\n\t\tprogressbar.OptionSetRenderBlankState(true),\n\t\tprogressbar.OptionSetTheme(progressbar.Theme{\n\t\t\tSaucer:        \"=\",\n\t\t\tSaucerHead:    \">\",\n\t\t\tSaucerPadding: \" \",\n\t\t\tBarStart:      \"[\",\n\t\t\tBarEnd:        \"]\",\n\t\t}),\n\t}\n\n\t// if total size is unknown, use a spinner\n\tif totalBytes <= 0 {\n\t\topts = append(opts, progressbar.OptionSpinnerType(11))\n\t\ttotalBytes = -1\n\t}\n\n\tbar := progressbar.NewOptions64(totalBytes, opts...)\n\n\t// if resuming, set initial progress\n\tif startOffset > 0 {\n\t\t_ = bar.Set64(startOffset)\n\t}\n\n\treturn bar\n}\n\n// parseContentRangeTotal extracts total size from Content-Range header\n// Format: \"bytes 21010-47021/47022\" or \"bytes 21010-47021/*\"\nfunc parseContentRangeTotal(header string) int64 {\n\tif header == \"\" {\n\t\treturn -1\n\t}\n\tparts := strings.Split(header, \"/\")\n\tif len(parts) != 2 || parts[1] == \"*\" {\n\t\treturn -1\n\t}\n\ttotal, err := strconv.ParseInt(parts[1], 10, 64)\n\tif err != nil {\n\t\treturn -1\n\t}\n\treturn total\n}\n\n// isTerminal returns true if stderr is a terminal\nfunc isTerminal() bool {\n\treturn term.IsTerminal(int(os.Stderr.Fd()))\n}\n"
  },
  {
    "path": "util/downloader/native.go",
    "content": "package downloader\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"time\"\n)\n\n// nativeDownloader uses Go's native HTTP client\ntype nativeDownloader struct{}\n\n// Download downloads a file using Go's native HTTP client\nfunc (n *nativeDownloader) Download(r Request, destPath string) error {\n\td := downloader{}\n\t// check for existing partial download and resume info\n\tvar resumeInfo ResumeInfo\n\tresumeInfoPath := d.resumeInfoPath(r.URL)\n\tif data, err := os.ReadFile(resumeInfoPath); err == nil {\n\t\t_ = json.Unmarshal(data, &resumeInfo)\n\t}\n\n\t// get existing file size for resume\n\tvar existingSize int64\n\tif stat, err := os.Stat(destPath); err == nil {\n\t\texistingSize = stat.Size()\n\t}\n\n\t// create HTTP client\n\tclient := NewHTTPClient()\n\n\t// use a long timeout for large files (2 hours)\n\tctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)\n\tdefer cancel()\n\n\t// get final URL (follows redirects)\n\tfinalURL, err := client.GetFinalURL(ctx, r.URL)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error resolving download URL '%s': %w\", r.URL, err)\n\t}\n\n\t// download the file\n\tresult, err := client.Download(ctx, DownloadOptions{\n\t\tURL:            finalURL,\n\t\tDestPath:       destPath,\n\t\tExpectedETag:   resumeInfo.ETag,\n\t\tResumeFromByte: existingSize,\n\t\tShowProgress:   true,\n\t})\n\tif err != nil {\n\t\t// save resume info for next attempt if we have ETag\n\t\tif result != nil && result.ETag != \"\" {\n\t\t\td.saveResumeInfo(r.URL, result.ETag, existingSize)\n\t\t}\n\t\treturn fmt.Errorf(\"error downloading '%s': %w\", path.Base(r.URL), err)\n\t}\n\n\t// clean up resume info on successful download\n\t_ = os.Remove(resumeInfoPath)\n\n\treturn nil\n}\n"
  },
  {
    "path": "util/downloader/sha.go",
    "content": "package downloader\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n)\n\n// SHA is the shasum of a file.\ntype SHA struct {\n\tDigest string // shasum\n\tURL    string // url to download the shasum file (if Digest is empty)\n\tSize   int    // one of 256 or 512\n}\n\n// ValidateFile validates the SHA of the file.\n// The host parameter is kept for API compatibility but is not used.\nfunc (s SHA) ValidateFile(host hostActions, file string) error {\n\treturn s.validateFile(file)\n}\n\n// validateFile performs SHA validation using pure Go crypto.\nfunc (s SHA) validateFile(file string) error {\n\t// open the file\n\tf, err := os.Open(file)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"cannot open file for validation: %w\", err)\n\t}\n\tdefer func() { _ = f.Close() }()\n\n\t// select hash algorithm\n\tvar h hash.Hash\n\tswitch s.Size {\n\tcase 256:\n\t\th = sha256.New()\n\tcase 512:\n\t\th = sha512.New()\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported SHA size: %d (must be 256 or 512)\", s.Size)\n\t}\n\n\t// compute hash\n\tif _, err := io.Copy(h, f); err != nil {\n\t\treturn fmt.Errorf(\"error reading file for SHA validation: %w\", err)\n\t}\n\n\t// compare\n\tcomputed := fmt.Sprintf(\"%x\", h.Sum(nil))\n\texpected := strings.TrimPrefix(s.Digest, fmt.Sprintf(\"sha%d:\", s.Size))\n\texpected = strings.ToLower(strings.TrimSpace(expected))\n\n\tif computed != expected {\n\t\treturn &SHAValidationError{\n\t\t\tFile:     filepath.Base(file),\n\t\t\tExpected: expected,\n\t\t\tActual:   computed,\n\t\t\tSize:     s.Size,\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s SHA) validateDownload(url string, filename string) error {\n\tif s.URL == \"\" && s.Digest == \"\" {\n\t\treturn fmt.Errorf(\"error validating SHA: one of Digest or URL must be set\")\n\t}\n\n\t// fetch digest from URL if empty\n\tif s.Digest == \"\" {\n\t\t// retrieve the filename from the download url.\n\t\ttargetFilename := \"\"\n\t\tif url != \"\" {\n\t\t\tsplit := strings.Split(url, \"/\")\n\t\t\ttargetFilename = split[len(split)-1]\n\t\t}\n\n\t\tdigest, err := fetchSHAFromURL(s.URL, targetFilename)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ts.Digest = digest\n\t}\n\n\treturn s.validateFile(filename)\n}\n\n// fetchSHAFromURL fetches SHA checksum file and extracts digest for the target file\nfunc fetchSHAFromURL(shaURL, targetFilename string) (string, error) {\n\tctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n\tdefer cancel()\n\n\tclient := NewHTTPClient()\n\n\t// fetch SHA file content\n\tdata, err := client.Fetch(ctx, shaURL)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error downloading SHA file from '%s': %w\", shaURL, err)\n\t}\n\n\t// parse SHA file to find the matching entry\n\tdigest, err := parseSHAContent(data, targetFilename)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error parsing SHA file from '%s': %w\", shaURL, err)\n\t}\n\n\treturn digest, nil\n}\n\n// parseSHAContent reads SHA checksum content and extracts the digest for the target filename.\n// Supports formats:\n//   - GNU coreutils: \"<hash>  <filename>\" (two spaces)\n//   - BSD/binary mode: \"<hash> *<filename>\" (space + asterisk)\nfunc parseSHAContent(data []byte, targetFilename string) (string, error) {\n\tscanner := bufio.NewScanner(bytes.NewReader(data))\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\t// format: \"<hash>  <filename>\" (two spaces) or \"<hash> *<filename>\" (binary mode)\n\t\tparts := strings.Fields(line)\n\t\tif len(parts) >= 2 {\n\t\t\thash := parts[0]\n\t\t\tfilename := strings.TrimPrefix(parts[len(parts)-1], \"*\")\n\n\t\t\tif filename == targetFilename || strings.HasSuffix(filename, \"/\"+targetFilename) {\n\t\t\t\treturn hash, nil\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := scanner.Err(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn \"\", fmt.Errorf(\"no SHA entry found for '%s' in checksum file\", targetFilename)\n}\n"
  },
  {
    "path": "util/fsutil/fs.go",
    "content": "package fsutil\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"testing/fstest\"\n)\n\n// FS is the host filesystem implementation.\nvar FS FileSystem = DefaultFS{}\n\n// MkdirAll calls FS.MakedirAll\nfunc MkdirAll(path string, perm os.FileMode) error { return FS.MkdirAll(path, perm) }\n\n// Open calls FS.Open\nfunc Open(name string) (fs.File, error) { return FS.Open(name) }\n\n// FS is abstraction for filesystem.\ntype FileSystem interface {\n\tMkdirAll(path string, perm os.FileMode) error\n\tfs.FS\n}\n\nvar _ FileSystem = DefaultFS{}\nvar _ FileSystem = fakeFS{}\n\n// DefaultFS is the default OS implementation of FileSystem.\ntype DefaultFS struct{}\n\n// Open implements FS\nfunc (DefaultFS) Open(name string) (fs.File, error) { return os.Open(name) }\n\n// MkdirAll implements FS\nfunc (DefaultFS) MkdirAll(path string, perm fs.FileMode) error { return os.MkdirAll(path, perm) }\n\n// FakeFS is a mock FS. The following can be done in a test before usage.\n//\n//\tosutil.FS = osutil.FakeFS\nvar FakeFS FileSystem = fakeFS{}\n\ntype fakeFS struct{}\n\n// Open implements FileSystem\nfunc (fakeFS) Open(name string) (fs.File, error) {\n\treturn fstest.MapFS{name: &fstest.MapFile{\n\t\tData: []byte(\"fake file - \" + name),\n\t}}.Open(name)\n}\n\n// MkdirAll implements FileSystem\nfunc (fakeFS) MkdirAll(path string, perm fs.FileMode) error { return nil }\n"
  },
  {
    "path": "util/macos.go",
    "content": "package util\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/cli\"\n\t\"github.com/coreos/go-semver/semver\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// MacOS returns if the current OS is macOS.\nfunc MacOS() bool {\n\treturn runtime.GOOS == \"darwin\"\n}\n\n// MacOS13OrNewer returns if the current OS is macOS 13 or newer.\nfunc MacOS13OrNewerOnArm() bool {\n\treturn runtime.GOARCH == \"arm64\" && MacOS13OrNewer()\n}\n\n// MacOS13OrNewer returns if the current OS is macOS 13 or newer.\nfunc MacOS13OrNewer() bool { return minMacOSVersion(\"13.0.0\") }\n\n// MacOS15OrNewer returns if the current OS is macOS 15 or newer.\nfunc MacOS15OrNewer() bool { return minMacOSVersion(\"15.0.0\") }\n\n// MacOSNestedVirtualizationSupported returns if the current device supports nested virtualization.\nfunc MacOSNestedVirtualizationSupported() bool {\n\treturn IsMxOrNewer(3) && MacOS15OrNewer()\n}\n\nfunc minMacOSVersion(version string) bool {\n\tif !MacOS() {\n\t\treturn false\n\t}\n\tver, err := macOSProductVersion()\n\tif err != nil {\n\t\tlogrus.Warnln(fmt.Errorf(\"error retrieving macOS version: %w\", err))\n\t\treturn false\n\t}\n\n\tcver, err := semver.NewVersion(version)\n\tif err != nil {\n\t\tlogrus.Warnln(fmt.Errorf(\"error parsing version: %w\", err))\n\t\treturn false\n\t}\n\n\treturn cver.Compare(*ver) <= 0\n}\n\n// IsMxOrNewer returns true if the machine is Apple Silicon M{n} where n >= min\n// e.g. IsMxOrNewer(3) returns true for M3, M4, M5, ...\nfunc IsMxOrNewer(min int) bool {\n\tchip, err := chipDetector.GetChipType()\n\tif err != nil {\n\t\tlogrus.Trace(fmt.Errorf(\"error getting chip type: %w\", err))\n\t\treturn false\n\t}\n\tn, ok := parseMNumber(chip)\n\tif !ok {\n\t\treturn false\n\t}\n\treturn n >= min\n}\n\n// chipTypeDetector fetches the chip type string from the host.\ntype chipTypeDetector interface {\n\tGetChipType() (string, error)\n}\n\n// systemProfilerChipDetector is the production implementation that calls\n// `system_profiler -json SPHardwareDataType`.\ntype systemProfilerChipDetector struct{}\n\nfunc (d systemProfilerChipDetector) GetChipType() (string, error) {\n\tif !MacOS() {\n\t\treturn \"\", fmt.Errorf(\"not macOS\")\n\t}\n\tvar resp struct {\n\t\tSPHardwareDataType []struct {\n\t\t\tChipType string `json:\"chip_type\"`\n\t\t} `json:\"SPHardwareDataType\"`\n\t}\n\n\tvar buf bytes.Buffer\n\tcmd := cli.Command(\"system_profiler\", \"-json\", \"SPHardwareDataType\")\n\tcmd.Stdout = &buf\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"error retrieving chip version: %w\", err)\n\t}\n\n\tif err := json.NewDecoder(&buf).Decode(&resp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"error decoding system_profiler response: %w\", err)\n\t}\n\n\tif len(resp.SPHardwareDataType) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no SPHardwareDataType in response\")\n\t}\n\n\treturn resp.SPHardwareDataType[0].ChipType, nil\n}\n\n// chipDetector is the instance used by IsMxOrNewer. Tests can replace\n// this with a fake implementation.\nvar chipDetector chipTypeDetector = systemProfilerChipDetector{}\n\nvar mRe = regexp.MustCompile(`\\bM(\\d+)\\b`)\n\nfunc parseMNumber(s string) (int, bool) {\n\tif s == \"\" {\n\t\treturn 0, false\n\t}\n\tmatches := mRe.FindStringSubmatch(strings.ToUpper(s))\n\tif len(matches) < 2 {\n\t\treturn 0, false\n\t}\n\tn, err := strconv.Atoi(matches[1])\n\tif err != nil {\n\t\treturn 0, false\n\t}\n\treturn n, true\n}\n\n// RosettaRunning checks if Rosetta process is running.\nfunc RosettaRunning() bool {\n\tif !MacOS() {\n\t\treturn false\n\t}\n\tcmd := cli.Command(\"pgrep\", \"oahd\")\n\tcmd.Stderr = nil\n\tcmd.Stdout = nil\n\treturn cmd.Run() == nil\n}\n\n// macOSProductVersion returns the host's macOS version.\nfunc macOSProductVersion() (*semver.Version, error) {\n\tcmd := exec.Command(\"sw_vers\", \"-productVersion\")\n\t// output is like \"12.3.1\\n\"\n\tb, err := cmd.Output()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute %v: %w\", cmd.Args, err)\n\t}\n\tverTrimmed := strings.TrimSpace(string(b))\n\t// macOS 12.4 returns just \"12.4\\n\"\n\tfor strings.Count(verTrimmed, \".\") < 2 {\n\t\tverTrimmed += \".0\"\n\t}\n\tverSem, err := semver.NewVersion(verTrimmed)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse macOS version %q: %w\", verTrimmed, err)\n\t}\n\treturn verSem, nil\n}\n"
  },
  {
    "path": "util/macos_test.go",
    "content": "package util\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\ntype fakeDetector struct {\n\tv string\n\te error\n}\n\nfunc (f fakeDetector) GetChipType() (string, error) { return f.v, f.e }\n\nfunc TestParseMNumber(t *testing.T) {\n\tcases := []struct {\n\t\tin   string\n\t\twant int\n\t\tok   bool\n\t}{\n\t\t{\"M3\", 3, true},\n\t\t{\"APPLE M1\", 1, true},\n\t\t{\"M10 Pro\", 10, true},\n\t\t{\"apple m3 pro\", 3, true},\n\t\t{\"Apple M1\", 1, true},\n\t\t{\"No M here\", 0, false},\n\t\t{\"\", 0, false},\n\t\t{\"ARM64\", 0, false},\n\t}\n\n\tfor _, c := range cases {\n\t\tn, ok := parseMNumber(c.in)\n\t\tif ok != c.ok || n != c.want {\n\t\t\tt.Fatalf(\"parseMNumber(%q) = (%d, %v), want (%d, %v)\", c.in, n, ok, c.want, c.ok)\n\t\t}\n\t}\n}\n\nfunc TestIsMxOrNewer(t *testing.T) {\n\tcases := []struct {\n\t\tname    string\n\t\tchip    string\n\t\tchipErr error\n\t\tmin     int\n\t\twant    bool\n\t}{\n\t\t{\"m3 satisfies min=3\", \"Apple M3 Pro\", nil, 3, true},\n\t\t{\"m3 satisfies min=1\", \"Apple M3 Pro\", nil, 1, true},\n\t\t{\"m3 does not satisfy min=4\", \"Apple M3 Pro\", nil, 4, false},\n\t\t{\"m1 satisfies min=1\", \"Apple M1\", nil, 1, true},\n\t\t{\"m1 does not satisfy min=2\", \"Apple M1\", nil, 2, false},\n\t\t{\"m10 satisfies min=10\", \"Apple M10\", nil, 10, true},\n\t\t{\"m10 satisfies min=3\", \"Apple M10\", nil, 3, true},\n\t\t{\"chip fetch error returns false\", \"\", fmt.Errorf(\"not mac\"), 3, false},\n\t\t{\"non-apple chip returns false\", \"INTEL CORE I9\", nil, 1, false},\n\t\t{\"empty chip returns false\", \"\", nil, 1, false},\n\t}\n\n\torig := chipDetector\n\tdefer func() { chipDetector = orig }()\n\n\tfor _, c := range cases {\n\t\tt.Run(c.name, func(t *testing.T) {\n\t\t\tchipDetector = fakeDetector{v: c.chip, e: c.chipErr}\n\t\t\tgot := IsMxOrNewer(c.min)\n\t\t\tif got != c.want {\n\t\t\t\tt.Fatalf(\"IsMxOrNewer(%d) = %v, want %v (chip=%q)\", c.min, got, c.want, c.chip)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "util/osutil/os.go",
    "content": "package osutil\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// EnvVar is environment variable\ntype EnvVar string\n\n// Exists checks if the environment variable has been set.\nfunc (e EnvVar) Exists() bool {\n\t_, ok := os.LookupEnv(string(e))\n\treturn ok\n}\n\n// Bool returns the environment variable value as boolean.\nfunc (e EnvVar) Bool() bool {\n\tok, _ := strconv.ParseBool(e.Val())\n\treturn ok\n}\n\n// Bool returns the environment variable value.\nfunc (e EnvVar) Val() string {\n\treturn os.Getenv(string(e))\n}\n\n// Or returns the environment variable value if set, otherwise returns val.\nfunc (e EnvVar) ValOr(val string) string {\n\tif v := os.Getenv(string(e)); v != \"\" {\n\t\treturn v\n\t}\n\treturn val\n}\n\n// WithPath appends p to the environment variable value as path list.\nfunc (e EnvVar) WithPath(p string) string {\n\tif v := e.Val(); v != \"\" {\n\t\treturn v + string(os.PathListSeparator) + p\n\t}\n\treturn p\n}\n\nconst EnvColimaBinary = \"COLIMA_BINARY\"\n\n// Executable returns the path name for the executable that started\n// the current process.\nfunc Executable() string {\n\te, err := func(s string) (string, error) {\n\t\t// prioritize env var in case this is a nested process\n\t\tif e := os.Getenv(EnvColimaBinary); e != \"\" {\n\t\t\treturn e, nil\n\t\t}\n\n\t\tif filepath.IsAbs(s) {\n\t\t\treturn s, nil\n\t\t}\n\n\t\te, err := exec.LookPath(s)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error looking up '%s' in PATH: %w\", s, err)\n\t\t}\n\n\t\tabs, err := filepath.Abs(e)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error computing absolute path of '%s': %w\", e, err)\n\t\t}\n\n\t\treturn abs, nil\n\t}(os.Args[0])\n\n\tif err != nil {\n\t\t// this should never happen, thereby it is safe to do\n\t\tlogrus.Traceln(fmt.Errorf(\"cannot detect current running executable: %w\", err))\n\t\tlogrus.Traceln(\"falling back to first CLI argument\")\n\t\treturn os.Args[0]\n\t}\n\n\treturn e\n}\n\n// Socket is a unix socket\ntype Socket string\n\n// Unix returns the unix address for the socket.\nfunc (s Socket) Unix() string { return \"unix://\" + s.File() }\n\n// File returns the file path for the socket.\nfunc (s Socket) File() string { return strings.TrimPrefix(string(s), \"unix://\") }\n"
  },
  {
    "path": "util/qemu.go",
    "content": "package util\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n)\n\n// AssertQemuImg checks if qemu-img is available.\nfunc AssertQemuImg() error {\n\tcmd := \"qemu-img\"\n\tif _, err := exec.LookPath(cmd); err != nil {\n\t\treturn fmt.Errorf(\"%s not found, run 'brew install %s' to install\", cmd, \"qemu\")\n\t}\n\n\treturn nil\n}\n\n// AssertKrunkit checks if krunkit is available.\nfunc AssertKrunkit() error {\n\tif _, err := exec.LookPath(\"krunkit\"); err != nil {\n\t\treturn fmt.Errorf(\"krunkit not found in $PATH\\nInstall with: brew tap slp/krunkit && brew install krunkit\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "util/shautil/sha.go",
    "content": "package shautil\n\nimport (\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"fmt\"\n)\n\n// SHA is a sha computation\ntype SHA interface {\n\tString() string\n\tBytes() []byte\n}\n\ntype s1 [20]byte\n\nfunc (s s1) String() string { return fmt.Sprintf(\"%x\", s[:]) }\nfunc (s s1) Bytes() []byte  { return s[:] }\n\ntype s256 [32]byte\n\nfunc (s s256) String() string { return fmt.Sprintf(\"%x\", s[:]) }\nfunc (s s256) Bytes() []byte  { return s[:] }\n\n// SHA256Hash computes a sha256sum of a string.\nfunc SHA256(s string) SHA {\n\treturn s256(sha256.Sum256([]byte(s)))\n}\n\n// SHA256Hash computes a sha256sum of a string.\nfunc SHA1(s string) SHA {\n\treturn s1(sha1.Sum([]byte(s)))\n}\n"
  },
  {
    "path": "util/template.go",
    "content": "package util\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"text/template\"\n)\n\n// WriteTemplate writes template with body to file after applying values.\nfunc WriteTemplate(body string, file string, values any) error {\n\tb, err := ParseTemplate(body, values)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.WriteFile(file, b, 0644)\n}\n\n// ParseTemplate parses template with body and values and returns the resulting bytes.\nfunc ParseTemplate(body string, values any) ([]byte, error) {\n\tt, err := template.New(\"\").Parse(body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing template: %w\", err)\n\t}\n\n\tvar b bytes.Buffer\n\tif err := t.Execute(&b, values); err != nil {\n\t\treturn nil, fmt.Errorf(\"error executing template: %w\", err)\n\t}\n\n\treturn b.Bytes(), err\n}\n"
  },
  {
    "path": "util/terminal/output.go",
    "content": "package terminal\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/fatih/color\"\n\t\"golang.org/x/term\"\n)\n\nvar _ io.WriteCloser = (*verboseWriter)(nil)\n\ntype verboseWriter struct {\n\tbuf   bytes.Buffer\n\tlines []string\n\n\tlineHeight int\n\ttermWidth  int\n\toverflow   int\n\n\tlastUpdate time.Time\n\n\tsync.Mutex\n}\n\n// NewVerboseWriter creates a new verbose writer.\n// A verbose writer pipes the input received to the stdout while tailing the specified lines.\n// Calling `Close` when done is recommended to clear the last uncleared output.\nfunc NewVerboseWriter(lineHeight int) io.WriteCloser {\n\treturn &verboseWriter{lineHeight: lineHeight}\n}\n\nfunc (v *verboseWriter) Write(p []byte) (n int, err error) {\n\t// if it's not a terminal, simply write to stdout\n\tif !isTerminal {\n\t\treturn os.Stdout.Write(p)\n\t}\n\n\tv.Lock()\n\tdefer v.Unlock()\n\n\tfor i, c := range p {\n\t\tif c != '\\n' {\n\t\t\tv.buf.WriteByte(c)\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := v.refresh(); err != nil {\n\t\t\treturn i + 1, err\n\t\t}\n\n\t}\n\treturn len(p), nil\n}\n\nfunc (v *verboseWriter) printLineVerbose() {\n\tline := v.sanitizeLine(v.buf.String())\n\tline = color.HiBlackString(line)\n\t_, _ = fmt.Fprintln(os.Stderr, line)\n}\n\nfunc (v *verboseWriter) refresh() error {\n\tv.clearScreen()\n\tv.addLine()\n\treturn v.printScreen()\n}\n\nfunc (v *verboseWriter) addLine() {\n\tdefer v.buf.Reset()\n\n\t// if height <=0, do not scroll\n\tif v.lineHeight <= 0 {\n\t\tv.printLineVerbose()\n\t\treturn\n\t}\n\n\tif len(v.lines) >= v.lineHeight {\n\t\tv.lines = v.lines[1:]\n\t}\n\tv.lines = append(v.lines, v.buf.String())\n}\n\nfunc (v *verboseWriter) Close() error {\n\tv.Lock()\n\tdefer v.Unlock()\n\n\tif v.buf.Len() > 0 {\n\t\tif err := v.refresh(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tv.clearScreen()\n\treturn nil\n}\n\nfunc (v *verboseWriter) sanitizeLine(line string) string {\n\t// remove logrus noises\n\tif strings.HasPrefix(line, \"time=\") && strings.Contains(line, \"msg=\") {\n\t\tline = line[strings.Index(line, \"msg=\")+4:]\n\t\tif l, err := strconv.Unquote(line); err == nil {\n\t\t\tline = l\n\t\t}\n\t}\n\n\treturn \"> \" + line\n}\n\nfunc (v *verboseWriter) printScreen() error {\n\tif err := v.updateTerm(); err != nil {\n\t\treturn err\n\t}\n\n\tv.overflow = 0\n\tfor _, line := range v.lines {\n\t\tline = v.sanitizeLine(line)\n\t\tif len(line) > v.termWidth {\n\t\t\tv.overflow += len(line) / v.termWidth\n\t\t\tif len(line)%v.termWidth == 0 {\n\t\t\t\tv.overflow -= 1\n\t\t\t}\n\t\t}\n\t\tline = color.HiBlackString(line)\n\t\tfmt.Println(line)\n\t}\n\treturn nil\n}\n\nfunc (v *verboseWriter) clearScreen() {\n\tfor i := 0; i < len(v.lines)+v.overflow; i++ {\n\t\tClearLine()\n\t}\n}\n\nfunc (v *verboseWriter) updateTerm() error {\n\t// no need to refresh so quickly\n\tif time.Since(v.lastUpdate) < time.Second*2 {\n\t\treturn nil\n\t}\n\tv.lastUpdate = time.Now().UTC()\n\n\tw, _, err := term.GetSize(int(os.Stdout.Fd()))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting terminal size: %w\", err)\n\t}\n\t// A width of zero would result in a division by zero panic when computing overflow\n\t// in printScreen. Therefore, set it to a safe - even though probably wrong - value.\n\t// We use <= 0 here because negative values are guaranteed to lead to unexpected\n\t// results, even if they don't cause panics.\n\tif w <= 0 {\n\t\tw = 80\n\t}\n\tv.termWidth = w\n\n\treturn nil\n}\n"
  },
  {
    "path": "util/terminal/terminal.go",
    "content": "package terminal\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\n\t\"golang.org/x/term\"\n)\n\nvar isTerminal = term.IsTerminal(int(os.Stdout.Fd()))\n\n// IsTerminal returns true if stdout is a terminal.\nfunc IsTerminal() bool {\n\treturn isTerminal\n}\n\n// ClearLine clears the previous line of the terminal\nfunc ClearLine() {\n\tif !isTerminal {\n\t\treturn\n\t}\n\n\tfmt.Print(\"\\033[1A \\033[2K \\r\")\n}\n\n// EnterAltScreen switches to the alternate screen buffer.\n// This preserves the main terminal content which can be restored\n// by calling ExitAltScreen.\nfunc EnterAltScreen() {\n\tif !isTerminal {\n\t\treturn\n\t}\n\t// Switch to alternate screen buffer and move cursor to top-left\n\tfmt.Print(\"\\033[?1049h\\033[H\")\n}\n\n// ExitAltScreen switches back to the main screen buffer,\n// restoring the previous terminal content.\nfunc ExitAltScreen() {\n\tif !isTerminal {\n\t\treturn\n\t}\n\tfmt.Print(\"\\033[?1049l\")\n}\n\n// WithAltScreen runs the provided function in the alternate screen buffer.\n// The main terminal content is preserved and restored after the function completes.\n// Handles Ctrl-C to ensure the terminal is restored even on interrupt.\n//\n// If header lines are provided, they are joined with newlines and displayed as a\n// fixed header at the top of the screen. The command output scrolls below the header.\n// The number of header lines is computed automatically based on newlines and terminal width.\nfunc WithAltScreen(fn func() error, header ...string) error {\n\thasHeader := len(header) > 0\n\tvar headerText string\n\tif hasHeader {\n\t\theaderText = strings.Join(header, \"\\n\")\n\t}\n\n\tif !isTerminal {\n\t\tif hasHeader {\n\t\t\tfmt.Println(headerText)\n\t\t}\n\t\treturn fn()\n\t}\n\n\tEnterAltScreen()\n\n\t// Handle Ctrl-C to ensure terminal is restored even on interrupt\n\tsigCh := make(chan os.Signal, 1)\n\tsignal.Notify(sigCh, os.Interrupt)\n\tdefer signal.Stop(sigCh)\n\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tselect {\n\t\tcase <-sigCh:\n\t\t\tif hasHeader {\n\t\t\t\tfmt.Print(\"\\033[r\") // Reset scroll region\n\t\t\t}\n\t\t\tExitAltScreen()\n\t\t\tos.Exit(1)\n\t\tcase <-done:\n\t\t\treturn\n\t\t}\n\t}()\n\n\tif hasHeader {\n\t\t// Get terminal dimensions\n\t\twidth, height, err := term.GetSize(int(os.Stdout.Fd()))\n\t\tif err != nil {\n\t\t\twidth = 80\n\t\t\theight = 24\n\t\t}\n\n\t\t// Print the header\n\t\tfmt.Println(headerText)\n\n\t\t// Calculate number of lines used by the header\n\t\theaderLines := countLines(headerText, width) + 1 // +1 for padding\n\n\t\t// Set scroll region from headerLines+1 to bottom\n\t\t// This keeps the header fixed while everything below scrolls\n\t\tfmt.Printf(\"\\033[%d;%dr\", headerLines+1, height)\n\n\t\t// Move cursor to the first line of the scroll region\n\t\tfmt.Printf(\"\\033[%d;1H\", headerLines+1)\n\t}\n\n\terr := fn()\n\n\tif hasHeader {\n\t\t// Reset scroll region\n\t\tfmt.Print(\"\\033[r\")\n\t}\n\n\tclose(done)\n\tExitAltScreen()\n\n\treturn err\n}\n\n// countLines calculates the number of terminal lines a string will occupy,\n// accounting for newlines and line wrapping based on terminal width.\nfunc countLines(s string, termWidth int) int {\n\tif termWidth <= 0 {\n\t\ttermWidth = 80\n\t}\n\n\tlines := 1\n\tcurrentLineLen := 0\n\n\tfor _, ch := range s {\n\t\tif ch == '\\n' {\n\t\t\tlines++\n\t\t\tcurrentLineLen = 0\n\t\t} else {\n\t\t\tcurrentLineLen++\n\t\t\tif currentLineLen >= termWidth {\n\t\t\t\tlines++\n\t\t\t\tcurrentLineLen = 0\n\t\t\t}\n\t\t}\n\t}\n\n\treturn lines\n}\n"
  },
  {
    "path": "util/util.go",
    "content": "package util\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/google/shlex\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// HomeDir returns the user home directory.\nfunc HomeDir() string {\n\thome, err := os.UserHomeDir()\n\tif err != nil {\n\t\t// this should never happen\n\t\tlogrus.Fatal(fmt.Errorf(\"error retrieving home directory: %w\", err))\n\t}\n\treturn home\n}\n\n// RandomAvailablePort returns an available port on the host machine.\nfunc RandomAvailablePort() int {\n\tlistener, err := net.Listen(\"tcp\", \":0\")\n\tif err != nil {\n\t\tlogrus.Fatal(fmt.Errorf(\"error picking an available port: %w\", err))\n\t}\n\n\tif err := listener.Close(); err != nil {\n\t\tlogrus.Fatal(fmt.Errorf(\"error closing temporary port listener: %w\", err))\n\t}\n\n\treturn listener.Addr().(*net.TCPAddr).Port\n}\n\n// isPortAvailable checks if a specific port is available on the host.\nfunc isPortAvailable(port int) bool {\n\tlistener, err := net.Listen(\"tcp\", fmt.Sprintf(\":%d\", port))\n\tif err != nil {\n\t\treturn false\n\t}\n\tif err := listener.Close(); err != nil {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// FindAvailablePort finds the first available port starting from startPort.\n// It checks up to maxAttempts consecutive ports (startPort, startPort+1, ...).\n// Returns the available port and true if found, or 0 and false if no port is available.\nfunc FindAvailablePort(startPort, maxAttempts int) (int, bool) {\n\tfor i := range maxAttempts {\n\t\tport := startPort + i\n\t\tif isPortAvailable(port) {\n\t\t\treturn port, true\n\t\t}\n\t}\n\treturn 0, false\n}\n\n// HostIPAddresses returns all IPv4 addresses on the host.\nfunc HostIPAddresses() []net.IP {\n\tvar addresses []net.IP\n\tints, err := net.InterfaceAddrs()\n\tif err != nil {\n\t\treturn nil\n\t}\n\tfor i := range ints {\n\t\tsplit := strings.Split(ints[i].String(), \"/\")\n\t\taddr := net.ParseIP(split[0]).To4()\n\t\t// ignore default loopback\n\t\tif addr != nil && addr.String() != \"127.0.0.1\" {\n\t\t\taddresses = append(addresses, addr)\n\t\t}\n\t}\n\n\treturn addresses\n}\n\n// SubnetAvailable checks if a subnet (in CIDR notation) does not conflict\n// with any existing host network interface addresses.\nfunc SubnetAvailable(subnet string) bool {\n\t_, cidr, err := net.ParseCIDR(subnet)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\taddrs, err := net.InterfaceAddrs()\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tfor _, addr := range addrs {\n\t\tip, _, err := net.ParseCIDR(addr.String())\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif ip = ip.To4(); ip == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif cidr.Contains(ip) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// RouteExists checks if a route exists for the given subnet on macOS.\nfunc RouteExists(subnet string) bool {\n\tif !MacOS() {\n\t\treturn false\n\t}\n\n\tip, _, err := net.ParseCIDR(subnet)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tout, err := exec.Command(\"netstat\", \"-rn\", \"-f\", \"inet\").Output()\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// macOS netstat shows /24 subnets without trailing .0\n\t// e.g. \"192.168.100\" instead of \"192.168.100.0\"\n\tnetworkAddr := strings.TrimSuffix(ip.String(), \".0\")\n\n\tfor _, line := range strings.Split(string(out), \"\\n\") {\n\t\tfields := strings.Fields(line)\n\t\tif len(fields) > 0 && (fields[0] == networkAddr || fields[0] == subnet) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ShellSplit splits cmd into arguments using.\nfunc ShellSplit(cmd string) []string {\n\tsplit, err := shlex.Split(cmd)\n\tif err != nil {\n\t\tlogrus.Warnln(\"error splitting into args: %w\", err)\n\t\tlogrus.Warnln(\"falling back to whitespace split\", err)\n\t\tsplit = strings.Fields(cmd)\n\t}\n\n\treturn split\n}\n\n// CleanPath returns the absolute path to the mount location.\n// If location is an empty string, nothing is done.\nfunc CleanPath(location string) (string, error) {\n\tif location == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\tstr := os.ExpandEnv(location)\n\n\tif strings.HasPrefix(str, \"~\") {\n\t\tstr = strings.Replace(str, \"~\", HomeDir(), 1)\n\t}\n\n\tstr = filepath.Clean(str)\n\tif !filepath.IsAbs(str) {\n\t\treturn \"\", fmt.Errorf(\"relative paths not supported for mount '%s'\", location)\n\t}\n\n\treturn strings.TrimSuffix(str, \"/\") + \"/\", nil\n}\n"
  },
  {
    "path": "util/yamlutil/yaml.go",
    "content": "package yamlutil\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"github.com/abiosoft/colima/embedded\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// WriteYAML encodes struct to file as YAML.\nfunc WriteYAML(value any, file string) error {\n\tb, err := yaml.Marshal(value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error encoding YAML: %w\", err)\n\t}\n\n\treturn os.WriteFile(file, b, 0644)\n}\n\n// Save saves the config.\nfunc Save(c config.Config, file string) error {\n\tb, err := encodeYAML(c)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := os.WriteFile(file, b, 0644); err != nil {\n\t\treturn fmt.Errorf(\"error writing yaml file: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc encodeYAML(conf config.Config) ([]byte, error) {\n\tvar doc yaml.Node\n\n\tf, err := embedded.Read(\"defaults/colima.yaml\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error reading config file: %w\", err)\n\t}\n\n\tif err := yaml.Unmarshal(f, &doc); err != nil {\n\t\treturn nil, fmt.Errorf(\"embedded default config is invalid yaml: %w\", err)\n\t}\n\n\tif l := len(doc.Content); l != 1 {\n\t\treturn nil, fmt.Errorf(\"unexpected error during yaml decode: doc has multiple children of len %d\", l)\n\t}\n\troot := doc.Content[0]\n\n\t// get all nodes\n\tnodeVals := map[string]*yaml.Node{}\n\tif err := traverseNode(\"\", root, nodeVals); err != nil {\n\t\treturn nil, fmt.Errorf(\"error traversing yaml node: %w\", err)\n\t}\n\n\t// get all node values\n\tstructVals := map[string]any{}\n\ttraverseConfig(\"\", conf, structVals)\n\n\t// apply values to nodes\n\tfor key, node := range nodeVals {\n\t\tval := structVals[key]\n\n\t\t// top level, ignore. except known maps.\n\t\tif node.Kind == yaml.MappingNode {\n\t\t\tswitch val.(type) {\n\t\t\tcase map[string]any:\n\t\t\tcase map[string]string:\n\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\t// nil slices are converted to untyped nil to encode as `null` instead of `[]`.\n\t\t// this preserves nil vs empty slice distinction when the yaml is loaded back.\n\t\tif v := reflect.ValueOf(val); v.Kind() == reflect.Slice && v.IsNil() {\n\t\t\tval = nil\n\t\t}\n\n\t\t// lazy way, delegate node construction to the yaml library via a roundtrip.\n\t\t// no performance concern as only one file is being read\n\t\tb, err := yaml.Marshal(val)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unexpected error nested value encoding: %w\", err)\n\t\t}\n\t\tvar newNode yaml.Node\n\t\tif err := yaml.Unmarshal(b, &newNode); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unexpected error during yaml node traversal: %w\", err)\n\t\t}\n\n\t\tif l := len(newNode.Content); l != 1 {\n\t\t\treturn nil, fmt.Errorf(\"unexpected error during yaml node traversal: doc has multiple children of len %d\", l)\n\t\t}\n\t\t*node = *newNode.Content[0]\n\t}\n\n\tb, err := encode(root)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error encoding yaml file: %w\", err)\n\t}\n\n\treturn b, nil\n}\n\nfunc traverseConfig(parentKey string, s any, vals map[string]any) {\n\ttyp := reflect.TypeOf(s)\n\tval := reflect.ValueOf(s)\n\n\t// everything else is a value, no nesting required\n\tif typ.Kind() != reflect.Struct {\n\t\tvals[parentKey] = val.Interface()\n\t\treturn\n\t}\n\n\t// traverse the struct fields recursively\n\tfor i := 0; i < typ.NumField(); i++ {\n\t\tfield := typ.Field(i)\n\t\tkey := strings.TrimSuffix(field.Tag.Get(\"yaml\"), \",omitempty\")\n\t\tif key == \"\" || key == \"-\" { // no yaml tag is present\n\t\t\tcontinue\n\t\t}\n\n\t\tif parentKey != \"\" {\n\t\t\tkey = parentKey + \".\" + key\n\t\t}\n\t\tval := val.Field(i)\n\n\t\ttraverseConfig(key, val.Interface(), vals)\n\t}\n\n}\n\nfunc traverseNode(parentKey string, node *yaml.Node, vals map[string]*yaml.Node) error {\n\tswitch node.Kind {\n\tcase yaml.MappingNode:\n\t\tif l := len(node.Content); l%2 != 0 {\n\t\t\treturn fmt.Errorf(\"uneven children of %d found for mapping node\", l)\n\t\t}\n\t\tfor i := 0; i < len(node.Content); i += 2 {\n\t\t\tif i > 1 {\n\t\t\t\t// fix jumbled comments\n\t\t\t\tif cn := node.Content[i]; cn.HeadComment != \"\" {\n\t\t\t\t\tif strings.Index(cn.HeadComment, \"#\") == 0 {\n\t\t\t\t\t\tcn.HeadComment = \"\\n\" + cn.HeadComment\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkey := node.Content[i].Value\n\t\t\tval := node.Content[i+1]\n\t\t\tif parentKey != \"\" {\n\t\t\t\tkey = parentKey + \".\" + key\n\t\t\t}\n\t\t\tvals[key] = val\n\n\t\t\tif err := traverseNode(key, val, vals); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\tcase yaml.SequenceNode:\n\t\tfor i := 0; i < len(node.Content); i++ {\n\t\t\tkey := strconv.Itoa(i)\n\t\t\tval := node.Content[i]\n\t\t\tif parentKey != \"\" {\n\t\t\t\tkey = parentKey + \".\" + key\n\t\t\t}\n\t\t\tvals[key] = val\n\n\t\t\tif err := traverseNode(key, val, vals); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\t// yaml.ScalarNode has nothing to do\n\treturn nil\n}\n\nfunc encode(v any) ([]byte, error) {\n\tvar buf bytes.Buffer\n\tenc := yaml.NewEncoder(&buf)\n\tenc.SetIndent(2)\n\n\terr := enc.Encode(v)\n\treturn buf.Bytes(), err\n}\n"
  },
  {
    "path": "util/yamlutil/yaml_test.go",
    "content": "package yamlutil\n\nimport (\n\t\"net\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/abiosoft/colima/config\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc Test_encode_Docker(t *testing.T) {\n\tconf := config.Config{\n\t\tDocker:     map[string]any{\"insecure-registries\": []any{\"127.0.0.1\"}},\n\t\tNetwork:    config.Network{DNSResolvers: []net.IP{net.ParseIP(\"1.1.1.1\")}},\n\t\tKubernetes: config.Kubernetes{K3sArgs: []string{\"--disable=traefik\"}},\n\t}\n\n\ttests := []struct {\n\t\tname    string\n\t\targs    config.Config\n\t\twant    config.Config\n\t\twantErr bool\n\t}{\n\t\t{name: \"nested\", args: conf, want: conf},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tb, err := encodeYAML(tt.args)\n\t\t\tvar got config.Config\n\t\t\tif err := yaml.Unmarshal(b, &got); err != nil {\n\t\t\t\tt.Errorf(\"resulting byte is not a valid yaml: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"save() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(got.Docker, tt.want.Docker) {\n\t\t\t\tt.Errorf(\"save() = %+v\\nwant %+v\", got.Docker, tt.want.Docker)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  }
]