diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index a851f9ef..4c0687ec 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -64,13 +64,13 @@ jobs: - name: Shell syntax and shellcheck if: runner.os == 'Linux' run: | - for f in agent-vm.sh agent-vm.setup.sh release.sh runtime.example.sh tests/*.sh tests/sftp/run tests/sftp/*.sh scripts/*; do + for f in agent-vm.sh provision/agent-vm.setup.sh release.sh runtime.example.sh tests/*.sh tests/sftp/run tests/sftp/*.sh scripts/*; do echo "bash -n $f" bash -n "$f" done sh -n www/public/install.sh shellcheck --version - shellcheck -S warning agent-vm.sh agent-vm.setup.sh release.sh runtime.example.sh tests/*.sh tests/sftp/run tests/sftp/*.sh scripts/* + shellcheck -S warning agent-vm.sh provision/agent-vm.setup.sh release.sh runtime.example.sh tests/*.sh tests/sftp/run tests/sftp/*.sh scripts/* shellcheck -S warning --shell=sh www/public/install.sh - name: Build, with the guest agent and templates @@ -83,7 +83,7 @@ jobs: run: go build -o _output/bin/agent-vm.exe ./cmd/agent-vm - name: Vet - run: go vet . ./cmd/... ./internal/... + run: go vet . ./agents/... ./cmd/... ./internal/... ./policy/... ./provision/... - name: Vet for Windows, amd64 and arm64 if: runner.os == 'Linux' @@ -99,15 +99,22 @@ jobs: GOOS=windows govulncheck ./... - name: Test - run: go test . ./cmd/... ./internal/... + run: go test . ./agents/... ./cmd/... ./internal/... ./policy/... ./provision/... - name: Tests of Lima, sshocker and pkg/sftp, patched if: runner.os != 'Windows' run: make test-third-party + # A program that embeds the library, built against this checkout. + # Its end-to-end test (go test -tags e2e in _output/example) boots a + # VM: not run here. + - name: Example program that embeds the library + if: runner.os == 'Linux' + run: make test-example + # Where the file system folds names (APFS, NTFS), it is the oracle of # the read-only names' matching; Linux's matches byte for byte. - name: Fuzz read-only name matching against the file system if: runner.os != 'Linux' shell: bash - run: GOWORK="$PWD/tests/fuzz/go.work" go test -vet=off -run '^$' -fuzz '^FuzzSameNameFS$' -fuzztime 2m github.com/lima-vm/sshocker/pkg/reversesshfs + run: go test -vet=off -run '^$' -fuzz '^FuzzSameNameFS$' -fuzztime 2m ./third_party/sshocker/pkg/reversesshfs diff --git a/.gitignore b/.gitignore index c4c74a7b..509ff024 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ dist # Built by the Makefile, embedded in the binary. /internal/limaembed/assets/* !/internal/limaembed/assets/README +# Made by go generate in the example. +/examples/consumer/assets/* +!/examples/consumer/assets/README diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d4faae5..e2a50736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,78 @@ # Changelog -## Unreleased +## 0.4.0 (unreleased) + +### Added + +- Named VMs: `agent-vm --name agent2 claude` runs a second VM on a folder, + called `agent2`, next to the folder's own. It belongs to the folder it is + made in and runs from there only: from another folder it stops, naming the + folder (`agent-vm rm agent2` gives it up, `--reset` makes it again, bound + anew). `stop NAME` and `rm NAME` reach it; `list` marks the ones of the + current folder with `>`. A name is lowercase letters, digits and dashes, at + most 40 characters, not `base` nor `base-…`, nor ending in a dash and 8 hex + digits (which every project's VM does). +- `--no-shares`, with `--name`: a VM that shares nothing, like `--scratch`, + but kept, and usable from any folder (`agent-vm --name sandbox --no-shares + claude`). It never gets shares; the folder's path is made in the VM as an + empty working folder. `--scratch` is still `--no-shares --rm` with a name of + its own; `--scratch --name` is a usage error. +- agent-vm is a Go library too, for programs that run their own agent in a VM + per project: `github.com/sylvinus/agent-vm` (`Open`, `Setup`, `ProjectVM`, + `Ensure`, `Exec`), with `provision` (what the base VM is built from), + `policy` (what the VMs may reach, and the files they may not change + unasked) and `agents`. The command line is built on it. It never writes to a + terminal nor reads an `AGENT_VM_*` variable: what it tells goes to a callback, + what it asks to an `Asker`. Experimental until a program other than + agent-vm uses it. `examples/consumer` is such a program; + `go run github.com/sylvinus/agent-vm/cmd/agent-vm-assets` makes the guest + agent and templates it embeds. + +### Changed + +- Each VM has a folder of its own, `~/.agent-vm/vms//`: Lima's files + for it (`lima/`), what agent-vm knows of it (`metadata.json`: the base it + was cloned from, its terminfo, its `--scratch` run), the policies it + booted with (`policy.json`) and the single files of `~/.agent-vm/volumes` + staged for it (`file-mounts/`). Deleting a VM deletes the folder, so + nothing of a VM outlives it. `~/.agent-vm/lima` keeps only what Lima + shares between VMs (`_config`, `_networks`). The `.agent-vm-*` files in + `~/.agent-vm` and `~/.agent-vm/file-mounts` are gone. +- The first command that changes VMs moves 0.3.0's: the VMs are renamed to + their new place (their disks keep sharing blocks with the base), the + markers become `metadata.json`. A running VM cannot move: on a terminal you + are asked whether to stop it, else it moves next time. Until then + `agent-vm list` says some wait. There is no way back: 0.3.0 sees no VM after + the move. `ssh_config` lines in `~/.ssh/config` become + `Include ~/.agent-vm/vms/*/lima/ssh.config`. +- The network and guarded-files policies a VM boots with are written to its + `policy.json`, which its host agent reads: the files in `~/.agent-vm` are + read by the command that starts the VM, not by the host agent. +- Lima, sshocker, pkg/sftp and gvisor-tap-vsock are packages of agent-vm's + module (`third_party/`, upstream plus `patches/`), not replaced modules: a + program that imports agent-vm gets them patched. + +## 0.3.1 + +### Added + +- Releases have a `.deb` and an `.rpm` for Linux (amd64, arm64): the binary in + `/usr/bin` and the docs, with QEMU and ssh recommended. Not in a package + repository yet: install the file with `apt install ./agent-vm_X.Y.Z_arm64.deb` + or `dnf install ./agent-vm-X.Y.Z-1.aarch64.rpm`. +- A Nix flake (Linux): `nix run github:sylvinus/agent-vm`. QEMU comes with it, + ssh is the system's. + +### Fixed + +- Linux: VMs turned Broken seconds to minutes after a start. The end of the + agent-vm command that started a VM (closing its terminal, a kill, and at + random a plain exit) killed Lima's host agent, which left QEMU running + without it, cut off, and failing every later start. The host agent now + outlives that command. On macOS, the same killed the VM. +- A VM left in that state is stopped by force, then started again, on the + next run; `agent-vm stop` stops it too. `stop` no longer says "VM stopped." + for a Broken VM it did not stop. ## 0.3.0 diff --git a/Makefile b/Makefile index 511fcb85..781a6b93 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,12 @@ -# agent-vm 0.3, the Go binary, with Lima built in from third_party/. +# agent-vm, the Go binary, with Lima built in from third_party/. # # make _output/bin/agent-vm, with the guest agent and templates # make install build it, then link it onto the PATH (AGENT_VM_BIN_DIR # to choose where; ~/.local/bin by default) # make limactl Lima's own limactl from third_party/, for debugging # make test the Go tests +# make test-example the example program that embeds the library, built +# against this checkout # make check-windows the Windows build and tests compile and vet, from any OS # make test-third-party the tests of the modules in third_party/, patched # make check-third-party third_party/ is upstream plus patches/, nothing else @@ -31,9 +33,10 @@ EXE := .exe endif ASSETS := internal/limaembed/assets +MOD := github.com/sylvinus/agent-vm LDFLAGS := -s -w \ - -X github.com/sylvinus/agent-vm/internal/version.Version=$(VERSION) \ - -X github.com/lima-vm/lima/v2/pkg/version.Version=$(LIMA_VERSION) + -X $(MOD)/internal/version.Version=$(VERSION) \ + -X $(MOD)/third_party/lima/pkg/version.Version=$(LIMA_VERSION) # yq only edits Lima's YAML here: without its other formats (Lua, HCL, TOML, # XML...), 10% smaller. Tests run with the same tags. TAGS := yq_noxml,yq_nolua,yq_noini,yq_nohcl,yq_notoml,yq_noprops,yq_nocsv,yq_nouri,yq_nobase64,yq_nosh,yq_noshell @@ -47,7 +50,7 @@ CGO_ENABLED ?= 0 endif GO_BUILD := CGO_ENABLED=$(CGO_ENABLED) $(GO) build -trimpath -tags $(TAGS) -ldflags "$(LDFLAGS)" -.PHONY: all agent-vm install limactl assets test test-go check-windows test-third-party check-third-party fuzz clean +.PHONY: all agent-vm install limactl assets test test-go test-example check-windows test-third-party check-third-party fuzz clean all: agent-vm @@ -63,60 +66,61 @@ install: agent-vm ./_output/bin/agent-vm$(EXE) install limactl: - $(GO_BUILD) -o _output/bin/limactl$(EXE) github.com/lima-vm/lima/v2/cmd/limactl + $(GO_BUILD) -o _output/bin/limactl$(EXE) ./third_party/lima/cmd/limactl mkdir -p _output/share/lima CGO_ENABLED=0 GOOS=linux GOARCH=$(GOARCH) $(GO) build -trimpath -ldflags "-s -w" \ - -o _output/share/lima/lima-guestagent.Linux-$(GUEST_ARCH) github.com/lima-vm/lima/v2/cmd/lima-guestagent + -o _output/share/lima/lima-guestagent.Linux-$(GUEST_ARCH) ./third_party/lima/cmd/lima-guestagent rm -rf _output/share/lima/templates && cp -R third_party/lima/templates _output/share/lima/templates ifeq ($(GOOS),darwin) codesign -f -v --entitlements third_party/lima/vz.entitlements -s - _output/bin/limactl endif -# The guest agent runs in the Linux guest: built for it, without cgo. +# The guest agent runs in the Linux guest: built for it, without cgo. The same +# tool a program that embeds agentvm runs (cmd/agent-vm-assets), run for this +# machine whatever GOOS and GOARCH build the binary for. assets: - CGO_ENABLED=0 GOOS=linux GOARCH=$(GOARCH) $(GO) build -trimpath \ - -ldflags "-s -w -X github.com/lima-vm/lima/v2/pkg/version.Version=$(LIMA_VERSION)" \ - -o $(ASSETS)/lima-guestagent.Linux-$(GUEST_ARCH) github.com/lima-vm/lima/v2/cmd/lima-guestagent - gzip -9 -n -f $(ASSETS)/lima-guestagent.Linux-$(GUEST_ARCH) - rm -rf $(ASSETS)/templates && cp -R third_party/lima/templates $(ASSETS)/templates + env -u GOOS -u GOARCH $(GO) run ./cmd/agent-vm-assets -o $(ASSETS) -arch $(GUEST_ARCH) + +# The example program that embeds agentvm, built against this checkout in a +# copy of its module, to which the replace is added (it has none: it requires +# the released agent-vm), and its test. +test-example: + rm -rf _output/example && mkdir -p _output && cp -R examples/consumer _output/example + cd _output/example && $(GO) mod edit -replace $(MOD)=$(CURDIR) && GOFLAGS=-mod=mod $(GO) mod tidy \ + && env -u GOOS -u GOARCH $(GO) generate ./... \ + && $(GO) vet ./... && $(GO) build -o ../sandbox$(EXE) . && $(GO) test ./... test: test-go test-go: - $(GO) test -tags $(TAGS) . ./cmd/... ./internal/... + $(GO) test -tags $(TAGS) . ./agents/... ./cmd/... ./internal/... ./policy/... ./provision/... # The Windows build and its tests compile and vet, from any OS. The Windows # paths' logic is tested everywhere (paths.Windows); the tests themselves # run on Windows in CI (windows-latest). check-windows: - GOOS=windows GOARCH=amd64 $(GO) vet -tags $(TAGS) . ./cmd/... ./internal/... - GOOS=windows GOARCH=arm64 $(GO) vet -tags $(TAGS) . ./cmd/... ./internal/... + GOOS=windows GOARCH=amd64 $(GO) vet -tags $(TAGS) . ./agents/... ./cmd/... ./internal/... ./policy/... ./provision/... + GOOS=windows GOARCH=arm64 $(GO) vet -tags $(TAGS) . ./agents/... ./cmd/... ./internal/... ./policy/... ./provision/... -# From this module, so that each library is built with the others patched. -# Lima's MCP packages and sshocker's command are not part of agent-vm (-e: -# listing them needs sums nothing built here uses; grep keeps them out of -# the tests). test-third-party: - $(GO) test -vet=off -race github.com/pkg/sftp/... - $(GO) test -race github.com/lima-vm/sshocker/pkg/... - $(GO) test $$($(GO) list -e github.com/lima-vm/lima/v2/pkg/... | grep -v /mcp) - $(GO) test github.com/containers/gvisor-tap-vsock/pkg/... + $(GO) test -vet=off -race ./third_party/sftp/... + $(GO) test -race ./third_party/sshocker/pkg/... + $(GO) test ./third_party/lima/pkg/... + $(GO) test ./third_party/gvisor-tap-vsock/pkg/... check-third-party: scripts/third-party-sync all --check -# Each fuzzer for FUZZTIME (their seeds run with the tests). The vendored -# sshocker's through tests/fuzz/go.work, which makes it fuzzable. +# Each fuzzer for FUZZTIME (their seeds run with the tests). FUZZTIME ?= 1m -FUZZ = github.com/lima-vm/sshocker/pkg/reversesshfs github.com/pkg/sftp ./internal/env ./internal/gitguard \ +FUZZ = ./third_party/sshocker/pkg/reversesshfs ./third_party/sftp ./internal/env ./internal/gitguard \ ./internal/mounts ./internal/paths ./internal/runscript ./internal/ui ./internal/vmname fuzz: @set -e; for pkg in $(FUZZ); do \ - case $$pkg in ./*) work=off ;; *) work=$(CURDIR)/tests/fuzz/go.work ;; esac; \ - fs=$$(GOWORK=$$work $(GO) test -vet=off -list '^Fuzz' $$pkg) || { echo "$$fs"; exit 1; }; \ + fs=$$($(GO) test -vet=off -list '^Fuzz' $$pkg) || { echo "$$fs"; exit 1; }; \ for f in $$(echo "$$fs" | grep '^Fuzz'); do \ echo "== $$pkg $$f"; \ - GOWORK=$$work $(GO) test -vet=off -run '^$$' -fuzz "^$$f$$" -fuzztime $(FUZZTIME) $$pkg; \ + $(GO) test -vet=off -run '^$$' -fuzz "^$$f$$" -fuzztime $(FUZZTIME) $$pkg; \ done; \ done diff --git a/README.md b/README.md index 8acb59a5..b575f32d 100644 --- a/README.md +++ b/README.md @@ -40,16 +40,23 @@ The agent is root in its VM and has the network. What it can reach on your machi - The agent writes the project folder: on your machine, open it in your editor and use git there, and run everything else in the VM. Open agent-vm projects in VS Code's Restricted Mode, and never connect an editor to the VM over SSH (Remote-SSH trusts the VM with your machine: [SSH from your machine](https://www.agent-vm.org/#ssh-from-your-machine)). Editors, agents, commit hooks and commands you run on the host can run code the agent wrote: [What else reads the project](https://www.agent-vm.org/#what-else-on-your-machine-reads-the-project). - The VM reaches the internet, not your machine nor your local network: what it may reach there, or the only domains it may reach, go in `~/.agent-vm/network`. A port the VM listens on is forwarded to your machine's loopback, unless something there uses it already or it is in 32768-60999, the range the guest picks random ports from. +## As a library + +`github.com/sylvinus/agent-vm` is the engine of the command, for programs that run their own agent in a VM per project: `agentvm.Open`, `Setup`, `ProjectVM`, `Ensure`, `Exec`, with `provision`, `policy` and `agents` beside it. It writes to no terminal and reads no `AGENT_VM_*` variable. Experimental until a program other than agent-vm uses it; [`examples/consumer`](examples/consumer) is one, and the package documentation says what a build needs (cgo and a signature on macOS, the guest agent from `cmd/agent-vm-assets`). + ## Development ```bash make # _output/bin/agent-vm make test-go # no VM; 0.2's recorded behavior where it is compared go test -tags e2e ./internal/vm/ ./internal/cli/ # real VMs +make test-example # the example program that embeds the library make check-third-party test-third-party # Lima, sshocker, pkg/sftp, gvisor-tap-vsock: upstream + patches/ make fuzz FUZZTIME=5m # each fuzzer; their seeds run with the tests ``` +`third_party/` is part of this module, so a program importing it builds Lima, sshocker, pkg/sftp and gvisor-tap-vsock patched. + Releases: [RELEASING.md](RELEASING.md). The website is in [`www/`](www/README.md). Changes: [CHANGELOG.md](CHANGELOG.md). ## License diff --git a/RELEASING.md b/RELEASING.md index d2e679ba..9769056e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -23,6 +23,9 @@ binaries `osslsigncode` (`brew install osslsigncode`). `AGENT_VM_WINDOWS_PFX_PASS=`. Without, they go out unsigned, with a warning. - Homebrew: `AGENT_VM_TAP=` updates `Formula/agent-vm.rb` there (each platform's url and sha256) and prints the commands to commit it. + - The Linux `.deb` and `.rpm` (`packaging/nfpm.yaml`) are made with nfpm, built with Go at the + version pinned in `release.sh` (`NFPM_VERSION`; `AGENT_VM_NFPM=` uses another). They + are release assets and in `SHA256SUMS`, after the tarballs. - A run that tagged and then failed is resumed by running it again. 3. In a commit after it: `NEXT_VERSION` to the next version, and a new `## Unreleased` section. @@ -34,5 +37,11 @@ binaries `osslsigncode` (`brew install osslsigncode`). `on_arm`/`on_intel`), installing the tarball's `agent-vm`: that is the shape `AGENT_VM_TAP` updates. Then, in the tap: `brew audit --strict sylvinus/tap/agent-vm`, `brew reinstall` and `brew test` before pushing. +- `flake.nix` builds from source, with its `vendorHash` for the Go dependencies. When go.mod or + go.sum change, `release.sh` stops until it is updated: on a machine with Nix, set + `pkgs.lib.fakeHash`, run `nix build .#agent-vm` (Linux), put the hash it prints back, and update + the `go.mod+go.sum:` checksum line next to it (the error gives the value). The hash is of the + downloaded modules only (`proxyVendor`), so patches to `third_party/` leave it as it is. Do the + same after `nix flake update`: `flake.lock` pins nixpkgs, and with it the Go that downloads them. - `--not-latest` publishes a release without making it GitHub's latest: the installer's default stays the previous release, and the Homebrew formula is left alone, `AGENT_VM_TAP` or not. diff --git a/agents/agents.go b/agents/agents.go new file mode 100644 index 00000000..6c041668 --- /dev/null +++ b/agents/agents.go @@ -0,0 +1,53 @@ +// Package agents lists the coding agents agent-vm runs, each with the flag +// that lets it work unattended, which is safe in a VM. A program that embeds +// agentvm can run these, or its own. +// +// This package is experimental until a program other than agent-vm has used +// it. +package agents + +// Agent is a command that works on a project, in a VM. +type Agent struct { + // Name is what the command line calls it: `agent-vm claude`. + Name string + // Command is the program and the flags that let it work without asking + // permission. + Command []string + // TTY says the agent draws a full-screen terminal interface: it needs + // one even when its output is not a terminal. + TTY bool +} + +// Defaults are agent-vm's agents. +func Defaults() []Agent { + return []Agent{ + // The VM also enforces bypass mode with managed settings: Claude Code + // drops the flag when it relaunches itself. + {Name: "claude", Command: []string{"claude", "--dangerously-skip-permissions"}}, + // Approves every prompt that is not explicitly denied. + {Name: "opencode", Command: []string{"opencode", "--auto"}, TTY: true}, + {Name: "codex", Command: []string{"codex", "--dangerously-bypass-approvals-and-sandbox"}}, + {Name: "vibe", Command: []string{"vibe", "--agent", "auto-approve"}, TTY: true}, + // No permission prompts: it runs every tool as asked. + {Name: "pi", Command: []string{"pi"}, TTY: true}, + } +} + +// Lookup is the default agent called name. +func Lookup(name string) (Agent, bool) { + for _, a := range Defaults() { + if a.Name == name { + return a, true + } + } + return Agent{}, false +} + +// Names are the names of the default agents, in the order they are listed. +func Names() []string { + var out []string + for _, a := range Defaults() { + out = append(out, a.Name) + } + return out +} diff --git a/agentvm.go b/agentvm.go new file mode 100644 index 00000000..e1f7ff91 --- /dev/null +++ b/agentvm.go @@ -0,0 +1,133 @@ +// Package agentvm runs AI coding agents, or any program, in a disposable Linux +// VM per project: the project's folder is shared with the VM, nothing else of +// the host is, and the VM reaches the internet but not the host's network. +// It is the engine of the agent-vm command (cmd/agent-vm), as a library. +// +// A program that embeds it: +// +// func main() { +// agentvm.RunHelper() // Lima's hostagent starts this executable again +// e, err := agentvm.Open(agentvm.Options{StateDir: "~/.mytool", AppName: "mytool", Assets: assets}) +// ... +// e.Setup(ctx, myTemplate, nil) // once: builds the base VM +// vm, _ := e.ProjectVM(dir) // the VM of a project folder +// vm.Ensure(ctx, agentvm.Spec{}) // exists, runs, shares the folder +// vm.Exec(ctx, agentvm.Exec{Args: []string{"mytool-agent"}}) +// } +// +// What it needs of the build: on macOS, cgo, and the binary signed with the +// entitlements in third_party/lima/vz.entitlements; and the guest agent and +// Lima's templates, made by `go run github.com/sylvinus/agent-vm/cmd/agent-vm-assets +// -o assets/` and embedded by the program, given as Options.Assets. +// +// The one-engine-per-process rule: Lima's home is an environment variable +// and its logging is global, so a process opens one Engine. The library +// installs no signal handler and writes nothing to standard output: what it +// tells goes to Options.OnEvent. +// +// This package is experimental until a program other than agent-vm has used +// it: v0.x, and its API may change. +package agentvm + +import ( + "errors" + "os" + + "github.com/sylvinus/agent-vm/internal/engine" + "github.com/sylvinus/agent-vm/internal/limaembed" +) + +// The engine, its handles and what they take and tell. +type ( + // Engine runs the VMs of one state folder. + Engine = engine.Engine + // Options configure an Engine; StateDir is the only one required. + Options = engine.Options + // VM is the VM of a project folder. + VM = engine.VM + // Spec says how a VM is wanted. + Spec = engine.Spec + // Exec is a command to run in a VM. + Exec = engine.Exec + // Details are what is known of a VM. + Details = engine.Details + + // Event is what the engine tells; Level its kind. + Event = engine.Event + Level = engine.Level + // Asker answers the questions of the engine, Question one of them. + Asker = engine.Asker + Question = engine.Question + QuestionKind = engine.QuestionKind + // NoAsker answers no to everything. + NoAsker = engine.NoAsker + + // Output shows what the steps of Setup print; SetupError is why Setup + // stopped. + Output = engine.Output + SetupError = engine.SetupError + + // Failure and RefusedError are the errors of a start that stopped. + Failure = engine.Failure + RefusedError = engine.RefusedError +) + +// Event levels. +const ( + Info = engine.Info + Warning = engine.Warning + Error = engine.Error +) + +// Questions the engine asks. +const ( + QHooks = engine.QHooks + QUnsafe = engine.QUnsafe + QBareRepo = engine.QBareRepo + QRestart = engine.QRestart + QStopToMove = engine.QStopToMove +) + +// DefaultBase is the name of the template Setup makes and VMs are cloned +// from, unless another is named. +const DefaultBase = engine.DefaultBase + +// What a call fails on, for errors.Is. +var ( + ErrBaseMissing = engine.ErrBaseMissing + ErrBaseIncomplete = engine.ErrBaseIncomplete + ErrBaseOld = engine.ErrBaseOld + ErrAborted = engine.ErrAborted + ErrNotApplied = engine.ErrNotApplied + ErrRefused = engine.ErrRefused + ErrFailed = engine.ErrFailed + ErrNameTooLong = engine.ErrNameTooLong + ErrLimaOverrides = engine.ErrLimaOverrides + ErrUnconfined = engine.ErrUnconfined + ErrNotFound = engine.ErrNotFound + // ErrNoAssets: no guest agent was given (Options.Assets). + ErrNoAssets = limaembed.ErrNoAssets +) + +// RunHelper must run first in main, before anything else is done. Lima starts +// the current executable again as the host agent of each VM, with arguments of +// its own: RunHelper recognises them, runs the host agent and exits. In any +// other run it returns at once. +// +// On macOS, the host agent needs the main goroutine on the process's first +// thread: put runtime.LockOSThread() in an init function of the main package +// (see cmd/agent-vm/main_darwin_gui.go). +func RunHelper() { + limaembed.Run(os.Args[1:]) +} + +// Open makes an Engine. It fails, saying why, when RunHelper was not called +// (and Options.Backend, for tests, is not set): the first VM would otherwise +// start a second copy of the program as its host agent that runs the +// program's own main. +func Open(opts Options) (*Engine, error) { + if opts.Backend == nil && !limaembed.Called() { + return nil, errors.New("agentvm: call agentvm.RunHelper() first in main: Lima starts this executable again as each VM's host agent") + } + return engine.Open(opts) +} diff --git a/assets.go b/assets.go index 36e976e8..0bad11e1 100644 --- a/assets.go +++ b/assets.go @@ -1,13 +1,14 @@ -// Package agentvm holds the files of this repository the binary embeds. package agentvm -import _ "embed" +import ( + _ "embed" + + "github.com/sylvinus/agent-vm/provision" +) // SetupScript provisions the base VM: `agent-vm setup` runs it in the guest, // after the choices as export lines. -// -//go:embed agent-vm.setup.sh -var SetupScript string +var SetupScript = provision.AgentVMScript() // The license of agent-vm, the notice about third_party/, and the licenses of // what the binary is built from (scripts/licenses): `agent-vm licenses`. diff --git a/cmd/agent-vm-assets/main.go b/cmd/agent-vm-assets/main.go new file mode 100644 index 00000000..f4f3d807 --- /dev/null +++ b/cmd/agent-vm-assets/main.go @@ -0,0 +1,183 @@ +// Command agent-vm-assets makes the files a program that embeds agentvm gives +// to Options.Assets: Lima's guest agent for Linux, gzipped, and Lima's +// templates. +// +// go run github.com/sylvinus/agent-vm/cmd/agent-vm-assets -o assets/ +// +// Run from the program's module, at build time (go generate); embed the +// folder: +// +// //go:embed all:assets +// var assetsFS embed.FS +// +// assets, _ := fs.Sub(assetsFS, "assets") // Options.Assets +// +// The guest agent is built for the architecture of the machine that runs the +// program, unless -arch names others (aarch64, x86_64: both for a program +// built for macOS and Linux hosts of both). +package main + +import ( + "bytes" + "compress/gzip" + "flag" + "fmt" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +const module = "github.com/sylvinus/agent-vm" + +func main() { + out := flag.String("o", "assets", "folder to fill") + arch := flag.String("arch", "", "guest architectures, comma-separated (aarch64, x86_64); this machine's by default") + flag.Parse() + if err := run(*out, *arch); err != nil { + fmt.Fprintf(os.Stderr, "agent-vm-assets: %v\n", err) + os.Exit(1) + } +} + +func goArch(a string) (string, error) { + switch a { + case "aarch64", "arm64": + return "arm64", nil + case "x86_64", "amd64": + return "amd64", nil + } + return "", fmt.Errorf("unknown architecture %q (aarch64, x86_64)", a) +} + +func run(out, archs string) error { + if archs == "" { + archs = map[string]string{"arm64": "aarch64", "amd64": "x86_64"}[runtime.GOARCH] + if archs == "" { + return fmt.Errorf("no guest agent for %s: name -arch", runtime.GOARCH) + } + } + dir, err := goOutput("list", "-m", "-f", "{{.Dir}}", module) + if err != nil { + return fmt.Errorf("where is %s? Run this from a module that requires it: %w", module, err) + } + version, err := limaVersion(filepath.Join(dir, "third_party", "SOURCES")) + if err != nil { + return err + } + if err := os.MkdirAll(out, 0o755); err != nil { + return err + } + tmp, err := os.MkdirTemp("", "agent-vm-assets") + if err != nil { + return err + } + defer os.RemoveAll(tmp) + for _, a := range strings.Split(archs, ",") { + a = strings.TrimSpace(a) + ga, err := goArch(a) + if err != nil { + return err + } + if err := buildGuestAgent(tmp, out, dir, a, ga, version); err != nil { + return err + } + } + dst := filepath.Join(out, "templates") + if err := os.RemoveAll(dst); err != nil { + return err + } + return copyTree(filepath.Join(dir, "third_party", "lima", "templates"), dst) +} + +// limaVersion is the version of Lima that SOURCES records: the one its guest +// agent reports. +func limaVersion(sources string) (string, error) { + b, err := os.ReadFile(sources) + if err != nil { + return "", err + } + for _, l := range strings.Split(string(b), "\n") { + if f := strings.Fields(l); len(f) >= 4 && f[0] == "lima" && !strings.HasPrefix(l, "#") { + return f[3], nil + } + } + return "", fmt.Errorf("no lima in %s", sources) +} + +// buildGuestAgent builds Lima's guest agent for the Linux guest of arch. From +// the folder of agent-vm's own module, with its go.sum: the program's module +// does not import the agent, so its go.sum has none of the agent's modules. +func buildGuestAgent(tmp, out, dir, arch, goarch, version string) error { + bin := filepath.Join(tmp, "lima-guestagent."+arch) + cmd := exec.Command("go", "build", "-trimpath", "-buildvcs=false", + "-ldflags", "-s -w -X "+module+"/third_party/lima/pkg/version.Version="+version, + "-o", bin, "./third_party/lima/cmd/lima-guestagent") + cmd.Dir = dir + cmd.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS=linux", "GOARCH="+goarch, "GOWORK=off") + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("building the guest agent for %s: %w", arch, err) + } + b, err := os.ReadFile(bin) + if err != nil { + return err + } + var z bytes.Buffer + // No name, no time: the same bytes every time. + w, _ := gzip.NewWriterLevel(&z, gzip.BestCompression) + if _, err := w.Write(b); err != nil { + return err + } + if err := w.Close(); err != nil { + return err + } + return os.WriteFile(filepath.Join(out, "lima-guestagent.Linux-"+arch+".gz"), z.Bytes(), 0o644) +} + +func goOutput(args ...string) (string, error) { + cmd := exec.Command("go", args...) + cmd.Stderr = os.Stderr + b, err := cmd.Output() + return strings.TrimSpace(string(b)), err +} + +func copyTree(src, dst string) error { + return filepath.WalkDir(src, func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(src, p) + if err != nil { + return err + } + t := filepath.Join(dst, rel) + if d.IsDir() { + return os.MkdirAll(t, 0o755) + } + in, err := os.Open(p) + if err != nil { + return err + } + defer in.Close() + o, err := os.OpenFile(t, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err != nil { + return err + } + _, err = io.Copy(o, in) + if err := closeAfter(o, err); err != nil { + return fmt.Errorf("%s: %w", t, err) + } + return nil + }) +} + +func closeAfter(f *os.File, err error) error { + if cerr := f.Close(); err == nil { + return cerr + } + return err +} diff --git a/cmd/agent-vm/main.go b/cmd/agent-vm/main.go index 08b74ddc..f32d46bd 100644 --- a/cmd/agent-vm/main.go +++ b/cmd/agent-vm/main.go @@ -4,15 +4,22 @@ package main import ( "os" + "strings" + agentvm "github.com/sylvinus/agent-vm" "github.com/sylvinus/agent-vm/internal/cli" - "github.com/sylvinus/agent-vm/internal/limaembed" ) +// extraPath goes first on PATH, for this process and what it starts (the +// hostagent, QEMU): set at build time (-ldflags -X) by a build that ships +// QEMU with agent-vm (flake.nix). Empty otherwise. +var extraPath string + func main() { - // Lima's libraries start this executable again for its hostagent. - if limaembed.Run(os.Args[1:]) { - return + if p := os.Getenv("PATH"); extraPath != "" && !strings.HasPrefix(p, extraPath+string(os.PathListSeparator)) { + os.Setenv("PATH", extraPath+string(os.PathListSeparator)+p) } + // Lima's libraries start this executable again for its hostagent. + agentvm.RunHelper() os.Exit(cli.Main(os.Args[1:], cli.Stdio())) } diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index de0a4e98..7e92195e 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -30,6 +30,39 @@ degrading, so every VM would stop starting. The flag would have to detect host support first. Nothing here has an M3 to test it on. It would also hand the agent a hypervisor interface. +## krunkit on macOS + +Status: vendored, not wired in. + +The Lima in `third_party/` has a krunkit driver (macOS arm64) that nothing here +imports. It runs `krunkit`, a program of its own (Homebrew): agent-vm would no +longer be one binary there, and `doctor` would check it. Templates would be per +driver (`vms/base`, `vms/base-krunkit`; `base-*` is reserved, and `base.name` in +`metadata.json` says which), and the driver is Lima's `vmType`, which a VM +changes with `--reset` only. To check when it is wired in: patch +`lima/0005-network-isolation-hooks` changes `usernet/gvproxy.go`, the host agent +and the qemu driver, and krunkit calls `usernet.StartGVisorNetstack` itself; its +socket names against `vm.MaxName`; and a separate process with GPU passthrough +gives the guest more of the host to reach than vz does. + +## Disk space of the VMs + +Status: ideas, by value. + +Lima clones with `clonefile` on APFS, so VMs share the base's blocks until they +write their own; `copy_file_range` on Linux shares blocks on btrfs and XFS with +reflink, and copies the data on ext4. A new base shares nothing with the VMs of +the old one, until `--reset`. + +1. `list` could show the space a VM uses (allocated blocks), not its disk's size. +2. ext4: a qemu VM's disk as a qcow2 overlay on its base's, the bases kept while + a VM uses them. qemu only. +3. Giving back the space of deleted files needs discard through the virtual disk + and `fstrim` in the guest: to check for vz and krunkit. + +Not proposed: caches written by several VMs (Docker images, npm), which let one +project's VM plant files another one uses. Read-only shared caches are fine. + ## Run the VMs on another machine Status: designed, not built. diff --git a/docs/design-0.4.0.md b/docs/design-0.4.0.md new file mode 100644 index 00000000..c67c19a9 --- /dev/null +++ b/docs/design-0.4.0.md @@ -0,0 +1,374 @@ +# 0.4.0 design + +Status: built, except for what "As built" (at the end) lists. The sections +below are the design as proposed; "As built" says where the code differs and +what it settled. + +0.4.0 changes where agent-vm keeps its VMs, splits the CLI from what it +drives, and makes that a Go library another tool can build on. Named VMs and +VMs with no shares come on top of it. + +Order of work, each step shippable and tested on its own: + +1. Vendor the patched dependencies under agent-vm's module path. +2. One folder per VM: `~/.agent-vm/vms//`, with the 0.3.0 migration. +3. `internal/engine`: the start sequence without terminal I/O. +4. The public packages, the CLI as their first consumer. +5. `--name` and `--no-shares`. + +## 1. Vendoring + +Today `third_party/` holds four modules (Lima, sshocker, pkg/sftp, +gvisor-tap-vsock), each its upstream commit plus `patches/`, wired in with +`replace`. A `replace` only applies in the main module: a tool importing +agent-vm would build against upstream, without the patches (read-only +`.git` names, network isolation, the SFTP fixes). Copying our `replace` +lines into every consumer is fragile, and breaks `go install` for it. + +They become packages of `github.com/sylvinus/agent-vm`: + +- `scripts/third-party-sync` builds each tree as upstream, then `patches/`, + then an import rewrite (`github.com/lima-vm/lima/v2` to + `github.com/sylvinus/agent-vm/third_party/lima`, and so on). `--check` + checks the result, rewrite included. Patches stay written against + upstream's paths, so they can still go upstream. +- The four `third_party/*/go.mod` go: their requirements move into ours. The + `replace` block and the `tool` directives go: the guest agent builds from + `./third_party/lima/cmd/lima-guestagent`. +- The `-X` path of Lima's version in the Makefile and `flake.nix` changes, + and `scripts/licenses` follows. +- Lima's `gomodjail` annotations in its `go.mod` are lost, and with them + `make gomodjail`, Lima's static check that the modules marked confined + reach no denied capability. agent-vm does not run it today. +- A consumer that also imports upstream Lima gets two copies under two + paths: slower to build, no conflict. + +Each patched tree gets a constant naming its patch level (for Lima, +`AgentVMPatches = "lima-5"`), which the engine refers to: a tree without the +patches does not compile. + +## 2. One folder per VM + +``` +~/.agent-vm/ + env, volumes, network, guarded, setup.sh, runtime.sh unchanged + locks/ + lima/ LIMA_HOME: Lima's global state only (_config, _networks) + vms// + lima/ Lima's instance folder + metadata.json what the markers hold today + policy.json the policies the VM boots with (section 4) + file-mounts/ staged single files of ~/.agent-vm/volumes +``` + +Deleting a VM is Lima's delete, then removing `vms/`: no marker can +outlive its VM, and `state.Forget` and `state.Rename` go. + +### Lima patch + +Lima wants every instance at `$LIMA_HOME/`. A `LIMA_HOME` per VM does +not work: with every instance named `lima`, the Windows drivers name their +WSL2 distro and HCS system `lima-lima` for all of them, and the guests' +hostnames and SSH aliases collide; named ``, the path holds the name +twice, which the socket path limit cannot afford (about 23 characters left +on macOS). + +A patch makes `dirnames.InstanceDir` return `//lima`, and +`store.Instances` and `store.Validate` (which joins the path by hand) follow. +Names stay unique, `_config` stays shared. `vm.MaxName` computes from the new +path: 4 characters less than today (42 for `/Users/sylvinus`). + +### What stays shared + +- `locks/`: the lock is a `flock` on a file kept forever. In + `vms//`, an `rm` would delete the file while a run waits on it, a + third run would create a new one, and two runs would hold the lock. +- Lima's `_config` (SSH key, networks). + +### metadata.json + +```json +{"schema": 1, + "base": {"name": "base", "built": 1760000000, "built_by": "0.4.0"}, + "term": "xterm-ghostty", + "bound": "/Users/x/proj", + "scratch_pid": 12345} +``` + +- Written only under the VM's lock, through a temporary file renamed over it. + Every write of a marker happens under that lock today. +- `base` in `vms/base/metadata.json` is the template itself, written last by + setup (what `.agent-vm-base-version` says today). In a VM it is the template + it was cloned from, copied by `cloneBase`: one field for both, so + `BaseLabel` has no special case. `name` is there for several templates + (krunkit, other tools' bases, section 7). +- `scratch_pid` is written before the clone: `vms//` exists before + Lima makes `lima/` in it. A `vms//` without `lima/lima.yaml` is a + creation that did not finish, removed unless its run is alive. +- An unreadable file is "nothing known": no `base` fails as a 0.1.0 base + does, no `bound` refuses a named VM. `doctor` names the file. +- `SSHFSOf`, `MountsOf`, `CacheOf` are not carried over. `PolicyOf` becomes + `policy.json`. + +### Migration from 0.3.0 + +One time, no way back. Run by the first command that changes VMs, under +`locks/migrate`: + +1. A VM still running cannot move (its hostagent holds sockets in its + folder): asked to stop on a terminal, as the 0.2 migration does, else left + for next time. +2. For each stopped `lima//` with a `lima.yaml`: `mkdir vms/`, + `os.Rename` into `vms//lima`, `metadata.json` from its markers, + `file-mounts/` moved, then its markers removed. `sshfs` without + `built-by` becomes `built_by: "0.1.0"`, which keeps the 0.1.0 check + working. +3. The base's `.agent-vm-base-*` go to `vms/base/metadata.json`. +4. Markers and `file-mounts/` of no VM are removed, and every `mounts` and + `sshfs-cache` marker. +5. Interrupted, it picks up where it stopped. + +`os.Rename` only: a copy would break the blocks shared with the base +(section 8). `EXDEV` stops the migration with the reason. + +0.2's migration moves its VMs into the 0.3.0 layout, then this one runs. + +0.3.0 after the migration sees no VM. The CHANGELOG says so. + +## 3. Engine + +`internal/cli/start.go` (and `checks.go`, `probe.go`, `policy.go`, +`commands.go`) runs the start sequence and prints and asks as it goes. +`internal/engine` does the same with no terminal: + +- Questions are typed (`hooks`, `git-config`, `bare-repo`, a restart to + change mode) and go to an `Asker`. No Asker is no, as with no terminal. +- Errors are typed (`ErrNameTooLong`, `ErrBaseMissing`, `*RefusedError`). + Progress and warnings are `Event`s. +- `context.Context` everywhere. The context that starts a VM is never the + one its hostagent lives by (see "Hostagent lifetime" below). +- It never reads `AGENT_VM_*`: the CLI turns them into options. + +The CLI keeps its output: the recorded `bashref` tests say so. + +## 4. Policies through the VM's folder + +Lima starts the program again as the VM's hostagent, and the network policy +and the write guard run there: today that process reads +`~/.agent-vm/network` and `guarded` itself. The engine writes +`vms//policy.json` (network rules, guarded paths, guard mode, app +name) before the VM boots, and the hostagent reads it from its instance +folder. Policies given as Go values reach it this way, and the file is the +record of what the VM booted with. + +Guarded writes: dialog, deny, or an allow-list. A Go callback would need the +hostagent to call the consumer's process: later, if needed. + +## 5. Library + +Public, v0.x and marked experimental until a real consumer has used it: + +| Package | Holds | +|---|---| +| `agentvm` (root, exists for the embeds) | `RunHelper`, `Open(Options)`, `Engine`, `VM`, `Spec`, `Asker`, `Question`, `Event`, errors | +| `agentvm/policy` | network rules, guarded paths and volumes as values, and the parsers of agent-vm's files | +| `agentvm/provision` | `Template{Name, Scripts, Preinstall, Env}`; `provision.AgentVM()` is `agent-vm.setup.sh` and its presets | +| `agentvm/agents` | `Agent{Name, Command, ...}`; the CLI's agent commands and their help come from the defaults | + +Internal: the Lima backend, building the mounts, the `.git` scan, the guard +and SFTP server, `limaembed`. They hold the guarantees (`.git` read-only, +shares confined, no share of home or state), which `Engine` enforces; an +`x/` package can expose lower layers later. + +```go +func main() { + agentvm.RunHelper() // Lima's hostagent re-exec; on macOS also main_darwin_gui.go's job + e, err := agentvm.Open(agentvm.Options{StateDir: "~/.mytool", AppName: "mytool", Asker: a}) + e.Setup(ctx, myTemplate) + vm := e.ProjectVM(dir) + vm.Ensure(ctx, agentvm.Spec{Base: "base-mytool"}) + vm.Exec(ctx, agentvm.Exec{Args: []string{"mytool-agent"}}) +} +``` + +- `Options` holds every feature, agent-vm's defaults otherwise: state folder + (required: each tool its own, and its own Lima home), app name, Asker, + events, network policy, guarded paths, volumes, env sources, runtime + scripts, `.git` protection, sshfs cache, host share. What lowers a + protection is named `Unsafe...`. +- One `Engine` per process: Lima's home is an environment variable, and + logrus is global. The library installs no signal handler and writes + nothing to stdout. +- `Open` fails, saying why, when `RunHelper` was not called. +- macOS: cgo, and the binary signed with Lima's `vz.entitlements`. +- Guest agent and templates: `go run + github.com/sylvinus/agent-vm/cmd/agent-vm-assets -o assets/` at build + time, embedded by the consumer, given as `Options.Assets` (`fs.FS`). + +`examples/consumer/` is a module of its own with no `replace`: CI builds it +and runs its e2e test. + +## 6. Named VMs, VMs with no shares + +Three choices, apart: + +| | default | flag | +|---|---|---| +| which VM | the folder's (`-`) | `--name NAME` | +| what is shared | the project and the volumes | `--no-shares` | +| how long it lives | kept | `--rm` | + +`--scratch` stays as it is: `--no-shares --rm`, a random name, the question +before deletion, the cleanup of what a killed run left. `--scratch --name` +is a usage error: `--no-shares --rm --name X` instead. + +```bash +agent-vm --name agent2 claude # a second VM on this folder +agent-vm --name sandbox --no-shares claude # nothing shared, kept, from any folder +agent-vm --name tmp --no-shares --rm run make +agent-vm stop agent2 / rm sandbox +``` + +A named VM belongs to the folder it was made in, or to none, recorded in +`metadata.json` (`bound`: the folder as `pwd` spells it, or `-`): + +- With shares: usable from its folder only. Elsewhere it stops, with "VM + 'agent2' belongs to /path. Run it from there, or agent-vm rm agent2". + Otherwise a VM that has seen one project would get another. +- With no shares: from any folder, `--no-shares` not needed again, and it + never gets shares. +- `--reset` re-clones it, the only way to change its binding. +- No `bound`: refused, `rm` suggested. +- `--no-shares` without `--name` is an error. + +Names: `^[a-z0-9]+(-[a-z0-9]+)*$`, at most 40 characters; lowercase because +macOS folders ignore case; 40 keeps `.localhost` (`agent-vm code`) a +DNS label. Not ending in `-[0-9a-f]{8}`, which every project and scratch name +does: names cannot meet. Not `base` nor `base-*`, the templates' and the +setup lock's. + +Two VMs on one folder: separate locks, probes, protections and editor ports +already. A port both listen on is forwarded for the first only, as between +projects today. They write the same tree: the docs point to git worktrees. + +`info [dir]`, `doctor`, and `stop`/`rm` with no argument keep meaning the +folder's VM. `list` marks the named VMs of the current folder too. + +## 7. krunkit + +The vendored Lima has a krunkit driver (macOS arm64), not imported yet. It +runs `krunkit`, a program of its own (Homebrew): agent-vm would no longer be +one binary there, and `doctor` checks it. Raw disks, like vz. + +Accounted for now: + +- Templates per driver: `vms/base`, `vms/base-krunkit`. Hence `base-*` + reserved, and `base.name` in `metadata.json`. +- The driver is Lima's `vmType`, in `lima.yaml`. A VM changes driver with + `--reset` only. + +To check when it is wired in: + +- Patch `0005` (network isolation) changes `usernet/gvproxy.go`, the + hostagent and the qemu driver; krunkit calls + `usernet.StartGVisorNetstack` itself. +- Its socket names against `vm.MaxName`. +- A separate process with GPU passthrough gives the guest more of the host + to reach than vz: the Security section says so. + +## 8. Disk space + +Lima clones with `continuity.CopyFile`: `clonefile` on APFS, so VMs share +the base's blocks until they write their own; `copy_file_range` on Linux, +which shares blocks on btrfs and XFS with reflink, and copies the data on +ext4. Holes are kept everywhere. + +The layout keeps the bases and the VMs on one filesystem, and the migration +renames. A new base shares nothing with the VMs of the old one, until +`--reset`. + +Next, by value: + +1. `list` shows the space a VM uses (allocated blocks), not its disk's size. +2. ext4: a qemu VM's disk as a qcow2 overlay on its base's, bases kept in + `~/.agent-vm/bases//` while a VM uses them. qemu only. +3. Giving back the space of deleted files needs discard through the virtual + disk and `fstrim` in the guest: to check for vz and krunkit. + +Not proposed: caches written by several VMs (Docker images, npm), which let +one project's VM plant files another one uses. Read-only shared caches are +fine. + +## Hostagent lifetime + +Upstream Lima starts the hostagent with `exec.CommandContext(ctx, ...)` +(`pkg/instance/start.go`), so canceling the context of the start kills it. +0.3.0 hands it the context of the command, which `endedBySignal` cancels on +SIGTERM, SIGHUP and when the command returns: the VM goes Broken. Lima +patch 0006 starts it with `exec.Command`. The engine keeps to that: the +context of a call never ends a VM, whose processes live by `Stop` and +`Delete` only. + +## Open questions + +1. `AGENT_VM_LIMA_HOME`: drop it, or keep it for Lima's global state only? +2. `vms//env`: an env layer per VM (`agent-vm env --name X set KEY`), + pushed after `~/.agent-vm/env` and before the project's, the place for the + secrets of a VM with no shares? Or only the name reserved? +3. The working folder of a VM with no shares: a fixed `~/work` (so + `claude --continue` finds its history from any host folder), or the host + path made in the VM, as `--scratch` does? +4. `list`: a column with the binding (folder, `none`, `scratch`)? +5. `AppName`: dialogs and messages only, or the guest side too + (`~/.agent-vm.env`, `/tmp/.agent-vm-file-mounts`)? +6. `policy.json` per VM as the only way to the hostagent? +7. Linux on ext4: plan the qcow2 overlays, or only show the space used? +8. A krunkit entry in `docs/BACKLOG.md`? + +## As built + +Where the code differs from the design, and the open questions as they were +settled by default, to revisit: + +1. Vendoring (`scripts/third-party-sync`): the import rewrite only touches + import lines (and the module path Lima's schema generator reads comments + of), not the strings of the `.pb.go` descriptors, which are + length-prefixed. The folders that would add requirements nothing uses are + left out (the last column of `third_party/SOURCES`: Lima's MCP server, + sshocker's command, gvisor-tap-vsock's commands and test suites). The + constant naming the patch level is `AgentVMPatches` in a generated + `agentvm_patches.go` of each tree ("lima-7": the number of the last + patch). `vendorHash` in `flake.nix` needs Nix to be recomputed. +2. Layout: Lima's patch 0007 adds `LIMA_INSTANCES_DIR`. `policy.json` is done + here, not in the engine step: the host agent reads the network rules and + the guarded paths from it, so a program's policies reach it as values. + Settled: `AGENT_VM_LIMA_HOME` stays, for Lima's global state only; the + migration is `state.MigrateLayout` (idempotent, run under `locks/migrate`); + `metadata.json` also holds `file_mounts` (the lines the + `.agent-vm-file-mounts-` marker held) and `scratch_pid` is -1 for a + record whose process number was unreadable. +3. Engine: `internal/engine`. Events are `Event{Level, VM, Title, Text}` with + the text as the command line prints it. Questions are `Question{Kind, ...}` + put to an `Asker` (`CanAsk` and `Ask`). Errors are `*Failure` (a kind that + `errors.Is` matches: `ErrBaseMissing`, `ErrAborted`...) and + `*RefusedError`. `Options` takes the volumes, the env and the runtime + script as paths of files in agent-vm's formats (`VolumesFile`, `EnvFile`, + `RuntimeScript`), the project's own by a function of the folder, and the + network and guarded policies as values (`Network`, `Guarded`) or by a + function called at each boot (`Policy`); not volumes as values yet. + `Setup` shows its steps through an `Output`, which the command line + draws as a window and a log. +4. Library: the root package re-exports the engine's types, and adds + `RunHelper` and `Open` (which refuses to open without `RunHelper`). The + setup script moved to `provision/agent-vm.setup.sh`. `agents`, `policy`, + `cmd/agent-vm-assets` and `examples/consumer` are as designed; the + example is built by `make test-example` in a copy of its module to which + the replace is added (it has none), because the module it requires is not + released yet. Settled: `AppName` is for the messages and dialogs, not the + guest side. +5. Named VMs: `bound` is the folder as `pwd` spells it (compared as a string), + or `-`. A VM with no shares gets its working folder as `--scratch` does: + the host path made in the VM (the other choice, a fixed `~/work`, is left + open). `list` marks them with `>` and has no column for the binding. + `vms//env` is not built. krunkit and the disk space ideas are in + `docs/BACKLOG.md`. diff --git a/examples/consumer/README.md b/examples/consumer/README.md new file mode 100644 index 00000000..220b000f --- /dev/null +++ b/examples/consumer/README.md @@ -0,0 +1,20 @@ +# sandbox + +A small program that embeds [agentvm](../..): it runs a command in a disposable Linux VM of the current folder, with nothing of the machine shared but that folder. + +```bash +go generate ./... # the guest agent and Lima's templates, into assets/ +go build -o sandbox . # on macOS, then: codesign -f --entitlements vz.entitlements -s - sandbox +sandbox setup # build the base VM, once +sandbox run make test # run a command in this folder's VM +sandbox shell +sandbox rm +``` + +Its VMs and settings are in `~/.sandbox` (`$SANDBOX_STATE_DIR`), apart from agent-vm's own. + +This module requires the released agent-vm and has no `replace`. In the agent-vm repository, `make test-example` builds it against the checkout, in a copy of its module, and runs its tests; `go test -tags e2e` boots a real VM. + +The base VM must have what the engine expects of a guest (zsh, sudo, a `~/.zshenv` that sources `~/.agent-vm.env`): see `provision.Template`. + +What it shows: `agentvm.RunHelper()` first in `main`; `Options` with `StateDir`, `AppName`, `Assets`, an `Asker` and an event callback; `Setup` with a `provision.Template`; `ProjectVM`, `Ensure` and `Exec`. diff --git a/examples/consumer/assets/README b/examples/consumer/assets/README new file mode 100644 index 00000000..2b900822 --- /dev/null +++ b/examples/consumer/assets/README @@ -0,0 +1,2 @@ +Filled by `go generate`: Lima's guest agent and templates, from agent-vm +(cmd/agent-vm-assets). Embedded in the program, given to agentvm.Options.Assets. diff --git a/examples/consumer/e2e_test.go b/examples/consumer/e2e_test.go new file mode 100644 index 00000000..6dd574e8 --- /dev/null +++ b/examples/consumer/e2e_test.go @@ -0,0 +1,50 @@ +//go:build e2e + +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +// A real VM: go test -tags e2e. Downloads Debian's image, boots it (slow +// without hardware acceleration), installs git and curl in the base, then +// runs a command in a project's VM, shares the folder and nothing else, and +// removes the VM. +func TestSandbox(t *testing.T) { + // A home of its own, where git is set as agent-vm asks: it would stop to + // ask, on a program that asks nothing. + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("GIT_CONFIG_NOSYSTEM", "1") + os.Unsetenv("GIT_CONFIG_GLOBAL") + os.Unsetenv("XDG_CONFIG_HOME") + os.WriteFile(filepath.Join(home, ".gitconfig"), []byte("[safe]\n\tbareRepository = explicit\n"), 0o644) + t.Setenv("SANDBOX_STATE_DIR", t.TempDir()) + proj := t.TempDir() + t.Chdir(proj) + t.Setenv("PWD", proj) + + var stdout, stderr bytes.Buffer + if code := run([]string{"setup"}, strings.NewReader(""), &stdout, &stderr); code != 0 { + t.Fatalf("setup: %d\n%s", code, stderr.String()) + } + stdout.Reset() + stderr.Reset() + + os.WriteFile("from-host", []byte("host\n"), 0o644) + script := `git --version >/dev/null && cat from-host && echo vm > from-vm` + if code := run([]string{"run", "sh", "-c", script}, strings.NewReader(""), &stdout, &stderr); code != 0 || stdout.String() != "host\n" { + t.Fatalf("run: %d %q\n%s", code, stdout.String(), stderr.String()) + } + // The folder is shared both ways. + if b, _ := os.ReadFile("from-vm"); string(b) != "vm\n" { + t.Errorf("from-vm on the host: %q", b) + } + if code := run([]string{"rm"}, strings.NewReader(""), &stdout, &stderr); code != 0 { + t.Errorf("rm: %d\n%s", code, stderr.String()) + } +} diff --git a/examples/consumer/go.mod b/examples/consumer/go.mod new file mode 100644 index 00000000..ff155702 --- /dev/null +++ b/examples/consumer/go.mod @@ -0,0 +1,9 @@ +module github.com/sylvinus/agent-vm/examples/consumer + +go 1.26.9 + +// No replace: this module builds against the agent-vm it requires, patched +// Lima included. In this repository it builds in a workspace with the root +// module (make test-example); once agent-vm 0.4.0 is released, `go mod tidy` +// pins it. +require github.com/sylvinus/agent-vm v0.4.0 diff --git a/examples/consumer/main.go b/examples/consumer/main.go new file mode 100644 index 00000000..45a93fb5 --- /dev/null +++ b/examples/consumer/main.go @@ -0,0 +1,191 @@ +// Command sandbox is a program that embeds agentvm: it runs a command in a +// disposable Linux VM of the current folder, with nothing of the machine +// shared but that folder. +// +// sandbox setup build the base VM, once +// sandbox run make test run a command in this folder's VM +// sandbox shell a shell in it +// sandbox rm delete it +// +// Its state, VMs included, is in ~/.sandbox ($SANDBOX_STATE_DIR): apart from +// agent-vm's own and from any other program that embeds agentvm. +package main + +//go:generate go run github.com/sylvinus/agent-vm/cmd/agent-vm-assets -o assets + +import ( + "bufio" + "context" + "embed" + "errors" + "fmt" + "io" + "io/fs" + "os" + "os/signal" + "strings" + + agentvm "github.com/sylvinus/agent-vm" + "github.com/sylvinus/agent-vm/provision" +) + +// The guest agent and Lima's templates, made by go generate. +// +//go:embed all:assets +var assetsFS embed.FS + +// baseName is this program's template: another than agent-vm's, so that the +// two programs' VMs never mix. +const baseName = "base-sandbox" + +func main() { + // Lima's host agent is this executable again: first thing. + agentvm.RunHelper() + os.Exit(run(os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) +} + +func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { + if len(args) == 0 { + fmt.Fprintln(stderr, "usage: sandbox setup | run [args] | shell | rm") + return 2 + } + assets, err := fs.Sub(assetsFS, "assets") + if err != nil { + fmt.Fprintln(stderr, err) + return 1 + } + stateDir := os.Getenv("SANDBOX_STATE_DIR") + if stateDir == "" { + stateDir = "~/.sandbox" + } + e, err := agentvm.Open(agentvm.Options{ + StateDir: stateDir, + AppName: "sandbox", + Assets: assets, + Asker: &askTerminal{in: bufio.NewReader(stdin), out: stderr}, + OnEvent: func(ev agentvm.Event) { printEvent(ev, stdout, stderr) }, + }) + if err != nil { + fmt.Fprintln(stderr, "sandbox:", err) + return 1 + } + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + switch args[0] { + case "setup": + return setup(ctx, e, stderr) + case "run", "shell": + cmd := args[1:] + if args[0] == "shell" { + cmd = []string{"zsh", "-l"} + } + if len(cmd) == 0 { + fmt.Fprintln(stderr, "usage: sandbox run [args]") + return 2 + } + return runIn(ctx, e, cmd, stdin, stdout, stderr) + case "rm": + v, err := e.ProjectVM("") + if err != nil { + fmt.Fprintln(stderr, "sandbox:", err) + return 1 + } + if !v.Remove(ctx) { + return 1 + } + return 0 + } + fmt.Fprintf(stderr, "sandbox: unknown command %q\n", args[0]) + return 2 +} + +// setup builds the base VM: Debian with zsh, git and curl, nothing else. The +// engine runs commands through a login zsh, whose ~/.zshenv reads the env +// file the engine pushes (~/.agent-vm.env): a template provides both. +func setup(ctx context.Context, e *agentvm.Engine, stderr io.Writer) int { + t := provision.Template{ + Name: baseName, + MemoryGiB: 2, + DiskGiB: 10, + Scripts: []provision.Script{{ + Name: "packages", + Message: "Installing zsh, git and curl...", + Shell: []string{"bash", "-l"}, + Content: `set -e +sudo apt-get update -qq +sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq zsh git curl +echo '[ -f "$HOME/.agent-vm.env" ] && . "$HOME/.agent-vm.env"' > "$HOME/.zshenv" +`, + }}, + } + if err := e.Setup(ctx, t, nil); err != nil { + fmt.Fprintln(stderr, "sandbox:", err) + return 1 + } + return 0 +} + +// runIn makes this folder's VM exist and run, then runs the command in it. +func runIn(ctx context.Context, e *agentvm.Engine, cmd []string, stdin io.Reader, stdout, stderr io.Writer) int { + v, err := e.ProjectVM("") + if err != nil { + fmt.Fprintln(stderr, "sandbox:", err) + return 1 + } + if err := v.Ensure(ctx, agentvm.Spec{Base: baseName}); err != nil { + if errors.Is(err, agentvm.ErrBaseMissing) { + fmt.Fprintln(stderr, "sandbox: run 'sandbox setup' first") + } + return 1 + } + code, err := v.Exec(ctx, agentvm.Exec{Args: cmd, Stdin: stdin, Stdout: stdout, Stderr: stderr}) + if err != nil { + fmt.Fprintln(stderr, "sandbox:", err) + return 1 + } + return code +} + +// printEvent shows what the engine tells: progress on stdout would mix with +// the command's output, so everything goes to stderr. +func printEvent(ev agentvm.Event, _, stderr io.Writer) { + if ev.Title != "" { + fmt.Fprintf(stderr, "== %s ==\n", ev.Title) + } + fmt.Fprintln(stderr, ev.Text) +} + +// askTerminal puts the engine's questions on the terminal. +type askTerminal struct { + in *bufio.Reader + out io.Writer +} + +func (a *askTerminal) CanAsk() bool { + f, ok := a.out.(*os.File) + if !ok { + return false + } + st, err := f.Stat() + return err == nil && st.Mode()&os.ModeCharDevice != 0 +} + +func (a *askTerminal) Ask(_ context.Context, q agentvm.Question) bool { + if q.Detail != "" { + fmt.Fprintf(a.out, "%s\n%s\n", q.Title, q.Detail) + } + def := "y/N" + if q.Default { + def = "Y/n" + } + fmt.Fprintf(a.out, "%s [%s] ", q.Prompt, def) + line, _ := a.in.ReadString('\n') + switch strings.ToLower(strings.TrimSpace(line)) { + case "y", "yes": + return true + case "n", "no": + return false + } + return q.Default +} diff --git a/examples/consumer/main_darwin.go b/examples/consumer/main_darwin.go new file mode 100644 index 00000000..e952f311 --- /dev/null +++ b/examples/consumer/main_darwin.go @@ -0,0 +1,14 @@ +//go:build darwin + +package main + +import "runtime" + +func init() { + // The host agent runs the VM on the process's first thread (vz needs it): + // see cmd/agent-vm/main_darwin_gui.go in agent-vm. The binary is also + // signed with the entitlements of third_party/lima/vz.entitlements: + // + // codesign -f --entitlements vz.entitlements -s - sandbox + runtime.LockOSThread() +} diff --git a/examples/consumer/main_test.go b/examples/consumer/main_test.go new file mode 100644 index 00000000..ac2b3a10 --- /dev/null +++ b/examples/consumer/main_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "bytes" + "os" + "strings" + "testing" + + agentvm "github.com/sylvinus/agent-vm" +) + +// Lima starts the test binary again as a host agent: the first thing a main, +// and a TestMain, does. +func TestMain(m *testing.M) { + agentvm.RunHelper() + os.Exit(m.Run()) +} + +func TestUsage(t *testing.T) { + t.Setenv("SANDBOX_STATE_DIR", t.TempDir()) + for _, c := range []struct { + args []string + want string + }{ + {nil, "usage: sandbox"}, + {[]string{"frobnicate"}, `unknown command "frobnicate"`}, + {[]string{"run"}, "usage: sandbox run"}, + } { + var stdout, stderr bytes.Buffer + if code := run(c.args, strings.NewReader(""), &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), c.want) { + t.Errorf("%q: %d %q", c.args, code, stderr.String()) + } + } +} + +// Before setup there is no base VM: said, and told how to go on, without +// Lima having started a VM. +func TestRunWithoutSetup(t *testing.T) { + t.Setenv("SANDBOX_STATE_DIR", t.TempDir()) + var stdout, stderr bytes.Buffer + code := run([]string{"run", "true"}, strings.NewReader(""), &stdout, &stderr) + if code != 1 || !strings.Contains(stderr.String(), "Run 'sandbox setup' first.") || !strings.Contains(stderr.String(), "sandbox: run 'sandbox setup' first") { + t.Errorf("%d %q", code, stderr.String()) + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..2899d811 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1792094642, + "narHash": "sha256-zp8ToDMnqZcu4o4IH9pdwXFXGp7yLm/M0dEnNWnyd7I=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "fe13242281a67641c0102cdb1087f0ebe586c7df", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..3f54f844 --- /dev/null +++ b/flake.nix @@ -0,0 +1,125 @@ +{ + description = "agent-vm: AI coding agents in a disposable Linux VM per project"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = + { self, nixpkgs }: + let + # Linux only for now: on macOS the binary needs cgo, the SDK and the + # virtualization entitlement (nixpkgs' lima signs it with + # darwin.sigtool), none of which this build sets up. + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system}); + + # The version, the build tags and Lima's version are the Makefile's and + # third_party/SOURCES's: there is one place to change each. + linesOf = file: builtins.filter builtins.isString (builtins.split "\n" (builtins.readFile file)); + lineWith = + file: prefix: + builtins.head ( + builtins.filter (l: builtins.substring 0 (builtins.stringLength prefix) l == prefix) (linesOf file) + ); + makeVar = + name: + let + prefix = "${name} := "; + in + builtins.substring (builtins.stringLength prefix) (-1) (lineWith ./Makefile prefix); + # lima + limaVersion = builtins.elemAt (builtins.filter (s: builtins.isString s && s != "") ( + builtins.split " +" (lineWith ./third_party/SOURCES "lima ") + )) 3; + in + { + packages = forAllSystems (pkgs: rec { + agent-vm = pkgs.buildGoModule { + pname = "agent-vm"; + version = makeVar "NEXT_VERSION"; + src = self; + + # The downloaded modules only: a vendor folder would hold + # third_party/ too, and change with each patch. + proxyVendor = true; + # Changes with go.mod and go.sum: set pkgs.lib.fakeHash here, build, + # and nix prints the new one. release.sh refuses a release when + # this checksum of `cat go.mod go.sum` is not the current one: + # update it with the hash. + # go.mod+go.sum: c31ced3cfd926565196a2feef87e7326d79cbf8ee00352a073bc529115b06932 + # 0.4.0 moved Lima and the others into this module: the hash was not + # computed (no Nix where it was changed). Build, and put the one nix + # prints here. + vendorHash = pkgs.lib.fakeHash; + + subPackages = [ "cmd/agent-vm" ]; + tags = pkgs.lib.splitString "," (makeVar "TAGS"); + env.CGO_ENABLED = "0"; + ldflags = [ + "-s" + "-w" + "-X github.com/sylvinus/agent-vm/internal/version.Version=${makeVar "NEXT_VERSION"}" + "-X github.com/sylvinus/agent-vm/third_party/lima/pkg/version.Version=${limaVersion}" + # QEMU comes with it, first on its PATH (cmd/agent-vm/main.go), as + # nixpkgs' lima does with a wrapper: a wrapped binary would make + # `agent-vm install` link past the wrapper. qemu_kvm, this host's + # architecture only, is a third smaller than qemu. ssh is the + # system's. + "-X main.extraPath=${pkgs.lib.makeBinPath [ pkgs.qemu_kvm ]}" + ]; + + nativeBuildInputs = [ pkgs.gzip ]; + + # go.mod names the Go the project is developed with, and nixpkgs + # has it a few days after a release: the build uses nixpkgs' (the + # sandbox cannot download a toolchain). + postPatch = '' + sed -i 's/^go [0-9.]*$/go ${pkgs.go.version}/' go.mod + ''; + + # What the Makefile's `assets` target does: the guest agent runs in + # the Linux guest, of this machine's architecture, and is embedded + # in the binary with Lima's templates. + preBuild = + let + guestArch = if pkgs.stdenv.hostPlatform.isAarch64 then "aarch64" else "x86_64"; + assets = "internal/limaembed/assets"; + in + '' + go build -trimpath -ldflags "-s -w -X github.com/sylvinus/agent-vm/third_party/lima/pkg/version.Version=${limaVersion}" \ + -o ${assets}/lima-guestagent.Linux-${guestArch} ./third_party/lima/cmd/lima-guestagent + gzip -9 -n -f ${assets}/lima-guestagent.Linux-${guestArch} + rm -rf ${assets}/templates + cp -R third_party/lima/templates ${assets}/templates + ''; + + # The module download shares preBuild with the build, and has no + # modules to build the guest agent from yet. + overrideModAttrs = _: { + preBuild = ""; + }; + + # The tests run in CI, on the three platforms. + doCheck = false; + + meta = { + description = "Run AI coding agents in a disposable Linux VM per project"; + homepage = "https://www.agent-vm.org/"; + license = pkgs.lib.licenses.mit; + mainProgram = "agent-vm"; + platforms = systems; + }; + }; + default = agent-vm; + }); + + apps = forAllSystems (pkgs: { + default = { + type = "app"; + program = "${self.packages.${pkgs.stdenv.hostPlatform.system}.agent-vm}/bin/agent-vm"; + }; + }); + }; +} diff --git a/go.mod b/go.mod index b54c5892..b9d4829a 100644 --- a/go.mod +++ b/go.mod @@ -2,29 +2,11 @@ module github.com/sylvinus/agent-vm go 1.26.9 -// Lima, sshocker and pkg/sftp are built from third_party/: upstream plus -// patches/ (see scripts/third-party-sync). These replaces only apply when -// this module is the main one, so `go install ...@latest` does not build -// agent-vm: releases ship binaries. -replace ( - github.com/containers/gvisor-tap-vsock => ./third_party/gvisor-tap-vsock - github.com/lima-vm/lima/v2 => ./third_party/lima - github.com/lima-vm/sshocker => ./third_party/sshocker - github.com/pkg/sftp => ./third_party/sftp -) - -tool ( - github.com/lima-vm/lima/v2/cmd/lima-guestagent - github.com/lima-vm/lima/v2/cmd/limactl -) - require ( al.essio.dev/pkg/shellescape v1.6.1 github.com/containerd/continuity v0.5.0 github.com/coreos/go-semver v0.3.1 github.com/docker/go-units v0.5.0 - github.com/lima-vm/lima/v2 v2.0.0 - github.com/lima-vm/sshocker v0.3.11 github.com/mattn/go-isatty v0.0.24 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/sirupsen/logrus v1.10.2 @@ -34,103 +16,106 @@ require ( ) require ( - github.com/AlecAivazis/survey/v2 v2.3.7 // indirect + github.com/AlecAivazis/survey/v2 v2.3.7 github.com/Code-Hex/go-infinity-channel v1.0.0 // indirect - github.com/Code-Hex/vz/v3 v3.7.1 // indirect - github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect - github.com/Microsoft/hcsshim v0.14.1 // indirect + github.com/Code-Hex/vz/v3 v3.7.1 + github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 + github.com/Microsoft/hcsshim v0.14.1 github.com/VividCortex/ewma v1.2.0 // indirect github.com/a8m/envsubst v1.4.3 // indirect github.com/agext/levenshtein v1.2.1 // indirect github.com/alecthomas/participle/v2 v2.1.4 // indirect - github.com/apparentlymart/go-cidr v1.1.1 // indirect + github.com/apparentlymart/go-cidr v1.1.1 github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/apparentlymart/go-textseg/v17 v17.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e // indirect + github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect github.com/buger/jsonparser v1.1.2 // indirect - github.com/cheggaaa/pb/v3 v3.2.1 // indirect - github.com/cilium/ebpf v0.22.0 // indirect + github.com/cheggaaa/pb/v3 v3.2.1 + github.com/cilium/ebpf v0.22.0 github.com/clipperhouse/uax29/v2 v2.2.0 // indirect github.com/containerd/cgroups/v3 v3.1.3 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/typeurl/v2 v2.3.0 // indirect - github.com/containers/gvisor-tap-vsock v0.8.9 // indirect - github.com/coreos/go-systemd/v22 v22.7.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 + github.com/cpuguy83/go-md2man/v2 v2.0.7 github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e // indirect - github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7 // indirect + github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7 github.com/dimchansky/utfbom v1.1.1 // indirect - github.com/diskfs/go-diskfs v1.9.4 // indirect + github.com/diskfs/go-diskfs v1.9.4 github.com/djherbis/times v1.6.0 // indirect github.com/elliotchance/orderedmap v1.8.0 // indirect github.com/fatih/color v1.19.0 // indirect + github.com/foxcpp/go-mockdns v1.2.0 github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/goccy/go-json v0.10.6 // indirect - github.com/goccy/go-yaml v1.19.2 // indirect + github.com/goccy/go-yaml v1.19.2 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/gopacket v1.1.19 // indirect - github.com/google/yamlfmt v0.21.0 // indirect + github.com/google/go-cmp v0.7.0 + github.com/google/gopacket v1.1.19 + github.com/google/yamlfmt v0.21.0 github.com/hashicorp/hcl/v2 v2.24.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 // indirect - github.com/insomniacslk/dhcp v0.0.0-20260901064844-234b97448fae // indirect - github.com/invopop/jsonschema v0.14.0 // indirect + github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 + github.com/insomniacslk/dhcp v0.0.0-20260901064844-234b97448fae + github.com/invopop/jsonschema v0.14.0 github.com/jinzhu/copier v0.4.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/kr/fs v0.1.0 // indirect - github.com/lima-vm/go-qcow2reader v0.8.0 // indirect - github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 // indirect + github.com/kr/fs v0.1.0 + github.com/lima-vm/go-qcow2reader v0.8.0 + github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 github.com/magiconair/properties v1.18.11 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-runewidth v0.0.27 // indirect - github.com/mattn/go-shellwords v1.0.15 // indirect - github.com/mdlayher/netlink v1.11.2 // indirect + github.com/mattn/go-shellwords v1.0.15 + github.com/mdlayher/netlink v1.11.2 github.com/mdlayher/socket v0.6.0 // indirect - github.com/mdlayher/vsock v1.3.0 // indirect + github.com/mdlayher/vsock v1.3.0 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect - github.com/miekg/dns v1.1.73 // indirect - github.com/mikefarah/yq/v4 v4.53.6 // indirect + github.com/miekg/dns v1.1.73 + github.com/mikefarah/yq/v4 v4.53.6 github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/nxadm/tail v1.4.11 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/pb33f/ordered-map/v2 v2.3.1 // indirect + github.com/nxadm/tail v1.4.11 + github.com/onsi/ginkgo v1.16.5 + github.com/onsi/gomega v1.39.1 + github.com/opencontainers/go-digest v1.0.0 + github.com/pb33f/ordered-map/v2 v2.3.1 github.com/pelletier/go-toml/v2 v2.4.3 // indirect github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pkg/sftp v1.13.11 // indirect - github.com/rjeczalik/notify v0.9.3 // indirect + github.com/rjeczalik/notify v0.9.3 github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect - github.com/sethvargo/go-password v0.4.0 // indirect - github.com/spakin/netpbm v1.3.2 // indirect - github.com/spf13/pflag v1.0.10 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 + github.com/sethvargo/go-password v0.4.0 + github.com/spakin/netpbm v1.3.2 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.12.1 github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/yuin/gopher-lua v1.1.2 // indirect github.com/zclconf/go-cty v1.19.0 // indirect go.opencensus.io v0.24.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect - golang.org/x/crypto v0.57.0 // indirect - golang.org/x/image v0.46.0 // indirect + golang.org/x/crypto v0.57.0 + golang.org/x/image v0.46.0 golang.org/x/mod v0.41.0 // indirect - golang.org/x/net v0.60.0 // indirect - golang.org/x/sync v0.23.0 // indirect - golang.org/x/text v0.42.0 // indirect + golang.org/x/net v0.60.0 + golang.org/x/sync v0.23.0 + golang.org/x/text v0.42.0 golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.49.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/grpc v1.83.2 // indirect - google.golang.org/protobuf v1.36.12 // indirect - gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 // indirect + google.golang.org/grpc v1.83.2 + google.golang.org/protobuf v1.36.12 + gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f // indirect + gotest.tools/v3 v3.5.2 + gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f ) diff --git a/go.sum b/go.sum index a507499c..a82ce01a 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/Code-Hex/go-infinity-channel v1.0.0 h1:M8BWlfDOxq9or9yvF9+YkceoTkDI1p github.com/Code-Hex/go-infinity-channel v1.0.0/go.mod h1:5yUVg/Fqao9dAjcpzoQ33WwfdMWmISOrQloDRn3bsvY= github.com/Code-Hex/vz/v3 v3.7.1 h1:EN1yNiyrbPq+dl388nne2NySo8I94EnPppvqypA65XM= github.com/Code-Hex/vz/v3 v3.7.1/go.mod h1:1LsW0jqW0r0cQ+IeR4hHbjdqOtSidNCVMWhStMHGho8= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= github.com/Microsoft/hcsshim v0.14.1 h1:CMuB3fqQVfPdhyXhUqYdUmPUIOhJkmghCx3dJet8Cqs= @@ -105,6 +107,8 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= @@ -116,6 +120,10 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= @@ -135,6 +143,7 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= @@ -150,6 +159,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -163,6 +174,7 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 h1:jaqViOFFlZtkAwqvwZN+id37fosQqR5l3Oki9Dk4hz8= @@ -214,6 +226,7 @@ github.com/mdlayher/vsock v1.3.0 h1:bqQfZ1OznI03y6YiXp2sze05RVdzLn/zsfjnjd4+ivI= github.com/mdlayher/vsock v1.3.0/go.mod h1:WsuksavOvwCnV5UqGHUkvAvCy+Dqy81y4goKQTzxxNY= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ= github.com/mikefarah/yq/v4 v4.53.6 h1:5vB15LTZRvrF6z/metQF31bmF63SjDzCKKX8o8UemQQ= @@ -222,10 +235,18 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc= +github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -270,6 +291,7 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -281,6 +303,7 @@ github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8 github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA= @@ -312,6 +335,9 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -322,37 +348,60 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -360,16 +409,30 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= @@ -381,8 +444,12 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -418,10 +485,14 @@ google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fw google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE= gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/cli/app.go b/internal/cli/app.go index 8cf38974..62011e79 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -5,9 +5,15 @@ import ( "fmt" "os" "path/filepath" - "sync" + agentvm "github.com/sylvinus/agent-vm" + "github.com/sylvinus/agent-vm/internal/engine" + "github.com/sylvinus/agent-vm/internal/env" + "github.com/sylvinus/agent-vm/internal/host" + "github.com/sylvinus/agent-vm/internal/mounts" "github.com/sylvinus/agent-vm/internal/paths" + "github.com/sylvinus/agent-vm/internal/policy" + "github.com/sylvinus/agent-vm/internal/runscript" "github.com/sylvinus/agent-vm/internal/state" "github.com/sylvinus/agent-vm/internal/ui" "github.com/sylvinus/agent-vm/internal/vm" @@ -20,43 +26,110 @@ type app struct { ui *ui.UI state state.Dir - migrateOnce sync.Once - migrate func() // moves 0.2's VMs, when set - backOnce sync.Once - newBack func() (vm.Backend, error) - back vm.Backend - backErr error + // migrate moves 0.2's VMs, when set: the first use of the backend by a + // command that changes VMs runs it (see Options.BeforeChange). + migrate func() + // backend replaces Lima, for tests. + testBackend vm.Backend + + eng *agentvm.Engine + engErr error } -// backend is made on first use: most commands never need Lima. The first -// use by a command that changes VMs also moves 0.2's VMs (migrateOld). -func (e *app) backend() (vm.Backend, error) { - e.migrateOnce.Do(func() { - if e.migrate != nil { - e.migrate() - } - }) - return e.readOnlyBackend() +// engine is the engine the commands run VMs with, opened on first use: most +// commands never need Lima. It is opened again for each command, so that the +// environment it reads is the command's. +func (e *app) engine() (*agentvm.Engine, error) { + if e.eng == nil && e.engErr == nil { + e.eng, e.engErr = agentvm.Open(e.engineOptions()) + } + return e.eng, e.engErr +} + +// engineOptions are the options agent-vm's command line gives the engine: +// its files in the state folder, and the AGENT_VM_* variables. +func (e *app) engineOptions() agentvm.Options { + div, _, _ := host.Divisor() + return agentvm.Options{ + StateDir: string(e.state), + LimaHome: LimaHome(e.state), + AppName: "agent-vm", + Asker: cliAsker{e}, + OnEvent: e.onEvent, + Backend: e.testBackend, + Policy: func() (*policy.File, error) { return policy.Current(string(e.state), "agent-vm") }, + + VolumesFile: e.state.Path("volumes"), + EnvFile: e.state.Path("env"), + RuntimeScript: e.state.Path("runtime.sh"), + ProjectEnvFile: env.ProjectEnvFile, + ProjectRuntime: runscript.ProjectPath, + + UnsafeWritableGit: os.Getenv("AGENT_VM_UNSAFE_WRITABLE_GIT") == "1", + SSHFSCache: mounts.CacheOn(), + HostShare: div, + Quiet: quiet(), + NeverShare: []string{oldLimaHome()}, + TERM: os.Getenv("TERM"), + BeforeChange: func(context.Context) { + if e.migrate != nil { + e.migrate() + } + }, + } +} + +// onEvent prints what the engine tells: progress on stdout, the rest on +// stderr. +func (e *app) onEvent(ev agentvm.Event) { + switch { + case ev.Title != "": + e.ui.Box(ev.Title, ev.Text) + case ev.Level == agentvm.Info: + fmt.Fprintln(e.io.Stdout, ev.Text) + default: + fmt.Fprintln(e.io.Stderr, ev.Text) + } +} + +// cliAsker puts the engine's questions to the user on the terminal. +type cliAsker struct{ e *app } + +func (a cliAsker) CanAsk() bool { return a.e.ui.CanAsk() } + +func (a cliAsker) Ask(_ context.Context, q agentvm.Question) bool { + if q.Title != "" && q.Detail != "" { + a.e.ui.Box(q.Title, q.Detail) + } + return a.e.ui.AskYN(q.Prompt, q.Default) +} + +// backend is the backend for a command that changes VMs: the first use also +// moves the VMs of older versions. +func (e *app) backend(ctx context.Context) (vm.Backend, error) { + eng, err := e.engine() + if err != nil { + return nil, err + } + return eng.Change(ctx) } -// readOnlyBackend is backend without the move of 0.2's VMs, for commands -// that only look (list, status, doctor, info, install). A command that goes -// on to change VMs asks for backend, which moves them then. +// readOnlyBackend is the backend for the commands that only look (list, +// status, doctor, info, install): no VM is moved. A command that goes on to +// change VMs asks for backend, which moves them then. func (e *app) readOnlyBackend() (vm.Backend, error) { - e.backOnce.Do(func() { e.back, e.backErr = e.newBack() }) - return e.back, e.backErr + eng, err := e.engine() + if err != nil { + return nil, err + } + return eng.Backend() } // migratedMarker says the VMs of 0.2 were moved into agent-vm's Lima home. const migratedMarker = ".agent-vm-lima-migrated" // oldLimaHome is where 0.2 kept its VMs: Lima's home, $LIMA_HOME or ~/.lima. -func oldLimaHome() string { - if h := vm.UserLimaHome(); h != "" { - return paths.Host(absOr(h)) - } - return paths.Host(filepath.Join(paths.Home(), ".lima")) -} +func oldLimaHome() string { return engine.UserLimaHome() } // absOr is p made absolute, as it is when that cannot be done: a relative // folder would defeat the checks that compare locations. @@ -84,6 +157,15 @@ func (e *app) unmovedOld() ([]vm.OldVM, string) { return vms, old } +// layoutPending are the VMs of 0.3.0 that have not been moved yet to a +// folder each. None for an app with no move wired. +func (e *app) layoutPending() []string { + if e.migrate == nil { + return nil + } + return e.state.LegacyVMs(LimaHome(e.state)) +} + // migrateOld moves, once, the VMs agent-vm 0.2 made in the user's Lima home // into its own (see LimaHome). Asked on a terminal, yes by default. Without // one, the stopped ones are moved and the running ones left: moving stops @@ -129,7 +211,7 @@ func (e *app) migrateOld(ctx context.Context, old, now string) { failed = true continue } - if err := e.state.Rename(v.Name, to); err != nil { + if err := e.state.LegacyRename(v.Name, to); err != nil { fmt.Fprintf(w, "Warning: %s moved, but not all that agent-vm knows of it: %v\n", to, err) } } @@ -144,24 +226,18 @@ func (e *app) migrateOld(ctx context.Context, old, now string) { // sameDir reports whether a and b are one folder, links resolved where they // exist. -func sameDir(a, b string) bool { - if fa, err := os.Stat(a); err == nil { - if fb, err := os.Stat(b); err == nil { - return os.SameFile(fa, fb) - } - } - return filepath.Clean(a) == filepath.Clean(b) -} +func sameDir(a, b string) bool { return engine.SameDir(a, b) } -// LimaHome is where agent-vm's Lima keeps its VMs: its own, so that the -// user's limactl, of another version, never runs them, and nothing of the -// user's own Lima is touched. AGENT_VM_LIMA_HOME overrides it, made +// LimaHome is agent-vm's own Lima home: what Lima keeps for every VM (its +// SSH key, its networks), so that the user's limactl, of another version, +// never runs the VMs, and nothing of the user's own Lima is touched. The VMs +// are in the state folder's vms/. AGENT_VM_LIMA_HOME overrides it, made // absolute. func LimaHome(st state.Dir) string { if h := os.Getenv("AGENT_VM_LIMA_HOME"); h != "" { return paths.Host(absOr(h)) } - return st.Path("lima") + return st.LimaHome() } func newApp(io IO) (*app, error) { @@ -172,17 +248,13 @@ func newApp(io IO) (*app, error) { u := ui.New() u.Stderr = io.Stderr e := &app{io: io, ui: u, state: st} + e.migrate = func() { e.migrateOld(context.Background(), oldLimaHome(), LimaHome(st)) } // Names that fit this machine's socket paths (a long home, a long user // name): cut in their folder part, not refused. - vmname.MaxLen = vm.MaxName(LimaHome(st)) - e.migrate = func() { e.migrateOld(context.Background(), oldLimaHome(), LimaHome(st)) } - e.newBack = func() (vm.Backend, error) { - // Every VM there is agent-vm's: list, rm and destroy-all take them - // all. Never a Lima home the user's own VMs live in. - if h := LimaHome(st); sameDir(h, oldLimaHome()) { - return nil, fmt.Errorf("agent-vm's Lima home %s is also your own Lima's home: set AGENT_VM_LIMA_HOME to a folder for agent-vm alone", h) - } - return vm.NewLima(LimaHome(st), string(st)) - } + vmname.MaxLen = vm.MaxName(st.VMs()) return e, nil } + +// quiet is AGENT_VM_QUIET=1: notices that are not about a risk to the host +// are not printed. +func quiet() bool { return os.Getenv("AGENT_VM_QUIET") == "1" } diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 03f7867d..d0d31ed2 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -12,6 +12,7 @@ import ( "regexp" "strings" + "github.com/sylvinus/agent-vm/agents" "github.com/sylvinus/agent-vm/internal/version" "github.com/sylvinus/agent-vm/internal/vmname" ) @@ -73,6 +74,8 @@ func Main(args []string, io IO) int { // main runs the command line. An app made only of io is enough for the // commands outside stateful. func (e *app) main(ctx context.Context, args []string) int { + // The engine reads the environment when it is opened: once per command. + e.eng, e.engErr = nil, nil io := e.io opts := newVMOpts() lead, args, err := opts.takeAll(args) @@ -112,8 +115,6 @@ func (e *app) main(ctx context.Context, args []string) int { return e.sharedEnvCmd(args) case "project-env": return e.projectEnvCmd(ctx, args) - case "claude", "opencode", "codex", "vibe", "pi": - return e.agentCmd(ctx, cmd, opts, args) case "shell", "sh": return e.shellCmd(ctx, opts, args) case "run": @@ -129,6 +130,9 @@ func (e *app) main(ctx context.Context, args []string) int { case "code": return e.codeCmd(ctx, opts, args) default: + if a, ok := agents.Lookup(cmd); ok { + return e.agentCmd(ctx, a, opts, args) + } fmt.Fprintf(io.Stderr, "Unknown command: %s\nRun 'agent-vm help' for usage.\n", cmd) return exitUsage } @@ -171,8 +175,26 @@ func versionCmd(args []string, io IO) int { return 1 } -// updateCommand is how this copy of agent-vm is updated. +// updateCommand is how this copy of agent-vm is updated: by what installed +// it. The installer's copy would otherwise land in ~/.local/bin, ahead of a +// package's on the PATH, and hide its updates. func updateCommand() string { + exe, err := self() + if err != nil { + exe = "" + } + return updateCommandFor(exe) +} + +func updateCommandFor(exe string) string { + switch { + case strings.HasPrefix(exe, "/nix/store/"): + return "with Nix, as installed (nix profile upgrade agent-vm, or the flake input)" + case strings.Contains(exe, "/Cellar/agent-vm/"): + return "brew upgrade agent-vm" + case exe == "/usr/bin/agent-vm": + return "install the .deb or .rpm of https://github.com/sylvinus/agent-vm/releases/latest" + } return "curl -fsSL https://www.agent-vm.org/install.sh | sh" } diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index b1ab42cc..eec80ace 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -94,6 +94,21 @@ func TestHelp(t *testing.T) { " ~/.agent-vm/network What the VMs may reach (above)\n", " ~/.agent-vm/guarded More files to ask about (above)\n", " licenses Print the licenses of agent-vm and of what is built into it\n", + // 0.4's named VMs. + " --name NAME The VM called NAME, not the folder's own, so that a\n" + + " folder can have several. It belongs to the folder it is\n" + + " made in and runs from there only ('rm NAME' to give it\n" + + " up, --reset to make it again). NAME: lowercase letters,\n" + + " digits and dashes, at most 40 characters, not ending in\n" + + " a dash and 8 hex digits, not 'base' nor 'base-...'.\n" + + " --no-shares With --name, when the VM is made: it shares nothing (as\n" + + " --scratch), is kept, and runs from any folder. It never\n" + + " gets shares. The folder's path is made in the VM as an\n" + + " empty working folder. ~/.agent-vm/env and runtime.sh go\n" + + " in, the project's files do not.\n", + " agent-vm --name agent2 claude # A second VM on this folder\n", + " agent-vm --name sandbox --no-shares claude # Nothing shared, kept, from any folder\n", + " agent-vm stop agent2 # Stop a VM by name (see 'list')\n", } { s = strings.Replace(s, cut, "", 1) } @@ -114,6 +129,23 @@ func TestHelp(t *testing.T) { } } +// A copy a package manager installed is updated by it: the installer's +// would land ahead of it on the PATH. +func TestUpdateCommand(t *testing.T) { + for exe, want := range map[string]string{ + "/nix/store/abc-agent-vm-0.3.1/bin/agent-vm": "nix profile upgrade", + "/opt/homebrew/Cellar/agent-vm/0.3.1/bin/agent-vm": "brew upgrade agent-vm", + "/usr/bin/agent-vm": ".deb or .rpm", + "/home/u/.local/share/agent-vm/0.3.1/agent-vm": "install.sh | sh", + "/usr/local/bin/agent-vm": "install.sh | sh", + "": "install.sh | sh", + } { + if got := updateCommandFor(exe); !strings.Contains(got, want) { + t.Errorf("%q: %q", exe, got) + } + } +} + func TestSplit(t *testing.T) { for _, c := range []struct { in []string diff --git a/internal/cli/code.go b/internal/cli/code.go index 8fcbbc47..78bc32c4 100644 --- a/internal/cli/code.go +++ b/internal/cli/code.go @@ -140,7 +140,7 @@ func (s *start) codeSession() int { for _, p := range ports { args = append(args, strconv.Itoa(p)) } - b, err := s.backend() + b, err := s.backend(s.ctx) if err != nil { return 1 } diff --git a/internal/cli/commands.go b/internal/cli/commands.go index 73c2245f..fc6f2661 100644 --- a/internal/cli/commands.go +++ b/internal/cli/commands.go @@ -5,18 +5,49 @@ import ( "fmt" "os" "os/signal" - "strconv" - "strings" "syscall" "time" - "github.com/sylvinus/agent-vm/internal/paths" - "github.com/sylvinus/agent-vm/internal/state" + agentvm "github.com/sylvinus/agent-vm" + "github.com/sylvinus/agent-vm/agents" + "github.com/sylvinus/agent-vm/internal/engine" "github.com/sylvinus/agent-vm/internal/ui" - "github.com/sylvinus/agent-vm/internal/vm" "github.com/sylvinus/agent-vm/internal/vmname" ) +// start is one run of a command in a project's VM. +type start struct { + *app + ctx context.Context + opts VMOpts + vm *agentvm.VM + name string // the VM + dir string // the project, as the shell spells it +} + +func (s *start) warnf(format string, a ...any) { + fmt.Fprintf(s.io.Stderr, format+"\n", a...) +} + +// spec is what the options ask of the VM. +func (o VMOpts) spec() agentvm.Spec { + sp := agentvm.Spec{ + CPUs: o.CPUs, MemoryGiB: o.Memory, DiskGiB: o.Disk, + Reset: o.Reset, ReadOnly: o.ReadOnly, NoShares: o.NoShares || o.Scratch, UnsafeWritableGit: o.UnsafeWritableGit, + } + if o.SSHPort >= 0 { + p := o.SSHPort + sp.SSHPort = &p + } + switch { + case o.UnsafeNoPrompts: + sp.UnsafeNoPrompts = "--unsafe-disable-security-prompts" + case os.Getenv("AGENT_VM_UNSAFE_DISABLE_SECURITY_PROMPTS") == "1": + sp.UnsafeNoPrompts = "AGENT_VM_UNSAFE_DISABLE_SECURITY_PROMPTS=1" + } + return sp +} + // session is what runs in the VM once it is up: an agent, a shell, a // command. It returns the exit status. type session func(s *start) int @@ -48,6 +79,17 @@ func endedBySignal(parent context.Context) (ctx context.Context, stop func()) { } } +// removeVM stops and deletes name, saying so, and reports whether it is gone. +func (e *app) removeVM(ctx context.Context, name string) bool { + fmt.Fprintf(e.io.Stdout, "Stopping and deleting VM '%s'...\n", name) + eng, err := e.engine() + if err != nil || !eng.Delete(ctx, name) { + return false + } + fmt.Fprintln(e.io.Stdout, "VM destroyed.") + return true +} + // inVM starts this directory's VM with opts, runs fn, then deletes the VM // when --rm asked: after fn whatever it returns, and after a start that // failed when this run made the VM (a VM that was there before is not @@ -55,6 +97,10 @@ func endedBySignal(parent context.Context) (ctx context.Context, stop func()) { // (SIGTERM, SIGHUP) ends the command and still deletes. --scratch: see // scratch. func (e *app) inVM(ctx context.Context, opts VMOpts, fn session) int { + if err := opts.check(); err != nil { + fmt.Fprintf(e.io.Stderr, "Error: %v\n", err) + return exitUsage + } dir, err := vmname.AbsDir("") if err != nil { fmt.Fprintf(e.io.Stderr, "Error: %v\n", err) @@ -66,9 +112,24 @@ func (e *app) inVM(ctx context.Context, opts VMOpts, fn session) int { if opts.Scratch { return e.scratch(ctx, cleanup, opts, dir, fn) } - s := &start{app: e, ctx: ctx, opts: opts, dir: dir, name: vmname.Name(dir)} + eng, err := e.engine() + if err != nil { + fmt.Fprintf(e.io.Stderr, "Error: %v\n", err) + return 1 + } + var v *agentvm.VM + if opts.Name != "" { + v, err = eng.NamedVM(opts.Name, dir) + } else { + v, err = eng.ProjectVM(dir) + } + if err != nil { + fmt.Fprintf(e.io.Stderr, "Error: %v\n", err) + return 1 + } + s := &start{app: e, ctx: ctx, opts: opts, vm: v, dir: dir, name: v.Name} st := 1 - started := s.ensureRunning() == nil + started := v.Ensure(ctx, opts.spec()) == nil if started { s.printResources() // A Ctrl-C is the command's, in the VM, as a shell leaves it to the job @@ -79,7 +140,7 @@ func (e *app) inVM(ctx context.Context, opts VMOpts, fn session) int { signal.Stop(sig) ui.ResetTermModes(os.Stdout) } - if opts.RM && (started || s.created) && !e.removeVM(cleanup, s.name) && st == 0 { + if opts.RM && (started || v.Created()) && !e.removeVM(cleanup, s.name) && st == 0 { st = 1 } return st @@ -88,33 +149,22 @@ func (e *app) inVM(ctx context.Context, opts VMOpts, fn session) int { // printResources says what the VM has, and when the base it was cloned // from was built: its agents and packages are that old. func (s *start) printResources() { - if inst, err := s.get(s.name); err == nil && inst != nil { - fmt.Fprintf(s.io.Stdout, " Resources: CPUs: %d, Memory: %d GiB, Disk: %d GiB\n", inst.CPUs, inst.Memory>>30, inst.Disk>>30) + d, err := s.vm.Details(s.ctx) + if err != nil || d == nil { + return } - if built, err := strconv.ParseInt(s.state.ReadMarker(state.VersionOf, s.name), 10, 64); err == nil && built >= 0 { - fmt.Fprintf(s.io.Stdout, " Base VM: built %s\n", time.Unix(built, 0).Format("2006-01-02")) + if inst := d.Instance; inst != nil { + fmt.Fprintln(s.io.Stdout, engine.ResourcesLine(inst)) + } + if b := d.Base; b != nil && b.Built > 0 { + fmt.Fprintf(s.io.Stdout, " Base VM: built %s\n", time.Unix(b.Built, 0).Format("2006-01-02")) } } -// lima runs args in the VM through a login zsh, from the project: agent PATH -// entries and ~/.agent-vm.env are in ~/.zshenv. The command and its -// arguments are positional parameters, never re-parsed; `env --` runs it, -// leading VAR=value assignments working and a command starting with - not -// taken for env's option. A command that is not there is said so, with -// env's status (an agent installed only as an editor extension has none); -// a path is env's to run, and to say why it cannot (126). +// lima runs args in the VM through a login zsh, from the project (see +// engine.VM.Exec). func (s *start) lima(tty bool, args ...string) int { - b, err := s.backend() - if err != nil { - return 1 - } - code, err := b.Shell(s.ctx, s.name, vm.ShellOpts{ - Workdir: paths.Guest(s.dir), - Args: append([]string{"zsh", "-l", "-c", - `case "$1" in *=*|*/*) ;; *) command -v -- "$1" >/dev/null || { print -r -- "agent-vm: $1 is not installed in this VM." >&2; exit 127; } ;; esac; exec env -- "$@"`, - "agent-vm"}, args...), - Stdin: s.io.Stdin, Stdout: s.io.Stdout, Stderr: s.io.Stderr, Interactive: tty, - }) + code, err := s.vm.Exec(s.ctx, agentvm.Exec{Args: args, TTY: tty, Stdin: s.io.Stdin, Stdout: s.io.Stdout, Stderr: s.io.Stderr}) if err != nil { s.warnf("Error: %v", err) return 1 @@ -122,32 +172,16 @@ func (s *start) lima(tty bool, args ...string) int { return code } -// The agents, with the flag that lets each work unattended (safe in the -// VM), and a terminal for the full-screen ones. -var agents = map[string]struct { - cmd []string - tty bool -}{ - // The VM also enforces bypass mode with managed settings: Claude Code - // drops the flag when it relaunches itself. - "claude": {[]string{"claude", "--dangerously-skip-permissions"}, false}, - // Approves every prompt that is not explicitly denied. - "opencode": {[]string{"opencode", "--auto"}, true}, - "codex": {[]string{"codex", "--dangerously-bypass-approvals-and-sandbox"}, false}, - "vibe": {[]string{"vibe", "--agent", "auto-approve"}, true}, - // No permission prompts: it runs every tool as asked. - "pi": {[]string{"pi"}, true}, -} - -func (e *app) agentCmd(ctx context.Context, agent string, opts VMOpts, args []string) int { - a := agents[agent] +// agentCmd runs one of the agents (see package agents), with the flag that +// lets it work unattended, and a terminal for the full-screen ones. +func (e *app) agentCmd(ctx context.Context, a agents.Agent, opts VMOpts, args []string) int { rest, tty, err := opts.split(args) if err != nil { fmt.Fprintf(e.io.Stderr, "Error: %v\n", err) return exitUsage } - cmd := append(append([]string(nil), a.cmd...), rest...) - return e.inVM(ctx, opts, func(s *start) int { return s.lima(a.tty || tty, cmd...) }) + cmd := append(append([]string(nil), a.Command...), rest...) + return e.inVM(ctx, opts, func(s *start) int { return s.lima(a.TTY || tty, cmd...) }) } // `run [--tty] [args]`: --tty gives the command a terminal, which @@ -205,21 +239,12 @@ func (e *app) shellCmd(ctx context.Context, opts VMOpts, args []string) int { }) } -// shellTo runs args in the VM from the project, on the terminal. -func (s *start) shellTo(tty bool, args ...string) (int, error) { - b, err := s.backend() - if err != nil { - return 1, err - } - return b.Shell(s.ctx, s.name, vm.ShellOpts{Workdir: paths.Guest(s.dir), Args: args, - Stdin: s.io.Stdin, Stdout: s.io.Stdout, Stderr: s.io.Stderr, Interactive: tty}) -} - -// shell is shellTo's exit status. When the shell could not run at all (the -// VM is not running, ssh fails), the reason is said and the status is 1: a -// caller must not read that as the command's success. +// shell runs args in the VM from the project, on the terminal, and returns +// the exit status. When the shell could not run at all (the VM is not +// running, ssh fails), the reason is said and the status is 1: a caller must +// not read that as the command's success. func (s *start) shell(tty bool, args ...string) int { - code, err := s.shellTo(tty, args...) + code, err := s.vm.Shell(s.ctx, agentvm.Exec{Args: args, TTY: tty, Stdin: s.io.Stdin, Stdout: s.io.Stdout, Stderr: s.io.Stderr}) if err != nil { s.warnf("Error: %v", err) return 1 @@ -234,27 +259,29 @@ func (s *start) shell(tty bool, args ...string) int { // the next scratch run deletes it. cleanup is the context of the deletions, // which a kill of this run (ctx) does not end. func (e *app) scratch(ctx, cleanup context.Context, opts VMOpts, dir string, fn session) int { - dead, unknown := e.scratchLeftovers() + eng, err := e.engine() + if err != nil { + fmt.Fprintf(e.io.Stderr, "Error: %v\n", err) + return 1 + } + dead, unknown := eng.ScratchLeftovers() for _, left := range dead { fmt.Fprintf(e.io.Stdout, "Deleting scratch VM '%s', left by a run that did not finish...\n", left) - e.deleteVM(cleanup, left) + eng.Delete(cleanup, left) } for _, left := range unknown { fmt.Fprintf(e.io.Stderr, "Note: scratch VM '%s' has a record without a process number, so it is not known whether a run still uses it: left alone. 'agent-vm rm %s' deletes it.\n", left, left) } - name, err := vmname.Scratch(dir) + v, err := eng.NewScratchVM(dir) if err != nil { fmt.Fprintf(e.io.Stderr, "Error: %v\n", err) return 1 } - if err := e.state.Write(state.ScratchOf+name, []byte(strconv.Itoa(os.Getpid())+"\n")); err != nil { - fmt.Fprintf(e.io.Stderr, "Error: could not record scratch VM '%s': %v\n", name, err) - return 1 - } + name := v.Name opts.RM = true - s := &start{app: e, ctx: ctx, opts: opts, dir: dir, name: name} + s := &start{app: e, ctx: ctx, opts: opts, vm: v, dir: dir, name: name} st := 1 - if s.ensureRunning() == nil { + if v.Ensure(ctx, opts.spec()) == nil { s.printResources() sig := make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt) @@ -272,31 +299,8 @@ func (e *app) scratch(ctx, cleanup context.Context, opts VMOpts, dir string, fn } } fmt.Fprintf(e.io.Stdout, "Deleting scratch VM '%s'...\n", name) - if !e.deleteVM(cleanup, name) { + if !eng.Delete(cleanup, name) { st = 1 } return st } - -// scratchLeftovers are the scratch VMs a run could not delete (killed, its -// terminal closed), by their record: dead are those recorded by a run whose -// pid is gone (a reused pid only delays the cleanup), unknown those whose -// record holds no pid, which is no proof that no run uses them. Only names -// Scratch makes. -func (e *app) scratchLeftovers() (dead, unknown []string) { - ents, _ := os.ReadDir(string(e.state)) - for _, f := range ents { - name, ok := strings.CutPrefix(f.Name(), state.ScratchOf) - if !ok || !vmname.IsScratch(name) { - continue - } - pid, err := strconv.Atoi(strings.TrimSpace(e.state.Read(f.Name()))) - switch { - case err != nil || pid <= 0: - unknown = append(unknown, name) - case !alive(pid): - dead = append(dead, name) - } - } - return dead, unknown -} diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 177aa129..dd022648 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -9,12 +9,12 @@ import ( "os/exec" "runtime" "slices" - "strconv" "strings" "time" - limaversion "github.com/lima-vm/lima/v2/pkg/version" + limaversion "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" + "github.com/sylvinus/agent-vm/internal/engine" "github.com/sylvinus/agent-vm/internal/env" "github.com/sylvinus/agent-vm/internal/gitguard" "github.com/sylvinus/agent-vm/internal/guard" @@ -22,7 +22,6 @@ import ( "github.com/sylvinus/agent-vm/internal/mounts" "github.com/sylvinus/agent-vm/internal/netguard" "github.com/sylvinus/agent-vm/internal/runscript" - "github.com/sylvinus/agent-vm/internal/state" "github.com/sylvinus/agent-vm/internal/version" "github.com/sylvinus/agent-vm/internal/vm" "github.com/sylvinus/agent-vm/internal/vmname" @@ -86,7 +85,7 @@ func (e *app) doctorCmd(ctx context.Context, args []string) int { } else { d.line("warn", "could not read the host's CPU or RAM: --cpus and --memory are not clamped") } - dir := LimaHome(e.state) + dir := e.state.VMs() if _, err := os.Stat(dir); err != nil { dir, _ = os.UserHomeDir() } @@ -100,7 +99,7 @@ func (e *app) doctorCmd(ctx context.Context, args []string) int { } d.section("Lima") - d.line("info", fmt.Sprintf("Lima %s, built in; its VMs in %s", strings.TrimPrefix(limaversion.Version, "v"), LimaHome(e.state))) + d.line("info", fmt.Sprintf("Lima %s, built in; its VMs in %s", strings.TrimPrefix(limaversion.Version, "v"), e.state.VMs())) for _, f := range vm.LimaOverrides(LimaHome(e.state)) { d.line("fail", f+" adds to every VM's config: agent-vm makes and starts no VM while it is there", "Move it out of the way.") } @@ -152,6 +151,10 @@ func (e *app) doctorCmd(ctx context.Context, args []string) int { d.line("info", fmt.Sprintf("%d VM(s) of agent-vm 0.2 in %s, not moved yet", len(vms), old), "The next agent-vm command that starts or removes a VM offers to move them.") } + if left := e.layoutPending(); len(left) > 0 { + d.line("info", fmt.Sprintf("%d VM(s) of agent-vm 0.3.0 in %s, not moved to %s yet", len(left), LimaHome(e.state), e.state.VMs()), + "The next agent-vm command that starts or removes a VM moves them.") + } getVM := func(name string) (*vm.Instance, error) { if berr != nil { return nil, berr @@ -163,20 +166,16 @@ func (e *app) doctorCmd(ctx context.Context, args []string) int { return inst, err } base, err := getVM(vmname.Template) - _, merr := os.Stat(e.state.Path(state.BaseVersion)) switch { case err != nil: d.line("warn", "could not query Lima") - case base != nil && merr == nil: - if built, perr := strconv.ParseInt(e.state.Read(state.BaseVersion), 10, 64); perr == nil { - d.line("ok", fmt.Sprintf("%s is ready, built %s", vmname.Template, time.Unix(built, 0).Format("2006-01-02"))) - if time.Since(time.Unix(built, 0)) > 90*24*time.Hour { - d.line("warn", "the base template is over 90 days old", "Its agents and packages are as old. 'agent-vm setup' rebuilds it.") - } - } else { - d.line("ok", vmname.Template+" is ready") + case base != nil && e.state.BaseReady(vmname.Template): + built := e.state.Known(vmname.Template).Base.Built + d.line("ok", fmt.Sprintf("%s is ready, built %s", vmname.Template, time.Unix(built, 0).Format("2006-01-02"))) + if time.Since(time.Unix(built, 0)) > 90*24*time.Hour { + d.line("warn", "the base template is over 90 days old", "Its agents and packages are as old. 'agent-vm setup' rebuilds it.") } - if _, err := os.Stat(e.state.Path(state.BaseBuiltBy)); err != nil { + if !engine.BuiltWithSSHFS(e.state.Known(vmname.Template).Base) { d.line("warn", "the base template was built by agent-vm 0.1.0, without what the shares that keep .git read-only need", "'agent-vm setup' rebuilds it, then '--reset' the VMs made from it.") } @@ -240,13 +239,21 @@ func (e *app) doctorCmd(ctx context.Context, args []string) int { d.line("info", fmt.Sprintf("%d running, %d CPUs and %d GiB in total", n, c, m)) } } - dead, unknown := e.scratchLeftovers() + var dead, unknown []string + if eng, err := e.engine(); err == nil { + dead, unknown = eng.ScratchLeftovers() + } for _, left := range dead { d.line("warn", "scratch VM "+left+" was left by a run that did not finish", "The next --scratch run deletes it, or: agent-vm rm "+left) } for _, left := range unknown { d.line("warn", "scratch VM "+left+" has a record without a process number: not known whether a run still uses it", "agent-vm rm "+left+" deletes it.") } + for _, n := range e.state.Names() { + if _, err := e.state.Meta(n); err != nil { + d.line("warn", "cannot read what agent-vm knows of "+n+": "+err.Error(), "It is treated as knowing nothing; 'agent-vm rm "+n+"' deletes the VM.") + } + } fmt.Fprintln(w) if d.fail > 0 { @@ -301,11 +308,15 @@ func (d *doctor) directory(ctx context.Context, dir string, protects bool, getVM case inst.Status == vm.Running: d.line("warn", "running with a share the host does not confine: "+confined.Error(), "The next start (after 'agent-vm stop') sets its shares back.") + case inst.Orphaned: + d.line("warn", "running without Lima's host agent, cut off", "'agent-vm stop' stops it, and so does the next agent-vm command here.") + case inst.Status == vm.Broken: + d.line("warn", "Broken: Lima cannot say whether it runs") default: d.line("info", "stopped") } - protected := len(inst.Mounts) > 0 && unprotected(inst, nil) == "" - current := unprotected(inst, names) == "" + protected := len(inst.Mounts) > 0 && engine.Unprotected(inst, nil) == "" + current := engine.Unprotected(inst, names) == "" switch { case protected && !current: d.line("warn", "its shares keep .git read-only, but not all of "+strings.Join(names, ", "), @@ -333,7 +344,7 @@ func (d *doctor) directory(ctx context.Context, dir string, protects bool, getVM hn, ok := gitguard.HooksName(inRepo) current := false if inst != nil { - current = unprotected(inst, names) == "" + current = engine.Unprotected(inst, names) == "" } switch { case !ok: diff --git a/internal/cli/doctor_test.go b/internal/cli/doctor_test.go index 94648b60..542a06ad 100644 --- a/internal/cli/doctor_test.go +++ b/internal/cli/doctor_test.go @@ -11,6 +11,7 @@ import ( "github.com/sylvinus/agent-vm/internal/paths" "github.com/sylvinus/agent-vm/internal/state" + "github.com/sylvinus/agent-vm/internal/vm" "github.com/sylvinus/agent-vm/internal/vmname" ) @@ -55,7 +56,7 @@ func TestDoctor(t *testing.T) { // An env others can read; a leftover scratch VM; a config risk. os.WriteFile(se.state.Path("env"), []byte("A=1\nB=2\nA=3\n"), 0o644) os.Chmod(se.state.Path("env"), 0o644) - os.WriteFile(se.state.Marker(state.ScratchOf, "x-scratch-0000aaaa"), []byte("999999999\n"), 0o644) + se.state.UpdateMeta("x-scratch-0000aaaa", func(m *state.Metadata) { m.ScratchPID = 999999999 }) exec.Command("git", "-C", se.proj, "config", "core.fsmonitor", "./mon.sh").Run() o = out() // Permission bits mean little on Windows (see doctor.go): only the @@ -83,6 +84,16 @@ func TestDoctor(t *testing.T) { if o = out(); !strings.Contains(o, " warn running with a share the host does not confine: ") || !strings.Contains(o, `the mount type is "9p"`) { t.Errorf("9p:\n%s", o) } + // Broken, orphaned or not: never "stopped". + v.Status = vm.Broken + if o = out(); !strings.Contains(o, " warn Broken: Lima cannot say whether it runs") { + t.Errorf("Broken:\n%s", o) + } + v.Orphaned = true + if o = out(); !strings.Contains(o, " warn running without Lima's host agent, cut off") { + t.Errorf("orphaned:\n%s", o) + } + v.Status, v.Orphaned = vm.Running, false if strings.Contains(se.fake.CallLog(), "edit") || strings.Contains(se.fake.CallLog(), "start") || strings.Contains(se.fake.CallLog(), "stop") { t.Errorf("doctor changed the VM:\n%s", se.fake.CallLog()) } diff --git a/internal/cli/e2e_test.go b/internal/cli/e2e_test.go index 4b7e55ae..ea7d0d68 100644 --- a/internal/cli/e2e_test.go +++ b/internal/cli/e2e_test.go @@ -9,11 +9,17 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" + "syscall" "testing" "time" - "github.com/lima-vm/lima/v2/pkg/instance" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance" + + "github.com/sylvinus/agent-vm/internal/state" + "github.com/sylvinus/agent-vm/internal/vm" + "github.com/sylvinus/agent-vm/internal/vmname" ) // agent-vm as a user runs it, on real VMs: setup, then a command in a @@ -93,6 +99,9 @@ func TestE2E(t *testing.T) { if b, _ := os.ReadFile(filepath.Join(proj, "from-vm")); string(b) != "hi\na\n" { t.Errorf("on the host: %q", b) } + // The run that started the VM is over and its context cancelled: the VM + // runs on (Lima once killed its hostagent then). + time.Sleep(2 * time.Second) if code, out := run(proj, "info"); code != 0 || !strings.Contains(out, "vm_running=1") || !strings.Contains(out, "git_protected=1") { t.Errorf("info: %s", out) } @@ -113,7 +122,66 @@ func TestE2E(t *testing.T) { if code != 0 || !strings.Contains(out, "the project share is not mounted") || !strings.Contains(out, "v2, longer") { t.Errorf("lost share: %d %s", code, out) } + // A named VM that shares nothing: the folder's path is made in it, empty, + // and it is the VM's own, kept. + code, out = run(proj, "--name", "sandbox", "--no-shares", "run", "sh", "-c", `pwd; test ! -e from-vm && echo EMPTY`) + if code != 0 || !strings.Contains(out, proj+"\n") || !strings.Contains(out, "EMPTY") { + t.Errorf("named VM, no shares: %d\n%s", code, out) + } + if code, out := run(proj, "stop", "sandbox"); code != 0 { + t.Errorf("stop sandbox: %d\n%s", code, out) + } + // The hostagent killed under the VM: with QEMU, the VM runs on, cut off. + // The next run stops it and starts it again; so does stop. With vz, the + // VM goes with the hostagent. + st := state.Dir(filepath.Join(home, ".agent-vm")) + b, err := vm.NewLima(LimaHome(st), st.VMs(), string(st)) + if err != nil { + t.Fatal(err) + } + name := vmname.Name(proj) + orphan := func() (driver int) { + t.Helper() + inst, err := b.Get(ctx, name) + if err != nil || inst.Status != vm.Running { + t.Fatalf("before the kill: %+v %v", inst, err) + } + pid := func(f string) int { + b, _ := os.ReadFile(filepath.Join(inst.Dir, f)) + n, _ := strconv.Atoi(strings.TrimSpace(string(b))) + return n + } + driver = pid(inst.VMType + ".pid") + if err := syscall.Kill(pid("ha.pid"), syscall.SIGKILL); err != nil { + t.Fatal(err) + } + // Broken without more until the hostagent is reaped. + for range 100 { + if inst, err = b.Get(ctx, name); err == nil && (inst.Orphaned || inst.Status == vm.Stopped) { + break + } + time.Sleep(100 * time.Millisecond) + } + if !inst.Orphaned { + t.Logf("not orphaned by the kill: %s", inst.Status) + return 0 + } + return driver + } + orphaned := orphan() != 0 + if code, out := run(proj, "run", "true"); code != 0 || orphaned != strings.Contains(out, "left running without Lima's host agent") { + t.Errorf("run on an orphaned VM: %d %s", code, out) + } + driver := orphan() if code, out := run(proj, "stop"); code != 0 || !strings.Contains(out, "VM stopped.") { t.Errorf("stop: %s", out) } + // Killed, then reaped by init. + for i := 0; driver != 0 && syscall.Kill(driver, 0) == nil; i++ { + if i == 50 { + t.Errorf("the orphaned driver %d still runs", driver) + break + } + time.Sleep(100 * time.Millisecond) + } } diff --git a/internal/cli/help.txt b/internal/cli/help.txt index 861434d1..4f6ffa1d 100644 --- a/internal/cli/help.txt +++ b/internal/cli/help.txt @@ -105,6 +105,17 @@ command or right after its name, never later: in 'agent-vm run docker run The project's env file and runtime script stay out; ~/.agent-vm/env and runtime.sh go in. The folder's own VM is left alone, and several can run at once. + --name NAME The VM called NAME, not the folder's own, so that a + folder can have several. It belongs to the folder it is + made in and runs from there only ('rm NAME' to give it + up, --reset to make it again). NAME: lowercase letters, + digits and dashes, at most 40 characters, not ending in + a dash and 8 hex digits, not 'base' nor 'base-...'. + --no-shares With --name, when the VM is made: it shares nothing (as + --scratch), is kept, and runs from any folder. It never + gets shares. The folder's path is made in the VM as an + empty working folder. ~/.agent-vm/env and runtime.sh go + in, the project's files do not. Examples: agent-vm setup # Create base VM @@ -118,6 +129,9 @@ Examples: agent-vm --rm claude # Destroy VM after Claude exits agent-vm --readonly shell # Nothing on the host is writable agent-vm --scratch claude # Nothing of yours mounted, then deleted + agent-vm --name agent2 claude # A second VM on this folder + agent-vm --name sandbox --no-shares claude # Nothing shared, kept, from any folder + agent-vm stop agent2 # Stop a VM by name (see 'list') agent-vm shell # Shell into the VM agent-vm sh -c "ls -la | grep config" # One-shot command via login zsh agent-vm run npm install # Run a command in the VM diff --git a/internal/cli/info.go b/internal/cli/info.go index 320355bf..d42afdf2 100644 --- a/internal/cli/info.go +++ b/internal/cli/info.go @@ -4,16 +4,12 @@ import ( "context" "errors" "fmt" - "io" "os" - "path/filepath" "strings" "github.com/sylvinus/agent-vm/internal/env" "github.com/sylvinus/agent-vm/internal/gitguard" "github.com/sylvinus/agent-vm/internal/mounts" - "github.com/sylvinus/agent-vm/internal/paths" - "github.com/sylvinus/agent-vm/internal/state" "github.com/sylvinus/agent-vm/internal/version" "github.com/sylvinus/agent-vm/internal/vm" "github.com/sylvinus/agent-vm/internal/vmname" @@ -21,26 +17,21 @@ import ( // refs are the locations no share may reach (see mounts.Refs). func (e *app) refs() []mounts.Ref { - self := "" - if exe, err := os.Executable(); err == nil { - if p, err := filepath.EvalSymlinks(exe); err == nil { - self = paths.Host(filepath.Dir(p)) - } + eng, err := e.engine() + if err != nil { + return nil } - return mounts.Refs(paths.Home(), self, string(e.state), LimaHome(e.state), oldLimaHome()) -} - -// entries are the usable entries of ~/.agent-vm/volumes for the project -// dir, their warnings on warn. -func (e *app) entries(dir string, warn io.Writer) []mounts.Entry { - return mounts.Entries(e.state.Path("volumes"), dir, paths.Home(), e.refs(), warn) + return eng.Refs() } // scan is the git scan of the project dir's writable shares (see // gitguard.Scan), the volumes read quietly. func (e *app) scan(ctx context.Context, dir string) *gitguard.Result { - shares := gitguard.Shares(dir, mounts.RWDirs(e.entries(dir, io.Discard))) - return gitguard.Scan(ctx, shares, paths.Home()) + eng, err := e.engine() + if err != nil { + return &gitguard.Result{} + } + return eng.Scan(ctx, dir) } // writableGitOptOut: AGENT_VM_UNSAFE_WRITABLE_GIT=1, or --unsafe-writable-git @@ -100,8 +91,7 @@ func (e *app) infoCmd(ctx context.Context, args []string) int { return inst, err } base, baseErr := get(vmname.Template) - _, markErr := os.Stat(e.state.Path(state.BaseVersion)) - fmt.Fprintf(w, "base_exists=%s\n", tristate(base != nil && markErr == nil, baseErr)) + fmt.Fprintf(w, "base_exists=%s\n", tristate(base != nil && e.state.BaseReady(vmname.Template), baseErr)) inst, instErr := get(name) fmt.Fprintf(w, "vm_exists=%s\n", tristate(inst != nil, instErr)) fmt.Fprintf(w, "vm_running=%s\n", tristate(inst != nil && inst.Status == vm.Running, instErr)) @@ -151,12 +141,11 @@ func (e *app) infoCmd(ctx context.Context, args []string) int { // current one: 1, 0, or unknown with nothing to compare against. A VM with // no record but a known base predates the record, which makes it stale. func (e *app) staleState(name string) string { - base, err := os.ReadFile(e.state.Path(state.BaseVersion)) - if err != nil { + base := e.state.Known(vmname.Template).Base + if base == nil || base.Built <= 0 { return "unknown" } - mine, err := os.ReadFile(e.state.Marker(state.VersionOf, name)) - if err != nil || strings.TrimRight(string(mine), "\n") != strings.TrimRight(string(base), "\n") { + if mine := e.state.Known(name).Base; mine == nil || mine.Built != base.Built { return "1" } return "0" diff --git a/internal/cli/info_test.go b/internal/cli/info_test.go index 98731650..e048721a 100644 --- a/internal/cli/info_test.go +++ b/internal/cli/info_test.go @@ -73,13 +73,13 @@ func TestInfo(t *testing.T) { if infoMap(t, te)["base_exists"] != "0" { t.Error("a base without its marker counted") } - os.WriteFile(te.state.Path(state.BaseVersion), []byte("2\n"), 0o644) - os.WriteFile(te.state.Marker(state.VersionOf, name), []byte("1\n"), 0o644) + te.state.UpdateMeta(vmname.Template, func(m *state.Metadata) { m.Base = &state.Base{Name: vmname.Template, Built: 2, BuiltBy: "0.4.0"} }) + te.state.UpdateMeta(name, func(m *state.Metadata) { m.Base = &state.Base{Name: vmname.Template, Built: 1, BuiltBy: "0.4.0"} }) m = infoMap(t, te) if m["base_exists"] != "1" || m["vm_exists"] != "1" || m["vm_running"] != "1" || m["vm_stale"] != "1" || m["ssh_config"] != "/x/ssh.config" { t.Errorf("with VMs: %v", m) } - os.WriteFile(te.state.Marker(state.VersionOf, name), []byte("2\n"), 0o644) + te.state.UpdateMeta(name, func(m *state.Metadata) { m.Base.Built = 2 }) if infoMap(t, te)["vm_stale"] != "0" { t.Error("an up to date VM said stale") } diff --git a/internal/cli/install.go b/internal/cli/install.go index 7b68ff40..d35234f5 100644 --- a/internal/cli/install.go +++ b/internal/cli/install.go @@ -16,7 +16,6 @@ import ( "syscall" "github.com/sylvinus/agent-vm/internal/paths" - "github.com/sylvinus/agent-vm/internal/state" "github.com/sylvinus/agent-vm/internal/version" "github.com/sylvinus/agent-vm/internal/vmname" ) @@ -233,7 +232,7 @@ func (e *app) installCmd(ctx context.Context, args []string) int { // Looking only: installing must not move 0.2's VMs, which stops running ones. if b, err := e.readOnlyBackend(); err == nil { if base, err := b.Get(ctx, vmname.Template); err == nil && base != nil { - if _, err := os.Stat(e.state.Path(state.BaseVersion)); err == nil { + if e.state.BaseReady(vmname.Template) { fmt.Fprintln(w, "Next: cd your-project && agent-vm claude # or opencode, codex, vibe, pi") return 0 } diff --git a/internal/cli/lock.go b/internal/cli/lock.go deleted file mode 100644 index afcac29f..00000000 --- a/internal/cli/lock.go +++ /dev/null @@ -1,48 +0,0 @@ -package cli - -import ( - "context" - "fmt" - "os" - "path/filepath" - "time" -) - -// lockVM takes the lock of name, which serialises what changes that VM -// between agent-vm runs: two runs in one folder would otherwise clean up, -// clone and configure the same VM at once. It waits, saying so, until the -// holder is done or ctx ends. The lock is a file in the state folder's -// locks/, kept for the next run: a crashed holder releases it with its -// process. -func (e *app) lockVM(ctx context.Context, name string) (unlock func(), err error) { - dir := e.state.Path("locks") - if err := os.MkdirAll(dir, 0o755); err != nil { - return nil, err - } - f, err := os.OpenFile(filepath.Join(dir, name), os.O_CREATE|os.O_RDWR, 0o644) - if err != nil { - return nil, err - } - told := false - for { - ok, err := tryLock(f) - if err != nil { - f.Close() - return nil, err - } - if ok { - // Closing the file releases the lock. - return func() { f.Close() }, nil - } - if !told { - fmt.Fprintf(e.io.Stderr, "Waiting for another agent-vm that is working on '%s'...\n", name) - told = true - } - select { - case <-ctx.Done(): - f.Close() - return nil, ctx.Err() - case <-time.After(200 * time.Millisecond): - } - } -} diff --git a/internal/cli/main_test.go b/internal/cli/main_test.go index bb600f8b..d1cb47e0 100644 --- a/internal/cli/main_test.go +++ b/internal/cli/main_test.go @@ -5,7 +5,7 @@ import ( "runtime" "testing" - limaversion "github.com/lima-vm/lima/v2/pkg/version" + limaversion "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" "github.com/sylvinus/agent-vm/internal/limaembed" ) diff --git a/internal/cli/migrate_test.go b/internal/cli/migrate_test.go index 95b0fd51..b84cd996 100644 --- a/internal/cli/migrate_test.go +++ b/internal/cli/migrate_test.go @@ -1,14 +1,18 @@ package cli import ( + "bytes" "context" + "errors" "io" "os" "path/filepath" "strings" "testing" + "github.com/sylvinus/agent-vm/internal/limaembed" "github.com/sylvinus/agent-vm/internal/state" + "github.com/sylvinus/agent-vm/internal/vm" "github.com/sylvinus/agent-vm/internal/vm/vmtest" ) @@ -26,7 +30,7 @@ func TestOwnLimaHome(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := e.backend(); err == nil || !strings.Contains(err.Error(), "your own Lima's home") { + if _, err := e.backend(context.Background()); err == nil || !strings.Contains(err.Error(), "your own Lima's home") { t.Errorf("own home: %v", err) } // The LIMA_HOME agent-vm sets for Lima is not the user's: a second @@ -35,7 +39,7 @@ func TestOwnLimaHome(t *testing.T) { t.Setenv("AGENT_VM_LIMA_HOME", "") for i := range 2 { e, _ := newApp(IO{Stdin: strings.NewReader(""), Stdout: io.Discard, Stderr: io.Discard}) - if _, err := e.backend(); err != nil { + if _, err := e.backend(context.Background()); err != nil { t.Errorf("command %d: %v", i+1, err) } } @@ -73,7 +77,8 @@ func TestMigrateOld(t *testing.T) { te := newTestEnv(t, vmtest.New(), "n\n") os.MkdirAll(string(te.state), 0o755) - os.WriteFile(te.state.Marker(state.BuiltByOf, "agent-vm-p-12345678"), []byte("0.2.1\n"), 0o644) + // 0.2's markers, named as 0.3.0 named them. + os.WriteFile(te.state.Path(".agent-vm-built-by-agent-vm-p-12345678"), []byte("0.2.1\n"), 0o644) te.migrateOld(context.Background(), old, now) if out := te.out(); !strings.Contains(out, " agent-vm-p-12345678, as p-12345678\n") || strings.Contains(out, "theirs") || !strings.Contains(out, "Not moved") { t.Errorf("declined:\n%s", out) @@ -104,7 +109,7 @@ func TestMigrateOld(t *testing.T) { if _, err := os.Stat(filepath.Join(now, "p-12345678", "lima.yaml")); err != nil { t.Error("not moved") } - if te.state.ReadMarker(state.BuiltByOf, "p-12345678") != "0.2.1" { + if te.state.Read(".agent-vm-built-by-p-12345678") != "0.2.1" { t.Error("its markers did not follow") } if _, err := os.Stat(filepath.Join(old, "theirs", "lima.yaml")); err != nil { @@ -127,3 +132,86 @@ func TestMigrateOld(t *testing.T) { t.Errorf("nothing to move: %q", te.out()) } } + +// 0.3.0's VMs, made by a real Lima in its Lima home, move to a folder each +// and are found there by the Lima that agent-vm now runs. +func TestMigrateLayoutLima(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + t.Setenv("LIMA_HOME", "") + t.Setenv("LIMA_INSTANCES_DIR", "") + t.Setenv("LIMA_TEMPLATES_PATH", "") + st := state.Dir(filepath.Join(home, ".agent-vm")) + t.Setenv("AGENT_VM_STATE_DIR", string(st)) + ctx := context.Background() + + // How 0.3.0 had them: in the Lima home, with markers. + old, err := vm.NewLima(st.LimaHome(), "", string(st)) + if err != nil { + t.Fatal(err) + } + none := []vm.Mount{} + for _, name := range []string{"base", "proj-12345678"} { + if err := old.Create(ctx, name, "template:debian-13", vm.Settings{CPUs: 1, Mounts: &none}, io.Discard); errors.Is(err, limaembed.ErrNoAssets) { + t.Skip("built without the guest agent: make assets") + } else if err != nil { + t.Fatal(err) + } + } + for name, content := range map[string]string{ + ".agent-vm-base-version": "1759000000\n", ".agent-vm-base-built-by": "0.3.0\n", + ".agent-vm-version-proj-12345678": "1759000000\n", ".agent-vm-built-by-proj-12345678": "0.3.0\n", + ".agent-vm-term-proj-12345678": "xterm\n", + } { + os.WriteFile(st.Path(name), []byte(content), 0o644) + } + + var stderr bytes.Buffer + e, err := newApp(IO{Stdin: strings.NewReader(""), Stdout: io.Discard, Stderr: &stderr}) + if err != nil { + t.Fatal(err) + } + if got := e.layoutPending(); len(got) != 2 { + t.Fatalf("pending: %v", got) + } + b, err := e.backend(ctx) // moves them + if err != nil { + t.Fatal(err) + } + if !strings.Contains(stderr.String(), "Moving base, proj-12345678") { + t.Errorf("said: %q", stderr.String()) + } + if got := e.layoutPending(); len(got) != 0 || st.PendingLayout(st.LimaHome()) { + t.Errorf("still pending: %v\n%s", got, stderr.String()) + } + all, err := b.List(ctx) + if err != nil || len(all) != 2 { + t.Fatalf("list: %v %v", all, err) + } + inst, err := b.Get(ctx, "proj-12345678") + if err != nil || inst.Status != vm.Stopped || inst.CPUs != 1 || inst.Dir != st.Lima("proj-12345678") { + t.Errorf("moved VM: %+v %v", inst, err) + } + if m := st.Known("proj-12345678"); m.Base == nil || m.Base.BuiltBy != "0.3.0" || m.Term != "xterm" { + t.Errorf("metadata: %+v", m) + } + if !st.BaseReady("base") { + t.Error("the base is not ready") + } + if _, err := os.Stat(filepath.Join(st.LimaHome(), "proj-12345678")); err == nil { + t.Error("left in the Lima home") + } + // Cloned and deleted at the new place, as a start does. + if err := b.Clone(ctx, "base", "other-12345678"); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(st.Lima("other-12345678"), "lima.yaml")); err != nil { + t.Errorf("clone: %v", err) + } + if !e.deleteVM(ctx, "other-12345678") { + t.Error("delete") + } else if _, err := os.Stat(st.VM("other-12345678")); err == nil { + t.Error("the folder of a deleted VM is left") + } +} diff --git a/internal/cli/options.go b/internal/cli/options.go index 921162b4..39489335 100644 --- a/internal/cli/options.go +++ b/internal/cli/options.go @@ -5,6 +5,8 @@ import ( "regexp" "strconv" "strings" + + "github.com/sylvinus/agent-vm/internal/vmname" ) // VMOpts are agent-vm's own options, for the commands that start a VM. @@ -16,7 +18,26 @@ type VMOpts struct { Scratch bool UnsafeWritableGit bool UnsafeNoPrompts bool - RM bool // --rm: delete the VM after the command + RM bool // --rm: delete the VM after the command + Name string // --name: the VM of that name, not the folder's + NoShares bool // --no-shares: nothing of the host is shared +} + +// check says why the options do not go together, "" when they do. +func (o VMOpts) check() error { + switch { + case o.Scratch && o.Name != "": + return fmt.Errorf("--scratch and --name do not go together: --no-shares --rm --name %s makes a named VM that is deleted after.", o.Name) + case o.NoShares && o.Name == "" && !o.Scratch: + return fmt.Errorf("--no-shares needs --name: --scratch is a VM with no shares that is deleted after, any other is called something.") + case o.NoShares && o.ReadOnly: + return fmt.Errorf("--no-shares and --readonly do not go together: nothing is shared.") + case o.Name != "": + if err := vmname.ValidateCustom(o.Name); err != nil { + return fmt.Errorf("--name: %v.", err) + } + } + return nil } func newVMOpts() VMOpts { @@ -34,7 +55,7 @@ func (o *VMOpts) take(args []string) (int, error) { } opt, val, n := args[0], "", 0 switch opt { - case "--disk", "--memory", "--ram", "--cpus", "--ssh-port": + case "--disk", "--memory", "--ram", "--cpus", "--ssh-port", "--name": if len(args) < 2 { return 0, fmt.Errorf("%s needs a value.", opt) } @@ -42,6 +63,9 @@ func (o *VMOpts) take(args []string) (int, error) { case "--reset": o.Reset = true return 1, nil + case "--no-shares": + o.NoShares = true + return 1, nil case "--readonly": o.ReadOnly = true return 1, nil @@ -62,7 +86,7 @@ func (o *VMOpts) take(args []string) (int, error) { switch { case !ok: return 0, nil - case name == "--disk", name == "--memory", name == "--ram", name == "--cpus", name == "--ssh-port": + case name == "--disk", name == "--memory", name == "--ram", name == "--cpus", name == "--ssh-port", name == "--name": opt, val, n = name, v, 1 default: return 0, nil @@ -71,6 +95,13 @@ func (o *VMOpts) take(args []string) (int, error) { if opt == "--ram" { opt = "--memory" } + if opt == "--name" { + if val == "" { + return 0, fmt.Errorf("--name needs a name.") + } + o.Name = val + return n, nil + } if opt == "--ssh-port" { p, err := strconv.Atoi(val) if val != "0" && (!positiveRe.MatchString(val) || err != nil || p < 1024 || p > 65535) { diff --git a/internal/cli/policy.go b/internal/cli/policy.go deleted file mode 100644 index 024c73ec..00000000 --- a/internal/cli/policy.go +++ /dev/null @@ -1,83 +0,0 @@ -package cli - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - "os" - "slices" - "strings" - - "github.com/sylvinus/agent-vm/internal/guard" - "github.com/sylvinus/agent-vm/internal/netguard" - "github.com/sylvinus/agent-vm/internal/state" -) - -// The network and guarded-files policies are read when a VM boots, by its -// hostagent, and kept until it stops. Starting a VM records what they were -// (PolicyOf); a later start that finds the VM running says so when they -// differ now. - -// quiet is AGENT_VM_QUIET=1: notices that are not about a risk to the host -// are not printed. -func quiet() bool { return os.Getenv("AGENT_VM_QUIET") == "1" } - -// policyFingerprint identifies the policies as the hostagent would load -// them now: the network file's rules and the guarded paths, comments and -// order not counting, and the two switches in the environment. -func policyFingerprint(st state.Dir) string { - var parts []string - if pol, err := netguard.Load(string(st)); err == nil { - parts = append(parts, fmt.Sprintf("open=%t", pol.Open)) - for _, r := range pol.Allow { - parts = append(parts, fmt.Sprintf("allow %v %d", r.Net, r.Port)) - } - for _, d := range pol.Domains { - parts = append(parts, "domain "+d) - } - } else { - parts = append(parts, "network: "+err.Error()) - } - if g, err := guard.Load(string(st), ""); err == nil { - for p := range g.Paths { - parts = append(parts, "guard "+p) - } - } else { - parts = append(parts, "guarded: "+err.Error()) - } - parts = append(parts, fmt.Sprintf("deny=%t", os.Getenv("AGENT_VM_GUARDED_WRITES") == "deny")) - slices.Sort(parts) - sum := sha256.Sum256([]byte(strings.Join(parts, "\n"))) - return hex.EncodeToString(sum[:]) -} - -// recordPolicy notes the policies the VM was just booted with. -func (s *start) recordPolicy() { - if err := s.state.Write(state.PolicyOf+s.name, []byte(policyFingerprint(s.state)+"\n")); err != nil { - s.warnf("Warning: could not record the network and guarded-files policies VM '%s' booted with: %v", s.name, err) - } -} - -// notePolicyChange says so when the running VM booted with other policies -// than the ones in force now. A VM with no record (booted by an agent-vm -// that kept none) is not said anything about. -func (s *start) notePolicyChange() { - booted := s.state.ReadMarker(state.PolicyOf, s.name) - if booted == "" || booted == policyFingerprint(s.state) { - return - } - s.warnf("Note: VM '%s' booted with other network or guarded-files settings than now (%s, %s, AGENT_VM_UNSAFE_OPEN_NETWORK, AGENT_VM_GUARDED_WRITES). The running VM keeps the old ones: 'agent-vm stop', then run again to apply them.", - s.name, netguard.FileName, guard.FileName) -} - -// warnDomains says, once per start, that the network file's 'domain' lines -// are experimental. -func warnDomains(st state.Dir, w io.Writer) { - if quiet() { - return - } - if pol, err := netguard.Load(string(st)); err == nil && len(pol.Domains) > 0 { - fmt.Fprintf(w, "Warning: the 'domain' lines in %s are experimental: do not rely on them alone to confine a VM. AGENT_VM_QUIET=1 hides this.\n", st.Path(netguard.FileName)) - } -} diff --git a/internal/cli/setup.go b/internal/cli/setup.go index 0709e07f..d0ce12ba 100644 --- a/internal/cli/setup.go +++ b/internal/cli/setup.go @@ -2,22 +2,18 @@ package cli import ( "context" + "errors" "fmt" "io" "os" - "path/filepath" - "strconv" "strings" - "time" agentvm "github.com/sylvinus/agent-vm" "github.com/sylvinus/agent-vm/internal/env" "github.com/sylvinus/agent-vm/internal/host" - "github.com/sylvinus/agent-vm/internal/state" "github.com/sylvinus/agent-vm/internal/ui" - "github.com/sylvinus/agent-vm/internal/version" - "github.com/sylvinus/agent-vm/internal/vm" "github.com/sylvinus/agent-vm/internal/vmname" + "github.com/sylvinus/agent-vm/provision" ) const setupUsage = "Usage: agent-vm setup [--disk GB] [--memory GB] [--cpus N] [--preinstall=LIST]" @@ -79,15 +75,11 @@ Options: ` // The software of the base, as agent-vm.setup.sh names it -// (AGENT_VM_INSTALL_). -var software = []string{"python", "node", "ruby", "rust", "golang", "docker", "chromium", "gh", "claude", "opencode", "codex", "vibe", "pi", - "code-server", "code-claude", "code-codex", "code-vibe", "mcp-chrome", "mcp-playwright"} - -// The default set. Opt-in: Ruby, Rust, Go, Pi (released several times a -// week), Playwright MCP (a second browser-driving server, whose tools cost -// context in every agent), and code-server with its extensions (an editor -// most use on the host). -var defaultSoftware = []string{"python", "node", "docker", "chromium", "gh", "claude", "opencode", "codex", "vibe", "mcp-chrome"} +// (AGENT_VM_INSTALL_), and the default set. +var ( + software = provision.Software + defaultSoftware = provision.DefaultSoftware +) // skipPrereqs is for tests, on hosts without KVM. var skipPrereqs = false @@ -100,32 +92,8 @@ func (in install) set(names ...string) { } } -// nodeNeededBy says why the choices need Node.js, if they do: Codex and Pi -// install with npm, Chrome DevTools MCP runs with npx. -func (in install) nodeNeededBy() string { - switch { - case in["codex"]: - return "Codex CLI requires npm" - case in["pi"]: - return "Pi requires npm" - case in["chromium"] && in["mcp-chrome"] && (in["claude"] || in["opencode"] || in["vibe"] || in["code-claude"] || in["code-codex"] || in["code-vibe"]): - return "Chrome DevTools MCP uses npx" - } - return "" -} - -// exports are the choices as the setup script reads them. -func (in install) exports() string { - var b strings.Builder - for _, n := range software { - v := 0 - if in[n] { - v = 1 - } - fmt.Fprintf(&b, "export AGENT_VM_INSTALL_%s=%d\n", strings.ToUpper(strings.ReplaceAll(n, "-", "_")), v) - } - return b.String() -} +// nodeNeededBy says why the choices need Node.js, if they do. +func (in install) nodeNeededBy() string { return provision.NodeNeededBy(in) } // setupCmd: `setup [options]`, the options given before it included. Of the // VM options, it takes the resources. @@ -219,51 +187,6 @@ func (e *app) setupCmd(ctx context.Context, args []string) int { return e.buildBase(ctx, in, disk, memory, cpus) } -// swapBase puts the finished build in the base's place: the old base and its -// markers go, the build is cloned to the base's name and marked ready, last -// the build goes. Under the base's lock, so that no start clones the base -// meanwhile. Lima has no rename, so the disk is copied once more; the build -// is kept if the copy cannot be made, and said. -func (e *app) swapBase(ctx context.Context, b vm.Backend) bool { - w := e.io.Stderr - unlock, err := e.lockVM(ctx, vmname.Template) - if err != nil { - fmt.Fprintf(w, "Error: could not lock the base template: %v. The new base is VM '%s'; 'agent-vm setup' again builds another.\n", err, building) - return false - } - defer unlock() - if d := filepath.Join(LimaHome(e.state), vmname.Template); isDirNoYAML(d) { - fmt.Fprintf(w, "Detected partial VM state at %s (no lima.yaml): cleaning up.\n", d) - if err := os.RemoveAll(d); err != nil { - fmt.Fprintf(w, "Error: could not remove %s: %v\n", d, err) - return false - } - } - // Forgets the old base's markers with it: from here on, nothing says a - // base is ready. - if !e.deleteVM(ctx, vmname.Template) { - fmt.Fprintf(w, "Error: could not replace the base VM. The new one is VM '%s'; 'agent-vm setup' again builds another.\n", building) - return false - } - if err := b.Clone(ctx, building, vmname.Template); err != nil { - fmt.Fprintf(w, "Error: could not copy the new base VM to '%s': %v\nThere is no base now. The new one is VM '%s'; 'agent-vm setup' again builds another.\n", vmname.Template, err, building) - return false - } - // The version last: it says the base can be cloned. - if err := e.state.Write(state.BaseBuiltBy, []byte(version.Version+"\n")); err != nil { - fmt.Fprintf(w, "Error: could not record the version that built the base: %v\nThe base VM is not marked ready: 'agent-vm setup' again.\n", err) - return false - } - if err := e.state.Write(state.BaseVersion, []byte(strconv.FormatInt(time.Now().Unix(), 10)+"\n")); err != nil { - fmt.Fprintf(w, "Error: could not record when the base was built: %v\nThe base VM is not marked ready: 'agent-vm setup' again.\n", err) - return false - } - if !e.deleteVM(ctx, building) { - fmt.Fprintf(w, "Warning: the base is ready, but VM '%s' stays: 'agent-vm rm %s' frees its disk.\n", building, building) - } - return true -} - func contains(s []string, v string) bool { for _, x := range s { if x == v { @@ -363,129 +286,114 @@ func (e *app) askCodeExtensions(in install) { } // building is the VM the new base is built as. The base that is there keeps -// serving the starts, with its markers, until the new one is complete. +// serving the starts, with its record, until the new one is complete. const building = vmname.Template + "-building" -// buildBase makes the base template: built as `building` (created, booted, -// provisioned by the setup script and the user's, stopped), then swapped in -// for the base and marked as ready. A setup that fails before the swap -// leaves the base it found. -func (e *app) buildBase(ctx context.Context, in install, disk, memory, cpus int) int { - b, err := e.backend() - if err != nil { - fmt.Fprintf(e.io.Stderr, "Error: %v\n", err) - return 1 - } - // One setup at a time: two would build in the same VM. - unlockSetup, err := e.lockVM(ctx, vmname.Template+"-setup") - if err != nil { - fmt.Fprintf(e.io.Stderr, "Error: could not lock the base template's setup: %v\n", err) - return 1 - } - defer unlockSetup() - if !(&start{app: e, ctx: ctx}).limaOverridesOK() { - return 1 - } - if err := policiesErr(e.state); err != nil { - fmt.Fprintf(e.io.Stderr, "Error: %v\n", err) - return 1 - } - warnDomains(e.state, e.io.Stderr) - // A build an earlier setup left, finished or not, is not this one's. - if d := filepath.Join(LimaHome(e.state), building); isDirNoYAML(d) { - fmt.Fprintf(e.io.Stderr, "Detected partial VM state at %s (no lima.yaml): cleaning up.\n", d) - if err := os.RemoveAll(d); err != nil { - fmt.Fprintf(e.io.Stderr, "Error: could not remove %s: %v\n", d, err) - return 1 - } +// customSetup is the name of the user's setup.sh among the scripts of the +// build. +const customSetup = "custom setup" + +// setupOutput shows what the steps of the build print: the last lines in a +// window, all of it in a log. +type setupOutput struct { + e *app + logPath string + log *os.File + err error +} + +// open makes the log, on first use. +func (o *setupOutput) open() error { + if o.log != nil || o.err != nil { + return o.err } - if !e.deleteVM(ctx, building) { - return 1 + if o.err = os.MkdirAll(string(o.e.state), 0o755); o.err != nil { + return o.err } + o.logPath = o.e.state.Path("setup.log") + o.log, o.err = os.Create(o.logPath) + return o.err +} - cpus = host.Cap("cpus", cpus, e.io.Stderr) - memory = host.Cap("memory", memory, e.io.Stderr) - host.WarnDiskSpace(LimaHome(e.state), disk, e.io.Stderr) - - if err := os.MkdirAll(string(e.state), 0o755); err != nil { - fmt.Fprintf(e.io.Stderr, "Error: %v\n", err) - return 1 +func (o *setupOutput) Step(name string, fn func(stdout, stderr io.Writer) error) error { + e := o.e + // The user's own script runs on the terminal, as it always did. + if name == "script:"+customSetup { + return fn(e.io.Stdout, e.io.Stderr) } - logPath := e.state.Path("setup.log") - log, err := os.Create(logPath) - if err != nil { - fmt.Fprintf(e.io.Stderr, "Error: %v\n", err) - return 1 + if err := o.open(); err != nil { + return errSetupLog{err} } - defer log.Close() // Every step from here shows its output in a 10-line window, all of it // kept in one log for when something fails. - windowed := func(fn func(w io.Writer) error) bool { - win := e.ui.NewWindow(e.io.Stdout, log) - err := fn(win) - win.Close() - if err != nil { - fmt.Fprintf(log, "%v\n", err) - if f, ok := e.io.Stdout.(*os.File); ok && ui.IsTerminal(f) { - fmt.Fprint(e.io.Stderr, ui.Tail(logPath, 20)) - } - return false + win := e.ui.NewWindow(e.io.Stdout, o.log) + err := fn(win, win) + win.Close() + if err != nil { + fmt.Fprintf(o.log, "%v\n", err) + if f, ok := e.io.Stdout.(*os.File); ok && ui.IsTerminal(f) { + fmt.Fprint(e.io.Stderr, ui.Tail(o.logPath, 20)) } - return true } - stopBase := func() { _ = b.Stop(ctx, building) } + return err +} - fmt.Fprintln(e.io.Stdout, "Creating base VM...") - none := []vm.Mount{} - if !windowed(func(w io.Writer) error { - return b.Create(ctx, building, "template:debian-13", vm.Settings{CPUs: cpus, MemoryGiB: memory, DiskGiB: disk, Mounts: &none, NoContainerd: true}, w) - }) { - fmt.Fprintf(e.io.Stderr, "Error: Failed to create base VM. Full log: %s\n", logPath) - return 1 - } - (&start{app: e, ctx: ctx, name: building}).printResources() +// errSetupLog is why the log of the build could not be made. +type errSetupLog struct{ error } - fmt.Fprintln(e.io.Stdout, "Starting base VM (the first run downloads a Debian image)...") - if !windowed(func(w io.Writer) error { return b.Start(ctx, building, w) }) { - ha := filepath.Join(LimaHome(e.state), building, "ha.stderr.log") - fmt.Fprintf(e.io.Stderr, "Error: Failed to start base VM. Full log: %s\nLima's own log: %s\n", logPath, ha) - host.WindowsStartHint(e.io.Stderr, logPath, ha) +// buildBase makes the base template: built as `building` (created, booted, +// provisioned by the setup script and the user's, stopped), then swapped in +// for the base and marked as ready. A setup that fails before the swap +// leaves the base it found. +func (e *app) buildBase(ctx context.Context, in install, disk, memory, cpus int) int { + eng, err := e.engine() + if err != nil { + fmt.Fprintf(e.io.Stderr, "Error: %v\n", err) return 1 } - - // The choices as export lines, then the script, on stdin. Without them it - // installs the default set. - fmt.Fprintln(e.io.Stdout, "Installing packages inside VM...") - if !windowed(func(w io.Writer) error { - code, err := b.Shell(ctx, building, vm.ShellOpts{Args: []string{"bash", "-l"}, - Stdin: strings.NewReader(in.exports() + agentvm.SetupScript), Stdout: w, Stderr: w}) - if err == nil && code != 0 { - err = fmt.Errorf("exit status %d", code) + t := provision.AgentVMChoices(in) + t.Name = vmname.Template + t.CPUs, t.MemoryGiB, t.DiskGiB = cpus, memory, disk + if user, ok := readRegular(e.state.Path("setup.sh")); ok { + t.Scripts = append(t.Scripts, provision.Script{ + Name: customSetup, + Message: fmt.Sprintf("Running custom setup from %s...", e.state.Path("setup.sh")), + Shell: []string{"zsh", "-l"}, + Content: env.StripCR(user), + }) + } + out := &setupOutput{e: e} + defer func() { + if out.log != nil { + out.log.Close() } - return err - }) { - fmt.Fprintf(e.io.Stderr, "Error: Setup script failed. Full log: %s\n", logPath) - stopBase() + }() + err = eng.Setup(ctx, t, out) + var se *agentvm.SetupError + switch { + case err == nil: + case errors.As(err, &se) && se.Told: return 1 - } - - if user, ok := readRegular(e.state.Path("setup.sh")); ok { - fmt.Fprintf(e.io.Stdout, "Running custom setup from %s...\n", e.state.Path("setup.sh")) - code, err := b.Shell(ctx, building, vm.ShellOpts{Args: []string{"zsh", "-l"}, - Stdin: strings.NewReader(env.StripCR(user)), Stdout: e.io.Stdout, Stderr: e.io.Stderr}) - if err != nil || code != 0 { - fmt.Fprintln(e.io.Stderr, "Error: Custom setup script failed.") - stopBase() + case errors.As(err, &se): + var logErr errSetupLog + if errors.As(se.Err, &logErr) { + fmt.Fprintf(e.io.Stderr, "Error: %v\n", logErr.error) return 1 } - } - - // Ready only once stopped: Lima clones only a stopped instance. - if !e.stopVM(ctx, building) { - fmt.Fprintf(e.io.Stderr, "Error: the new base VM is set up but did not stop: 'agent-vm stop %s', then 'agent-vm setup' again. The base from before is untouched.\n", building) + switch { + case se.Step == "create": + fmt.Fprintf(e.io.Stderr, "Error: Failed to create base VM. Full log: %s\n", out.logPath) + case se.Step == "start": + fmt.Fprintf(e.io.Stderr, "Error: Failed to start base VM. Full log: %s\nLima's own log: %s\n", out.logPath, se.LimaLog) + host.WindowsStartHint(e.io.Stderr, out.logPath, se.LimaLog) + case se.Script == customSetup: + fmt.Fprintln(e.io.Stderr, "Error: Custom setup script failed.") + default: + fmt.Fprintf(e.io.Stderr, "Error: Setup script failed. Full log: %s\n", out.logPath) + } return 1 - } - if !e.swapBase(ctx, b) { + default: + fmt.Fprintf(e.io.Stderr, "Error: %v\n", err) return 1 } @@ -504,3 +412,15 @@ func (e *app) buildBase(ctx context.Context, in install, disk, memory, cpus int) } return 0 } + +// isFile and readRegular: a regular file, once links are followed; another +// kind (a FIFO, a device) is not read, it could block. +func isFile(p string) bool { fi, err := os.Stat(p); return err == nil && fi.Mode().IsRegular() } + +func readRegular(p string) (string, bool) { + if !isFile(p) { + return "", false + } + b, err := os.ReadFile(p) + return string(b), err == nil +} diff --git a/internal/cli/setup_test.go b/internal/cli/setup_test.go index eb18cc40..00c29936 100644 --- a/internal/cli/setup_test.go +++ b/internal/cli/setup_test.go @@ -11,7 +11,8 @@ import ( agentvm "github.com/sylvinus/agent-vm" "github.com/sylvinus/agent-vm/internal/host" - "github.com/sylvinus/agent-vm/internal/state" + "github.com/sylvinus/agent-vm/internal/policy" + "github.com/sylvinus/agent-vm/internal/version" "github.com/sylvinus/agent-vm/internal/vm" "github.com/sylvinus/agent-vm/internal/vm/vmtest" "github.com/sylvinus/agent-vm/internal/vmname" @@ -28,6 +29,11 @@ func setupRun(t *testing.T, args ...string) (*testEnv, int, []string) { te.fake.ShellFunc = func(name string, o vm.ShellOpts) int { b, _ := io.ReadAll(o.Stdin) stdin = append(stdin, string(b)) + // The build booted with the policies of now, as every VM does: its + // hostagent reads them from its folder. + if _, err := policy.Read(te.state.Policy(name)); err != nil { + t.Errorf("no policy for %s to boot with: %v", name, err) + } return 0 } code := te.setupBase(append([]string{"setup"}, args...)) @@ -199,8 +205,8 @@ func TestSetupFailures(t *testing.T) { if log, _ := os.ReadFile(te.state.Path("setup.log")); !strings.Contains(string(log), "boom") { t.Errorf("%s: log %q", c.op, log) } - if _, err := os.Stat(te.state.Path(state.BaseVersion)); err == nil { - t.Errorf("%s: ready marker", c.op) + if te.state.BaseReady(vmname.Template) { + t.Errorf("%s: marked ready", c.op) } } // A base that does not stop is not marked ready: Lima clones only a @@ -210,7 +216,7 @@ func TestSetupFailures(t *testing.T) { if te.setupBase([]string{"setup", "--preinstall=none"}) != 1 || !strings.Contains(te.out(), "the new base VM is set up but did not stop") { t.Errorf("running base: %s", te.out()) } - if _, err := os.Stat(te.state.Path(state.BaseVersion)); err == nil { + if te.state.BaseReady(vmname.Template) { t.Error("a running base was marked ready") } } @@ -243,8 +249,8 @@ func TestSetupBase(t *testing.T) { if len(stdin) != 1 { t.Errorf("guest stdin: %q", stdin) } - if te.state.Read(state.BaseBuiltBy) == "" || te.state.Read(state.BaseVersion) == "" { - t.Error("markers not written") + if b := te.state.Known(vmname.Template).Base; b == nil || b.Built <= 0 || b.BuiltBy != version.Version || b.Name != vmname.Template { + t.Errorf("the base is not recorded: %+v", b) } if !strings.Contains(te.out(), "Base VM ready.") || strings.Contains(te.out(), "agent-vm claude") { t.Errorf("output: %s", te.out()) @@ -268,7 +274,7 @@ func TestSetupBase(t *testing.T) { t.Errorf("setup.sh: %q", got) } // The base from before is untouched. - if _, err := os.Stat(te.state.Path(state.BaseVersion)); err != nil || te.fake.VMs[vmname.Template] == nil { + if !te.state.BaseReady(vmname.Template) || te.fake.VMs[vmname.Template] == nil { t.Error("a failed setup took the base from before") } // Lima's override.yaml: nothing made. diff --git a/internal/cli/start_test.go b/internal/cli/start_test.go index d0f5c8fc..d2bad16d 100644 --- a/internal/cli/start_test.go +++ b/internal/cli/start_test.go @@ -1,7 +1,6 @@ package cli import ( - "context" "errors" "fmt" "io" @@ -11,7 +10,9 @@ import ( "strings" "testing" + "github.com/sylvinus/agent-vm/internal/engine" "github.com/sylvinus/agent-vm/internal/paths" + "github.com/sylvinus/agent-vm/internal/policy" "github.com/sylvinus/agent-vm/internal/state" "github.com/sylvinus/agent-vm/internal/vm" "github.com/sylvinus/agent-vm/internal/vm/vmtest" @@ -25,10 +26,13 @@ type guest struct { lost bool payloads []string ran [][]string + // local are the VMs that share nothing, and have the project's folder + // of their own disk (scratch ones always). + local map[string]bool } func (g *guest) shell(name string, o vm.ShellOpts) int { - if len(o.Args) > 2 && o.Args[0] == "sh" && o.Args[2] == probeScript { + if len(o.Args) > 2 && o.Args[0] == "sh" && o.Args[2] == engine.ProbeScript { b, _ := io.ReadAll(o.Stdin) g.payloads = append(g.payloads, string(b)) fmt.Fprintln(o.Stdout, "env-ok") @@ -36,7 +40,7 @@ func (g *guest) shell(name string, o vm.ShellOpts) int { fmt.Fprintln(o.Stdout, "share-ok") } v := g.fake.VMs[name] - if g.lost || v == nil || len(v.Mounts) == 0 && !strings.Contains(name, "scratch") || len(v.Mounts) > 0 && !v.Mounts[0].Writable { + if g.lost || v == nil || len(v.Mounts) == 0 && !strings.Contains(name, "scratch") && !g.local[name] || len(v.Mounts) > 0 && !v.Mounts[0].Writable { return 1 } return 0 @@ -89,8 +93,9 @@ func newStartEnv(t *testing.T, answers ...string) *startEnv { func (se *startEnv) base() { se.fake.VMs[vmname.Template] = &vm.Instance{Name: vmname.Template, Status: vm.Stopped, CPUs: 1, Memory: 3 << 30, Disk: 10 << 30} - os.WriteFile(se.state.Path(state.BaseVersion), []byte("1759000000\n"), 0o644) - os.WriteFile(se.state.Path(state.BaseBuiltBy), []byte("0.3.0\n"), 0o644) + se.state.UpdateMeta(vmname.Template, func(m *state.Metadata) { + m.Base = &state.Base{Name: vmname.Template, Built: 1759000000, BuiltBy: "0.3.0"} + }) } func TestStartNoBase(t *testing.T) { @@ -127,10 +132,12 @@ func TestStartNewVM(t *testing.T) { if last[0] != paths.Guest(se.proj) || last[1] != "zsh" || last[len(last)-2] != "echo" || last[len(last)-1] != "hi" { t.Errorf("command: %q", last) } - for _, m := range []string{state.VersionOf, state.BuiltByOf} { - if _, err := os.Stat(se.state.Marker(m, name)); err != nil { - t.Errorf("marker %s not recorded", m) - } + if b := se.state.Known(name).Base; b == nil || *b != (state.Base{Name: vmname.Template, Built: 1759000000, BuiltBy: "0.3.0"}) { + t.Errorf("the base it was cloned from is not recorded: %+v", b) + } + // Its policies, as the hostagent will read them. + if pf, err := policy.Read(se.state.Policy(name)); err != nil || pf.Network == nil || pf.App != "agent-vm" { + t.Errorf("policy.json: %+v %v", pf, err) } // Again: nothing to change, nothing restarted. se.run("run", "true") @@ -143,32 +150,26 @@ func TestStartNewVM(t *testing.T) { } } -// The resources asked against the VM's: in bytes, the disk one way (it -// only grows), a VM that cannot be read counts as a change. -func TestResourcesDiffer(t *testing.T) { - inst := &vm.Instance{CPUs: 2, Memory: 4 << 30, Disk: 20 << 30} - for _, c := range []struct { - o VMOpts - want bool - }{ - {VMOpts{}, false}, - {VMOpts{CPUs: 2, Memory: 4, Disk: 20}, false}, - {VMOpts{CPUs: 3}, true}, - {VMOpts{Memory: 5}, true}, - {VMOpts{Disk: 10}, false}, - {VMOpts{Disk: 30}, true}, - } { - s := &start{opts: c.o} - if got := s.resourcesDiffer(inst); got != c.want { - t.Errorf("%+v: %v", c.o, got) - } +// A VM left running without Lima's hostagent is stopped by force, then +// started again. +func TestStartOrphaned(t *testing.T) { + se := newStartEnv(t) + se.base() + name := vmname.Name(se.proj) + if code := se.run("run", "true"); code != 0 { + t.Fatalf("run: %d %s", code, se.out()) } - if !(&start{opts: VMOpts{CPUs: 2}}).resourcesDiffer(nil) { - t.Error("an unknown VM is no change") + v := se.fake.VMs[name] + v.Status, v.Orphaned = vm.Broken, true + if se.run("run", "true") != 0 || !strings.Contains(se.out(), "left running without Lima's host agent") || + !strings.HasPrefix(se.fake.CallLog(), "stop "+name) || !strings.Contains(se.fake.CallLog(), "start "+name) || v.Status != vm.Running { + t.Errorf("orphaned: %s\n%s", se.out(), se.fake.CallLog()) } - // Half a GiB more than asked: a change. - if !(&start{opts: VMOpts{Memory: 4}}).resourcesDiffer(&vm.Instance{Memory: 4<<30 + 1<<29}) { - t.Error("compared in whole GiB") + // One that does not stop: no start. + v.Status, v.Orphaned = vm.Broken, true + se.fake.Sticky[name] = true + if se.run("run", "true") != 1 || !strings.Contains(se.out(), "could not stop VM") || strings.Contains(se.fake.CallLog(), "start") { + t.Errorf("orphaned, stop failed: %s\n%s", se.out(), se.fake.CallLog()) } } @@ -176,7 +177,7 @@ func TestResourcesDiffer(t *testing.T) { func TestStartPartialVM(t *testing.T) { se := newStartEnv(t) se.base() - d := filepath.Join(LimaHome(se.state), vmname.Name(se.proj)) + d := se.state.VM(vmname.Name(se.proj)) os.MkdirAll(d, 0o755) os.WriteFile(filepath.Join(d, "basedisk"), nil, 0o644) if se.run("run", "true") != 0 || !strings.Contains(se.out(), "Detected partial VM state at "+d) { @@ -222,8 +223,8 @@ func TestStartFailures(t *testing.T) { if se.run("run", "true") != 1 || strings.Contains(se.fake.CallLog(), "start") || se.fake.VMs[name] != nil { t.Errorf("%s fails: %s\n%s", op, se.out(), se.fake.CallLog()) } - if _, err := os.Stat(se.state.Marker(state.VersionOf, name)); err == nil { - t.Errorf("%s fails: recorded", op) + if _, err := os.Stat(se.state.VM(name)); err == nil { + t.Errorf("%s fails: its folder is left", op) } } se.fake.FailOn = nil @@ -350,55 +351,6 @@ func TestStartReadonly(t *testing.T) { } } -// beforeBoot holds every check that applies: --readonly with .git -// protected refuses a protected share left writable. -func TestBeforeBootAll(t *testing.T) { - se := newStartEnv(t) - se.base() - if se.run("run", "true") != 0 { - t.Fatal(se.out()) - } - name := vmname.Name(se.proj) - se.fake.VMs[name].Status = vm.Stopped - s := &start{app: se.app, ctx: context.Background(), name: name, opts: VMOpts{ReadOnly: true}, protect: true, names: []string{".git", ".hg"}} - if s.beforeBoot() == nil || !strings.Contains(se.stderr.String(), "a writable share of "+se.proj) { - t.Errorf("--readonly, protected, writable: %s", se.out()) - } - s.opts.ReadOnly = false - if s.beforeBoot() != nil { - t.Errorf("writable asked: %s", se.out()) - } -} - -// The verdicts on a VM's shares: confined first, then the names or the -// read-only mode. -func TestShareVerdicts(t *testing.T) { - b := func(names ...string) *vm.SSHFS { return &vm.SSHFS{SFTPDriver: "builtin", ReadonlyNames: names} } - inst := func(mt string, ms ...vm.Mount) *vm.Instance { return &vm.Instance{MountType: mt, Mounts: ms} } - rs := vm.ReverseSSHFS - for _, c := range []struct { - inst *vm.Instance - names []string - unprot, notRO string - }{ - {inst(rs, vm.Mount{Location: "/p", SSHFS: b(".git", ".hg")}), []string{".git"}, "", ""}, - {inst(rs, vm.Mount{Location: "/p", SSHFS: b(".git", ".hg", "tools")}), []string{".git", ".hg"}, "", ""}, - {inst(rs, vm.Mount{Location: "/p", SSHFS: b(".git")}), []string{".git", "tools"}, "lacks the read-only names", ""}, - {inst(rs, vm.Mount{Location: "/p", SSHFS: b()}), nil, "lacks the read-only names", ""}, - {inst(rs, vm.Mount{Location: "/p", Writable: true, SSHFS: b(".git", ".hg", ".sl", ".jj")}), nil, "", "a writable share of /p"}, - {inst(rs, vm.Mount{Location: "/p", SSHFS: b("tools")}), nil, "lacks the read-only names", ""}, - {inst(rs, vm.Mount{Location: "/p", SSHFS: &vm.SSHFS{ReadonlyNames: []string{".git"}}}), nil, "not served by the builtin SFTP server", "not served by the builtin SFTP server"}, - {inst("9p", vm.Mount{Location: "/p", SSHFS: b(".git")}), nil, "the mount type", "the mount type"}, - } { - if got := unprotected(c.inst, c.names); c.unprot == "" && got != "" || !strings.Contains(got, c.unprot) { - t.Errorf("unprotected %+v %v: %q", c.inst.Mounts, c.names, got) - } - if got := notReadOnly(c.inst); c.notRO == "" && got != "" || !strings.Contains(got, c.notRO) { - t.Errorf("notReadOnly %+v: %q", c.inst.Mounts, got) - } - } -} - // Lima's own config: its override files refuse everything; a VM config it // cannot read is said, nothing touched. func TestStartLimaConfig(t *testing.T) { @@ -752,14 +704,150 @@ func TestStartScratch(t *testing.T) { for _, n := range []string{gone, used} { se.fake.VMs[n] = &vm.Instance{Name: n, Status: vm.Running} } - os.WriteFile(se.state.Marker(state.ScratchOf, gone), []byte("999999999\n"), 0o644) - os.WriteFile(se.state.Marker(state.ScratchOf, used), []byte(fmt.Sprintln(os.Getpid())), 0o644) + se.state.UpdateMeta(gone, func(m *state.Metadata) { m.ScratchPID = 999999999 }) + se.state.UpdateMeta(used, func(m *state.Metadata) { m.ScratchPID = os.Getpid() }) se.run("--scratch", "run", "true") if se.fake.VMs[gone] != nil || se.fake.VMs[used] == nil { t.Errorf("leftovers: %s", se.out()) } } +// --name: a second VM on the folder, bound to it. +func TestStartNamed(t *testing.T) { + se := newStartEnv(t) + se.base() + here := vmname.Name(se.proj) + if se.run("--name", "agent2", "run", "true") != 0 { + t.Fatalf("first run: %s", se.out()) + } + v := se.fake.VMs["agent2"] + if v == nil || v.Status != vm.Running || len(v.Mounts) != 1 || v.Mounts[0].Location != se.proj || se.fake.VMs[here] != nil { + t.Fatalf("VM: %+v\n%s", v, se.fake.CallLog()) + } + if got := se.state.Known("agent2").Bound; got != se.proj { + t.Errorf("bound to %q, want %q", got, se.proj) + } + // The folder's own VM is another; the two run side by side. + if se.run("run", "true") != 0 || se.fake.VMs[here] == nil || se.fake.VMs["agent2"] == nil { + t.Errorf("the folder's VM: %s", se.out()) + } + se.run("list") + marked := map[string]bool{} + for _, l := range strings.Split(se.stdout.String(), "\n") { + if rest, ok := strings.CutPrefix(l, "> "); ok { + marked[strings.Fields(rest)[0]] = true + } + } + if !marked[here] || !marked["agent2"] || marked["base"] || len(marked) != 2 { + t.Errorf("list marks %v:\n%s", marked, se.stdout.String()) + } + + // From another folder it stops, naming the folder. + other := filepath.Join(se.home, "other") + os.MkdirAll(other, 0o755) + t.Chdir(other) + t.Setenv("PWD", other) + se.fake.Calls = nil + if se.run("--name", "agent2", "run", "true") != 1 || !strings.Contains(se.out(), "VM 'agent2' belongs to "+se.proj+". Run it from there, or 'agent-vm rm agent2'.") || + strings.Contains(se.fake.CallLog(), "start") || strings.Contains(se.fake.CallLog(), "shell") { + t.Errorf("another folder: %s\n%s", se.out(), se.fake.CallLog()) + } + // And it never loses its shares. + t.Chdir(se.proj) + t.Setenv("PWD", se.proj) + if se.run("--name", "agent2", "--no-shares", "run", "true") != 1 || !strings.Contains(se.out(), "shares "+se.proj+", and keeps doing so") { + t.Errorf("--no-shares on a VM that shares: %s", se.out()) + } + // --reset makes it again, bound anew. + se.g.local = map[string]bool{"agent2": true} + if se.run("--name", "agent2", "--no-shares", "--reset", "run", "true") != 0 || len(se.fake.VMs["agent2"].Mounts) != 0 || se.state.Known("agent2").Bound != "-" { + t.Errorf("--reset: %s\n%s", se.out(), se.fake.CallLog()) + } + // No record of what it belongs to: refused, and told how to give it up. + se.state.UpdateMeta("agent2", func(m *state.Metadata) { m.Bound = "" }) + if se.run("--name", "agent2", "run", "true") != 1 || !strings.Contains(se.out(), "has no record of the folder it belongs to. 'agent-vm rm agent2' deletes it.") { + t.Errorf("no record: %s", se.out()) + } + // The VM keeps its place on rm, like any. + if se.run("rm", "agent2") != 0 || se.fake.VMs["agent2"] != nil { + t.Errorf("rm: %s", se.out()) + } +} + +// --no-shares: a VM that shares nothing, kept, usable from any folder. +func TestStartNoShares(t *testing.T) { + se := newStartEnv(t) + se.base() + os.WriteFile(filepath.Join(se.proj, ".agent-vm.env"), []byte("P=1\n"), 0o600) + os.WriteFile(se.state.Path("env"), []byte("S=1\n"), 0o600) + se.g.local = map[string]bool{"sandbox": true} + if se.run("--name", "sandbox", "--no-shares", "run", "true") != 0 { + t.Fatalf("run: %s", se.out()) + } + v := se.fake.VMs["sandbox"] + if v == nil || v.Status != vm.Running || len(v.Mounts) != 0 || se.state.Known("sandbox").Bound != "-" { + t.Fatalf("VM: %+v %+v", v, se.state.Known("sandbox")) + } + if len(se.g.payloads) != 1 || se.g.payloads[0] != "S=1\n" { + t.Errorf("only the shared env goes in: %q", se.g.payloads) + } + if last := se.g.ran[len(se.g.ran)-1]; last[0] != paths.Guest(se.proj) { + t.Errorf("working folder %q", last[0]) + } + // From another folder, without saying --no-shares again: it still has + // none, and that folder's path is made in the VM. + other := filepath.Join(se.home, "other") + os.MkdirAll(other, 0o755) + t.Chdir(other) + t.Setenv("PWD", other) + if se.run("--name", "sandbox", "run", "true") != 0 || len(se.fake.VMs["sandbox"].Mounts) != 0 { + t.Errorf("another folder: %s\n%s", se.out(), se.fake.CallLog()) + } + if !strings.Contains(se.fake.CallLog(), "install -d") || !strings.Contains(se.fake.CallLog(), paths.Guest(other)) { + t.Errorf("the folder is not made in the VM:\n%s", se.fake.CallLog()) + } + // The project's checks do not apply: no question, whatever it holds. + exec.Command("git", "-C", other, "init", "-q").Run() + exec.Command("git", "-C", other, "config", "core.hooksPath", ".").Run() + if se.run("--name", "sandbox", "run", "true") != 0 || strings.Contains(se.out(), "core.hooksPath") { + t.Errorf("checks on a VM with no shares: %s", se.out()) + } +} + +func TestStartNamedUsage(t *testing.T) { + se := newStartEnv(t) + se.base() + for args, want := range map[string]string{ + "--scratch --name x run true": "--scratch and --name do not go together: --no-shares --rm --name x", + "--no-shares run true": "--no-shares needs --name", + "--name x --no-shares --readonly run true": "--no-shares and --readonly do not go together", + "--name Agent run true": "--name: a name is lowercase letters", + "--name base run true": "'base' is reserved", + "--name base-x run true": "'base-x' is reserved", + "--name proj-1a2b3c4d run true": "--name: a name does not end with a dash and 8 hexadecimal digits", + "--name run true": "", + } { + code := se.run(strings.Fields(args)...) + if want == "" { + // "run" is the name here: the command is true. + continue + } + if code != exitUsage || !strings.Contains(se.out(), want) || se.fake.CallLog() != "" { + t.Errorf("%s: %d %s\n%s", args, code, se.out(), se.fake.CallLog()) + } + } + // The options mean nothing to the commands that do not start a VM. + if se.run("--name", "x", "list") != exitUsage || !strings.Contains(se.out(), "--name is an option for the commands that start a VM") { + t.Errorf("--name list: %s", se.out()) + } + if se.run("--name") != exitUsage || !strings.Contains(se.out(), "--name needs a value.") { + t.Errorf("--name: %s", se.out()) + } + if se.run("--name=", "run", "true") != exitUsage || !strings.Contains(se.out(), "--name needs a name.") { + t.Errorf("--name=: %s", se.out()) + } +} + // On a terminal: a no opens a shell in the VM, then asks again. func TestStartScratchAsk(t *testing.T) { se := newStartEnv(t, "n", "y") diff --git a/internal/cli/vms.go b/internal/cli/vms.go index f4af937f..d8ea8162 100644 --- a/internal/cli/vms.go +++ b/internal/cli/vms.go @@ -34,7 +34,7 @@ func (e *app) target(ctx context.Context, verb string, args []string) (name stri fmt.Fprintf(e.io.Stderr, "Usage: agent-vm %s [vm-name]\n", verb) return "", exitUsage } - b, err := e.backend() + b, err := e.backend(ctx) if err == nil { _, err = b.Get(ctx, name) } @@ -69,13 +69,8 @@ func (e *app) stopCmd(ctx context.Context, args []string) int { // stopVM stops name, and reports whether it is known to be stopped. func (e *app) stopVM(ctx context.Context, name string) bool { - b, err := e.backend() - if err != nil { - return false - } - _ = b.Stop(ctx, name) - inst, err := b.Get(ctx, name) - return err == nil && inst.Status != vm.Running + eng, err := e.engine() + return err == nil && eng.Stop(ctx, name) } func (e *app) rmCmd(ctx context.Context, args []string) int { @@ -89,33 +84,16 @@ func (e *app) rmCmd(ctx context.Context, args []string) int { return 0 } -// removeVM stops and deletes name, saying so, and reports whether it is gone. -func (e *app) removeVM(ctx context.Context, name string) bool { - fmt.Fprintf(e.io.Stdout, "Stopping and deleting VM '%s'...\n", name) - if !e.deleteVM(ctx, name) { - return false - } - fmt.Fprintln(e.io.Stdout, "VM destroyed.") - return true -} - // deleteVM stops and deletes name, then forgets what agent-vm knew of it. // Fails, saying so, when it is still there (or cannot be asked): --reset // would otherwise go on with the old VM and its shares. func (e *app) deleteVM(ctx context.Context, name string) bool { - b, err := e.backend() - if err == nil { - _ = b.Delete(ctx, name) - _, err = b.Get(ctx, name) - } - if !errors.Is(err, vm.ErrNotFound) { + eng, err := e.engine() + if err != nil { fmt.Fprintf(e.io.Stderr, "Error: could not delete VM '%s'. See 'agent-vm list', then retry.\n", name) return false } - if err := e.state.Forget(name, vmname.Template); err != nil { - fmt.Fprintf(e.io.Stderr, "Warning: %v\n", err) - } - return true + return eng.Delete(ctx, name) } // `destroy-all`: every agent-vm VM, the base template included: the command @@ -168,7 +146,7 @@ func (e *app) destroyAllCmd(ctx context.Context, args []string) int { // agentVMs lists the VMs, after moving 0.2's: for the commands that change // them. func (e *app) agentVMs(ctx context.Context) ([]*vm.Instance, error) { - b, err := e.backend() + b, err := e.backend(ctx) if err != nil { return nil, err } @@ -185,16 +163,17 @@ func (e *app) lookVMs(ctx context.Context) ([]*vm.Instance, error) { } // `list`, `status`: the agent-vm VMs as Lima's table shows them, the -// current directory's marked with >, and the base each was cloned from in a -// last column. Read-only: 0.2's VMs are said to wait, not moved. +// current directory's marked with > (its own, and the named ones that share +// it), and the base each was cloned from in a last column. Read-only: 0.2's +// VMs are said to wait, not moved. func (e *app) listCmd(ctx context.Context, args []string) int { if len(args) > 0 { fmt.Fprintln(e.io.Stderr, "Usage: agent-vm list") return exitUsage } - current := "" - if wd, err := vmname.AbsDir(""); err == nil { - current = vmname.Name(wd) + current, wd := "", "" + if d, err := vmname.AbsDir(""); err == nil { + current, wd = vmname.Name(d), d } vms, err := e.lookVMs(ctx) if err != nil { @@ -204,6 +183,9 @@ func (e *app) listCmd(ctx context.Context, args []string) int { if old, from := e.unmovedOld(); len(old) > 0 { fmt.Fprintf(e.io.Stderr, "%d VM(s) of agent-vm 0.2 in %s are not listed: the next command that starts a VM offers to move them.\n", len(old), from) } + if left := e.layoutPending(); len(left) > 0 { + fmt.Fprintf(e.io.Stderr, "%d VM(s) of agent-vm 0.3.0 in %s are not listed: the next command that starts a VM moves them.\n", len(left), LimaHome(e.state)) + } if len(vms) == 0 { fmt.Fprintln(e.io.Stdout, " (no VMs)") return 0 @@ -216,10 +198,10 @@ func (e *app) listCmd(ctx context.Context, args []string) int { fmt.Fprintf(e.io.Stdout, " %-*s %s\n", w, lines[0], "BASE") for i, l := range lines[1:] { mark := " " - if vms[i].Name == current { + if vms[i].Name == current || wd != "" && e.state.Known(vms[i].Name).Bound == wd { mark = ">" } - fmt.Fprintf(e.io.Stdout, "%s %-*s %s\n", mark, w, l, e.state.BaseLabel(vms[i].Name, vmname.Template)) + fmt.Fprintf(e.io.Stdout, "%s %-*s %s\n", mark, w, l, e.state.BaseLabel(vms[i].Name)) } return 0 } diff --git a/internal/cli/vms_test.go b/internal/cli/vms_test.go index 1c078ab7..0405be30 100644 --- a/internal/cli/vms_test.go +++ b/internal/cli/vms_test.go @@ -42,10 +42,10 @@ func newTestEnv(t *testing.T, fake *vmtest.Fake, answers ...string) *testEnv { u.OpenTTY = func() (io.ReadCloser, error) { return &answer{pop: pop}, nil } } te.app = &app{ - io: IO{Stdin: strings.NewReader(""), Stdout: &te.stdout, Stderr: &te.stderr}, - ui: u, - state: state.Dir(t.TempDir()), - newBack: func() (vm.Backend, error) { return fake, nil }, + io: IO{Stdin: strings.NewReader(""), Stdout: &te.stdout, Stderr: &te.stderr}, + ui: u, + state: state.Dir(t.TempDir()), + testBackend: fake, } return te } @@ -92,21 +92,33 @@ func TestStopRm(t *testing.T) { if te.run("stop", "proj-deadbeef") != 1 || !strings.Contains(te.out(), "is still running") { t.Errorf("stop that did not take: %s", te.out()) } + // A Broken VM is not stopped: said so. An orphaned one is stopped by force. + te.fake.Sticky = map[string]bool{} + te.fake.VMs["proj-deadbeef"].Status = vm.Broken + if te.run("stop", "proj-deadbeef") != 1 || !strings.Contains(te.out(), "is still running") { + t.Errorf("stop of a Broken VM: %s", te.out()) + } + te.fake.VMs["proj-deadbeef"].Orphaned = true + if te.run("stop", "proj-deadbeef") != 0 || !strings.HasSuffix(te.out(), "VM stopped.\n") || te.fake.VMs["proj-deadbeef"].Status != vm.Stopped { + t.Errorf("stop of an orphaned VM: %s", te.out()) + } + te.fake.VMs["proj-deadbeef"].Status = vm.Running + te.fake.Sticky["proj-deadbeef"] = true // Nor a delete that did not take. - os.WriteFile(te.state.Marker(state.VersionOf, "proj-deadbeef"), []byte("1\n"), 0o644) + te.state.UpdateMeta("proj-deadbeef", func(m *state.Metadata) { m.Term = "xterm" }) if te.run("rm", "proj-deadbeef") != 1 || !strings.Contains(te.out(), "could not delete VM 'proj-deadbeef'") || strings.Contains(te.out(), "VM destroyed.") { t.Errorf("rm that did not take: %s", te.out()) } - if _, err := os.Stat(te.state.Marker(state.VersionOf, "proj-deadbeef")); err != nil { + if te.state.Known("proj-deadbeef").Term != "xterm" { t.Error("a VM still there was forgotten") } te.fake.Sticky = map[string]bool{} if te.run("rm", "proj-deadbeef") != 0 || te.fake.CallLog() != "delete proj-deadbeef" || !strings.HasSuffix(te.out(), "VM destroyed.\n") { t.Errorf("rm : %s %s", te.fake.CallLog(), te.out()) } - if _, err := os.Stat(te.state.Marker(state.VersionOf, "proj-deadbeef")); !os.IsNotExist(err) { - t.Error("rm left the VM's markers") + if _, err := os.Stat(te.state.VM("proj-deadbeef")); !os.IsNotExist(err) { + t.Error("rm left the VM's folder") } // agent-vm's Lima home holds its VMs alone: any name is looked up. @@ -152,17 +164,12 @@ func TestDestroyAll(t *testing.T) { } te.fake.Sticky = map[string]bool{} te.fake.VMs["base"] = running("base") - os.MkdirAll(string(te.state), 0o755) - for _, f := range []string{state.BaseVersion, state.BaseBuiltBy} { - os.WriteFile(te.state.Path(f), []byte("x\n"), 0o644) - } + te.state.UpdateMeta("base", func(m *state.Metadata) { m.Base = &state.Base{Name: "base", Built: 5, BuiltBy: "0.4.0"} }) if te.run("destroy-all") != 0 || !strings.HasSuffix(te.stdout.String(), "All VMs destroyed.\n") || len(te.fake.VMs) != 0 { t.Errorf("a yes: %s", te.out()) } - for _, f := range []string{state.BaseVersion, state.BaseBuiltBy} { - if _, err := os.Stat(te.state.Path(f)); err == nil { - t.Errorf("%s left: the next start would take a missing base as ready", f) - } + if te.state.BaseReady("base") { + t.Error("the base is left ready: the next start would take a missing base as ready") } if te.run("destroy-all") != 0 || te.out() != "No agent-vm VMs found.\n" { t.Errorf("none left: %q", te.out()) @@ -185,9 +192,8 @@ func TestList(t *testing.T) { te.fake.VMs[here] = &vm.Instance{Name: here, Status: vm.Running, SSHAddress: "127.0.0.1", SSHLocalPort: 60022, VMType: "qemu", Arch: "aarch64", CPUs: 2, Memory: 4 << 30, Disk: 10 << 30, Dir: filepath.Join(proj, "lima", here)} te.fake.VMs["base"] = &vm.Instance{Name: "base", Status: vm.Stopped, VMType: "qemu", Arch: "aarch64", CPUs: 1, Memory: 2 << 30, Disk: 10 << 30} - os.WriteFile(te.state.Path(state.BaseVersion), []byte("1759000000\n"), 0o644) - os.WriteFile(te.state.Path(state.BaseBuiltBy), []byte("0.2.1\n"), 0o644) - os.WriteFile(te.state.Marker(state.BuiltByOf, here), []byte("0.2.0\n"), 0o644) + te.state.UpdateMeta("base", func(m *state.Metadata) { m.Base = &state.Base{Name: "base", Built: 1759000000, BuiltBy: "0.2.1"} }) + te.state.UpdateMeta(here, func(m *state.Metadata) { m.Base = &state.Base{Name: "base", BuiltBy: "0.2.0"} }) if te.run("status") != 0 { t.Fatal(te.out()) } diff --git a/internal/cli/alive_unix.go b/internal/engine/alive_unix.go similarity index 94% rename from internal/cli/alive_unix.go rename to internal/engine/alive_unix.go index d00650dd..63f2a04d 100644 --- a/internal/cli/alive_unix.go +++ b/internal/engine/alive_unix.go @@ -1,6 +1,6 @@ //go:build !windows -package cli +package engine import ( "errors" diff --git a/internal/cli/alive_windows.go b/internal/engine/alive_windows.go similarity index 96% rename from internal/cli/alive_windows.go rename to internal/engine/alive_windows.go index a9625c08..b6e81166 100644 --- a/internal/cli/alive_windows.go +++ b/internal/engine/alive_windows.go @@ -1,4 +1,4 @@ -package cli +package engine import "golang.org/x/sys/windows" diff --git a/internal/cli/checks.go b/internal/engine/checks.go similarity index 50% rename from internal/cli/checks.go rename to internal/engine/checks.go index 29c32a3e..28977ef5 100644 --- a/internal/cli/checks.go +++ b/internal/engine/checks.go @@ -1,70 +1,59 @@ -package cli +package engine import ( - "context" - "fmt" - "os" "os/exec" "strings" "github.com/sylvinus/agent-vm/internal/gitguard" - "github.com/sylvinus/agent-vm/internal/mounts" - "github.com/sylvinus/agent-vm/internal/vm" + "github.com/sylvinus/agent-vm/internal/guard" + "github.com/sylvinus/agent-vm/internal/netguard" + "github.com/sylvinus/agent-vm/internal/policy" ) -// promptsDisabledBy is what turned the security questions off, if -// something did: --unsafe-disable-security-prompts, or -// AGENT_VM_UNSAFE_DISABLE_SECURITY_PROMPTS=1 in the shell. Never a file of -// the project, which the VM can write. -func (s *start) promptsDisabledBy() (string, bool) { - switch { - case s.opts.UnsafeNoPrompts: - return "--unsafe-disable-security-prompts", true - case os.Getenv("AGENT_VM_UNSAFE_DISABLE_SECURITY_PROMPTS") == "1": - return "AGENT_VM_UNSAFE_DISABLE_SECURITY_PROMPTS=1", true +// promptsDisabledBy is what turned the security questions off, if something +// did: Spec.UnsafeNoPrompts. Never a file of the project, which the VM can +// write. +func (s *session) promptsDisabledBy() (string, bool) { + if s.spec.UnsafeNoPrompts != "" { + return s.spec.UnsafeNoPrompts, true } return "", false } -// confirmUnsafe is the question after a security warning: true to go on. -// No by default, and when it cannot be asked; yes when the questions are -// off. -func (s *start) confirmUnsafe() bool { +// confirmUnsafe is the question after a security warning: true to go on. No +// by default, and when it cannot be asked; yes when the questions are off. +// topic says what the warning was about. +func (s *session) confirmUnsafe(topic string) bool { if by, ok := s.promptsDisabledBy(); ok { - fmt.Fprintf(s.io.Stderr, "Continuing: %s.\n", by) + s.warnf("Continuing: %s.", by) return true } - return s.ui.CanAsk() && s.ui.AskYN("Continue anyway?", false) + return s.canAsk() && s.ask(Question{Kind: QUnsafe, Title: topic, Prompt: "Continue anyway?"}) } -func (s *start) warnf(format string, a ...any) { - fmt.Fprintf(s.io.Stderr, format+"\n", a...) -} - -// writableGitWarning is printed on every start with .git left writable. -func (s *start) writableGitWarning() { +// writableGitWarning is told on every start with .git left writable. +func (s *session) writableGitWarning() { why := "AGENT_VM_UNSAFE_WRITABLE_GIT=1" - if s.opts.UnsafeWritableGit { + if s.spec.UnsafeWritableGit { why = "--unsafe-writable-git" } - fmt.Fprintf(s.io.Stderr, `!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + s.warnf(`!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! WARNING: %s. The VM can write every .git. !! !! The agent can change .git/config and .git/hooks in the shared folders, and !! git on this machine runs what they name: on your next commit, and whenever !! your editor or shell prompt calls git. That is running commands on your !! host, outside the VM. Without it, .git stays read-only. -!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -`, why) +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!`, why) } // checks are the security checks of a start, before anything changes. A // running VM that keeps running (s.vmUp) gets the warnings and nothing is // asked. False when the start must stop. -func (s *start) checks() bool { +func (s *session) checks() bool { s.protect, s.names, s.notes = false, nil, "" - if !s.opts.Scratch { - if writableGitOptOut(s.opts.UnsafeWritableGit) { + if !s.noShares { + if s.writableGitOptOut() { s.writableGitWarning() } else { s.protect = true @@ -78,7 +67,7 @@ func (s *start) checks() bool { // default, and when no one can answer). A folder that cannot be // protected, or one declined, is a risk to accept. So are the // scan's risks. Under --readonly the VM writes none of them. - scan := s.scan(s.ctx, s.dir) + scan := s.Scan(s.ctx, s.dir) var hooks []string for _, h := range scan.Hooks { rel, inRepo, _ := strings.Cut(h, "\t") @@ -86,7 +75,7 @@ func (s *start) checks() bool { continue } name, ok := gitguard.HooksName(inRepo) - if s.opts.ReadOnly { + if s.spec.ReadOnly { if ok { hooks = append(hooks, name) } @@ -101,33 +90,33 @@ func (s *start) checks() bool { default: s.warnf("Warning: git's core.hooksPath is '%s', which the VM can write, and its name cannot be made read-only: git on this machine runs the hooks the agent puts there.", rel) } - if !s.vmUp && !s.confirmUnsafe() { - s.warnf("Aborted. Point core.hooksPath to a folder of the project (agent-vm keeps it read-only), or outside it.") + if !s.vmUp && !s.confirmUnsafe("hooks") { + s.warnf("Aborted. Point core.hooksPath to a folder of the project (%s keeps it read-only), or outside it.", s.app) return false } continue } - if _, off := s.promptsDisabledBy(); !strings.HasPrefix(name, ".") && !s.vmUp && !off && s.ui.CanAsk() && - !s.ui.AskYN(fmt.Sprintf("Git runs hooks from %s. Make every '%s' folder in the project read-only for the VM?", rel, name), true) { + if _, off := s.promptsDisabledBy(); !strings.HasPrefix(name, ".") && !s.vmUp && !off && s.canAsk() && + !s.ask(Question{Kind: QHooks, Title: rel, Prompt: "Git runs hooks from " + rel + ". Make every '" + name + "' folder in the project read-only for the VM?", Default: true}) { s.warnf("Warning: '%s' stays writable, and git on this machine runs the hooks the agent puts in %s.", name, rel) - if !s.confirmUnsafe() { + if !s.confirmUnsafe("hooks") { s.warnf("Aborted. Answer yes to protect it, or point core.hooksPath outside the project.") return false } continue } hooks = append(hooks, name) - s.notes += fmt.Sprintf("Note: git runs hooks from %s (core.hooksPath): every '%s' in the project is read-only for the VM too.\n", rel, name) + s.notes += "Note: git runs hooks from " + rel + " (core.hooksPath): every '" + name + "' in the project is read-only for the VM too.\n" } s.names = gitguard.NamesWith(hooks) // What the names accepted keep read-only is no risk; a name declined // keeps nothing. - if risks := scan.Risks(s.names); !s.opts.ReadOnly && len(risks) > 0 { + if risks := scan.Risks(s.names); !s.spec.ReadOnly && len(risks) > 0 { s.warnf("Warning: git on this machine uses these, and the VM can write them:") for _, r := range risks { s.warnf(" %s", r) } - if !s.vmUp && !s.confirmUnsafe() { + if !s.vmUp && !s.confirmUnsafe("git-config") { s.warnf("Aborted. Move them out of the shared folders, or have them name commands outside them.") return false } @@ -135,66 +124,95 @@ func (s *start) checks() bool { } // A repository not named .git, which no name protects: only a VM that // can write needs the setting. - if !s.opts.ReadOnly && !s.opts.Scratch { + if !s.spec.ReadOnly && !s.noShares { return s.checkBareRepo() } return true } +// writableGitOptOut: Options.UnsafeWritableGit, or Spec.UnsafeWritableGit +// for one start, leaves .git writable. Only the program that runs the engine +// can ask for it. +func (s *session) writableGitOptOut() bool { + return s.spec.UnsafeWritableGit || s.opts.UnsafeWritableGit +} + +// WritableGitOptOut reports whether .git is left writable by the options. +func (e *Engine) WritableGitOptOut(flag bool) bool { return flag || e.opts.UnsafeWritableGit } + // checkBareRepo offers to set safe.bareRepository=explicit when it can ask. // Not set: a security question, no by default. With the questions off, // nothing is offered: the global git config is not changed unasked. For a // VM already running, a warning only. -func (s *start) checkBareRepo() bool { +func (s *session) checkBareRepo() bool { st := gitguard.BareState(s.ctx) if st == gitguard.BareOK || st == gitguard.BareNoGit { return true } if s.vmUp { - s.warnf("Warning: git on this machine uses repositories a VM creates under another name than .git ('agent-vm doctor' says more).") + s.warnf("Warning: git on this machine uses repositories a VM creates under another name than .git ('%s doctor' says more).", s.app) return true } - s.ui.Box("Git: repositories not named .git", gitguard.BareHint) if st == gitguard.BareOld { + s.box("Git: repositories not named .git", gitguard.BareHint) out, _ := exec.CommandContext(s.ctx, "git", "--version").Output() s.warnf("Warning: %s is older than 2.38 and ignores that setting.", strings.TrimSpace(string(out))) - if s.confirmUnsafe() { + if s.confirmUnsafe("bare-repo") { return true } s.warnf("Aborted. Upgrade git, then run the command above.") return false } - if _, off := s.promptsDisabledBy(); !off && s.ui.CanAsk() && s.ui.AskYN("Run it now? It changes your global git config.", true) { - err := exec.CommandContext(s.ctx, "git", "config", "--global", "safe.bareRepository", "explicit").Run() - if err == nil && gitguard.BareState(s.ctx) == gitguard.BareOK { - fmt.Fprintln(s.io.Stdout, "Git on this machine now ignores repositories not named .git unless you name them.") - return true + q := Question{Kind: QBareRepo, Title: "Git: repositories not named .git", Detail: gitguard.BareHint, Prompt: "Run it now? It changes your global git config.", Default: true} + if _, off := s.promptsDisabledBy(); !off && s.canAsk() { + if s.ask(q) { + err := exec.CommandContext(s.ctx, "git", "config", "--global", "safe.bareRepository", "explicit").Run() + if err == nil && gitguard.BareState(s.ctx) == gitguard.BareOK { + s.infof("Git on this machine now ignores repositories not named .git unless you name them.") + return true + } + s.warnf("Warning: the setting did not take.") } - s.warnf("Warning: the setting did not take.") + } else { + s.box(q.Title, q.Detail) } s.warnf("Warning: not set. Until it is, git on this machine can run what a VM writes.") - if s.confirmUnsafe() { + if s.confirmUnsafe("bare-repo") { return true } - s.warnf("Aborted. Run the command above, then run agent-vm again.") + s.warnf("Aborted. Run the command above, then run %s again.", s.app) return false } -// start is one start of a project's VM. -type start struct { - *app - ctx context.Context - opts VMOpts - name string // the VM - dir string // the project, as the shell spells it +// box tells a boxed notice: the program decides how to show a title and a +// body together. +func (s *session) box(title, body string) { + s.emit(Event{Level: Warning, VM: s.name, Title: title, Text: body}) +} - created bool // this run made the VM, and it is still there - vmUp bool // running when the start began, and not reset - protect bool // every .git read-only - names []string // the read-only names, when protect - notes string // hooks notes, printed once the names apply +// notePolicyChange says so when the running VM booted with other policies +// than the ones in force now. A VM with no record (booted by an agent-vm +// that kept none) is not said anything about. +func (s *session) notePolicyChange() { + booted, err := policy.Read(s.state.Policy(s.name)) + if err != nil { + return + } + now, err := s.policyNow() + if err == nil && booted.Fingerprint() == now.Fingerprint() { + return + } + s.warnf("Note: VM '%s' booted with other network or guarded-files settings than now (%s, %s, AGENT_VM_UNSAFE_OPEN_NETWORK, AGENT_VM_GUARDED_WRITES). The running VM keeps the old ones: '%s stop', then run again to apply them.", + s.name, netguard.FileName, guard.FileName, s.app) +} - built []vm.Mount // see shares - builtFiles []mounts.FileMount - builtKey string +// warnDomains says, once per start, that the network policy's domains are +// experimental. +func (s *session) warnDomains() { + if s.opts.Quiet { + return + } + if pf, err := s.policyNow(); err == nil && len(pf.Network.Domains) > 0 { + s.warnf("Warning: the 'domain' lines in %s are experimental: do not rely on them alone to confine a VM. AGENT_VM_QUIET=1 hides this.", s.state.Path(netguard.FileName)) + } } diff --git a/internal/engine/engine.go b/internal/engine/engine.go new file mode 100644 index 00000000..09194fd0 --- /dev/null +++ b/internal/engine/engine.go @@ -0,0 +1,378 @@ +// Package engine runs agent-vm's VMs: the sequence that makes a VM for a +// project exist and run, with its shares and the security checks, and what +// runs in it. It has no terminal: what it tells goes to Options.OnEvent, what +// it asks to Options.Asker, and it reads no AGENT_VM_* variable. The command +// line (internal/cli) is one program that drives it; the public package +// agentvm re-exports it for others. +package engine + +import ( + "context" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/sylvinus/agent-vm/internal/gitguard" + "github.com/sylvinus/agent-vm/internal/mounts" + "github.com/sylvinus/agent-vm/internal/paths" + "github.com/sylvinus/agent-vm/internal/policy" + "github.com/sylvinus/agent-vm/internal/state" + "github.com/sylvinus/agent-vm/internal/vm" + "github.com/sylvinus/agent-vm/internal/vmname" +) + +// Options configure an Engine. Every feature has agent-vm's default when +// its field is the zero value. What lowers a protection is named Unsafe.... +type Options struct { + // StateDir is the folder the engine keeps everything in: the VMs + // (vms/), Lima's own files (lima/) and the user's files (env, + // volumes...). Required, and one per program: two programs must not + // share one, nor Lima's home. + StateDir string + // LimaHome is Lima's home, for what every VM shares (its SSH key, its + // networks); StateDir/lima when empty. Never the home of the user's own + // Lima. + LimaHome string + // AppName names the program in what the user is shown (dialogs and + // messages); "agent-vm" when empty. + AppName string + // Asker answers the questions of the engine; none: every answer is no. + Asker Asker + // OnEvent gets progress, warnings and errors, as they happen, from the + // goroutine that runs the call. Nil drops them. + OnEvent func(Event) + // Assets is Lima's guest agent (gzipped, for the host's architecture, + // lima-guestagent.Linux-.gz) and templates (templates/) at its + // root. Nil: the ones embedded in this binary by agent-vm's own build. + Assets fs.FS + // Backend replaces Lima, for tests. + Backend vm.Backend + + // Policy gives the network and guarded-files policies a VM boots with, + // called at each boot. Nil: Network and Guarded. + Policy func() (*policy.File, error) + // Network is what the VMs may reach; nil: the internet only. + Network *policy.Network + // Guarded are the files the host runs that the VM may not write unasked; + // nil: agent-vm's list. + Guarded *policy.Guarded + + // VolumesFile is a file in the format of agent-vm's volumes: extra + // folders and files to share. EnvFile holds KEY=value lines pushed into + // every VM on each start. RuntimeScript is run on the host after each + // start. ProjectEnvFile and ProjectRuntime give the project's own, by + // its folder ("" for none). All optional. + VolumesFile string + EnvFile string + RuntimeScript string + ProjectEnvFile func(dir string) string + ProjectRuntime func(dir string) string + + // UnsafeWritableGit leaves .git writable for every VM, as Spec's does for + // one start. SSHFSCache turns sshfs's attribute cache on: a change on the + // host can show up to 20 seconds late. + UnsafeWritableGit bool + SSHFSCache bool + // HostShare is the part of the host a VM may use: 2 (half) by default, 1 + // for all of it. It caps the CPUs and the memory asked for. + HostShare int + // Quiet leaves out the notices that are not about a risk to the host. + Quiet bool + // NeverShare are more folders no share may reach, beside the user's home, + // the state folder and Lima's home: the user's own Lima home, say. + NeverShare []string + // TERM is the terminal whose terminfo is installed in the VMs ("" for + // none). + TERM string + // BeforeChange is called once, before the first call that changes VMs + // uses the backend: for a program with VMs of an older version to move. + BeforeChange func(ctx context.Context) +} + +// Engine runs the VMs of one state folder. +type Engine struct { + opts Options + state state.Dir + app string + + mu sync.Mutex // the backend + back vm.Backend + backErr error + backMade bool + changeOne sync.Once +} + +// Open makes an Engine. It touches nothing yet: Lima is started on first use. +func Open(opts Options) (*Engine, error) { + if opts.StateDir == "" { + return nil, errors.New("agent-vm: Options.StateDir is required") + } + dir := opts.StateDir + if rest, ok := strings.CutPrefix(dir, "~/"); ok { + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + dir = filepath.Join(home, rest) + } + abs, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + e := &Engine{opts: opts, state: state.Dir(paths.Host(abs)), app: opts.AppName} + if e.app == "" { + e.app = "agent-vm" + } + if e.opts.Asker == nil { + e.opts.Asker = NoAsker{} + } + if e.opts.LimaHome == "" { + e.opts.LimaHome = e.state.LimaHome() + } else { + e.opts.LimaHome = paths.Host(absOr(e.opts.LimaHome)) + } + // Names that fit this machine's socket paths (a long home, a long user + // name): cut in their folder part, not refused. + vmname.MaxLen = vm.MaxName(e.state.VMs()) + return e, nil +} + +// absOr is p made absolute, as it is when that cannot be done: a relative +// folder would defeat the checks that compare locations. +func absOr(p string) string { + if abs, err := filepath.Abs(p); err == nil { + return abs + } + return p +} + +// State is the state folder. +func (e *Engine) State() state.Dir { return e.state } + +// LimaHome is Lima's home. +func (e *Engine) LimaHome() string { return e.opts.LimaHome } + +// AppName is what the program is called to the user. +func (e *Engine) AppName() string { return e.app } + +// Options are the options the engine was opened with. +func (e *Engine) Options() Options { return e.opts } + +// emit gives ev to the program. +func (e *Engine) emit(ev Event) { + if e.opts.OnEvent != nil { + e.opts.OnEvent(ev) + } +} + +func (e *Engine) infof(format string, a ...any) { + e.emit(Event{Level: Info, Text: fmt.Sprintf(format, a...)}) +} + +func (e *Engine) warnf(format string, a ...any) { + e.emit(Event{Level: Warning, Text: fmt.Sprintf(format, a...)}) +} + +// UserLimaHome is the Lima home of the user's own Lima: $LIMA_HOME, or ~/.lima. +func UserLimaHome() string { + if h := vm.UserLimaHome(); h != "" { + return paths.Host(absOr(h)) + } + return paths.Host(filepath.Join(paths.Home(), ".lima")) +} + +// SameDir reports whether a and b are one folder, links resolved where they +// exist. +func SameDir(a, b string) bool { + if fa, err := os.Stat(a); err == nil { + if fb, err := os.Stat(b); err == nil { + return os.SameFile(fa, fb) + } + } + return filepath.Clean(a) == filepath.Clean(b) +} + +// Backend is the backend of the calls that only look, made on first use: +// most programs never need Lima. A call that changes VMs uses Change. +func (e *Engine) Backend() (vm.Backend, error) { + e.mu.Lock() + defer e.mu.Unlock() + if !e.backMade { + e.backMade = true + e.back, e.backErr = e.newBackend() + } + return e.back, e.backErr +} + +func (e *Engine) newBackend() (vm.Backend, error) { + if e.opts.Backend != nil { + return e.opts.Backend, nil + } + // Every VM there is the engine's: list, rm and destroy-all take them + // all. Never a Lima home the user's own VMs live in. + if SameDir(e.opts.LimaHome, UserLimaHome()) { + return nil, fmt.Errorf("%s's Lima home %s is also your own Lima's home: give it a folder of its own", e.app, e.opts.LimaHome) + } + l, err := vm.NewLima(e.opts.LimaHome, e.state.VMs(), string(e.state)) + if err != nil { + return nil, err + } + return l.WithAssets(e.opts.Assets), nil +} + +// Change is the backend for a call that changes VMs. The first use also moves +// the VMs of an older version (BeforeChange, then the move of 0.3.0's to +// their own folders), which stops running ones. +func (e *Engine) Change(ctx context.Context) (vm.Backend, error) { + e.changeOne.Do(func() { + if e.opts.BeforeChange != nil { + e.opts.BeforeChange(ctx) + } + e.migrateLayout(ctx) + }) + return e.Backend() +} + +// get is the VM name, nil when there is none. +func (e *Engine) get(ctx context.Context, name string) (*vm.Instance, error) { + b, err := e.Backend() + if err != nil { + return nil, err + } + inst, err := b.Get(ctx, name) + if errors.Is(err, vm.ErrNotFound) { + return nil, nil + } + return inst, err +} + +// Get is the VM name, or an error that is ErrNotFound. +func (e *Engine) Get(ctx context.Context, name string) (*vm.Instance, error) { + b, err := e.Backend() + if err != nil { + return nil, err + } + return b.Get(ctx, name) +} + +// List is every VM, in name order. +func (e *Engine) List(ctx context.Context) ([]*vm.Instance, error) { + b, err := e.Backend() + if err != nil { + return nil, err + } + return b.List(ctx) +} + +// ListForChange is List for a call that goes on to change VMs. +func (e *Engine) ListForChange(ctx context.Context) ([]*vm.Instance, error) { + b, err := e.Change(ctx) + if err != nil { + return nil, err + } + return b.List(ctx) +} + +// Stop stops the VM name, and reports whether it is known to be stopped. +func (e *Engine) Stop(ctx context.Context, name string) bool { + b, err := e.Change(ctx) + if err != nil { + return false + } + _ = b.Stop(ctx, name) + inst, err := b.Get(ctx, name) + return err == nil && inst.Status == vm.Stopped +} + +// Delete stops and deletes the VM name, then forgets what was known of it. +// False, with an Error event, when it is still there (or cannot be asked): +// --reset would otherwise go on with the old VM and its shares. +func (e *Engine) Delete(ctx context.Context, name string) bool { + b, err := e.Change(ctx) + if err == nil { + _ = b.Delete(ctx, name) + _, err = b.Get(ctx, name) + } + if !errors.Is(err, vm.ErrNotFound) { + e.warnf("Error: could not delete VM '%s'. See '%s list', then retry.", name, e.app) + return false + } + // Lima's folder went with the VM; the rest of its folder goes now, and + // with it all that is known of the VM: nothing outlives it. + if err := e.state.Forget(name); err != nil { + e.warnf("Warning: %v", err) + } + return true +} + +// lockVM takes the lock of name, which serialises what changes that VM +// between runs: two runs in one folder would otherwise clean up, clone and +// configure the same VM at once. It waits, saying so, until the holder is +// done or ctx ends. The lock is a file in the state folder's locks/, kept +// for the next run: a crashed holder releases it with its process. +func (e *Engine) lockVM(ctx context.Context, name string) (unlock func(), err error) { + dir := e.state.Path("locks") + if err := os.MkdirAll(dir, 0o755); err != nil { + return nil, err + } + f, err := os.OpenFile(filepath.Join(dir, name), os.O_CREATE|os.O_RDWR, 0o644) + if err != nil { + return nil, err + } + told := false + for { + ok, err := tryLock(f) + if err != nil { + f.Close() + return nil, err + } + if ok { + // Closing the file releases the lock. + return func() { f.Close() }, nil + } + if !told { + e.warnf("Waiting for another %s that is working on '%s'...", e.app, name) + told = true + } + select { + case <-ctx.Done(): + f.Close() + return nil, ctx.Err() + case <-time.After(200 * time.Millisecond): + } + } +} + +// Refs are the locations no share may reach (see mounts.Refs). +func (e *Engine) Refs() []mounts.Ref { + self := "" + if exe, err := os.Executable(); err == nil { + if p, err := filepath.EvalSymlinks(exe); err == nil { + self = paths.Host(filepath.Dir(p)) + } + } + limaHomes := append([]string{e.opts.LimaHome}, e.opts.NeverShare...) + return mounts.Refs(paths.Home(), self, string(e.state), limaHomes...) +} + +// Entries are the usable entries of the volumes file for the project dir, +// their warnings on warn. +func (e *Engine) Entries(dir string, warn io.Writer) []mounts.Entry { + if e.opts.VolumesFile == "" { + return nil + } + return mounts.Entries(e.opts.VolumesFile, dir, paths.Home(), e.Refs(), warn) +} + +// Scan is the git scan of the project dir's writable shares (see +// gitguard.Scan), the volumes read quietly. +func (e *Engine) Scan(ctx context.Context, dir string) *gitguard.Result { + shares := gitguard.Shares(dir, mounts.RWDirs(e.Entries(dir, io.Discard))) + return gitguard.Scan(ctx, shares, paths.Home()) +} diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go new file mode 100644 index 00000000..3011849d --- /dev/null +++ b/internal/engine/engine_test.go @@ -0,0 +1,510 @@ +package engine + +import ( + "context" + "errors" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "testing" + + "github.com/sylvinus/agent-vm/internal/host" + "github.com/sylvinus/agent-vm/internal/paths" + "github.com/sylvinus/agent-vm/internal/policy" + "github.com/sylvinus/agent-vm/internal/state" + "github.com/sylvinus/agent-vm/internal/vm" + "github.com/sylvinus/agent-vm/internal/vm/vmtest" + "github.com/sylvinus/agent-vm/provision" +) + +// testAsker answers from a script and records the questions. +type testAsker struct { + can bool + answers []bool + asked []Question +} + +func (a *testAsker) CanAsk() bool { return a.can } + +func (a *testAsker) Ask(_ context.Context, q Question) bool { + a.asked = append(a.asked, q) + if len(a.answers) == 0 { + return q.Default + } + ans := a.answers[0] + a.answers = a.answers[1:] + return ans +} + +type testEngine struct { + *Engine + fake *vmtest.Fake + asker *testAsker + mu sync.Mutex + events []Event + proj string +} + +func (te *testEngine) text() string { + te.mu.Lock() + defer te.mu.Unlock() + var b strings.Builder + for _, ev := range te.events { + b.WriteString(ev.Text + "\n") + } + return b.String() +} + +// newTest is an engine over a fake backend, in a state folder and with a +// project folder of its own, home and git config out of the way. +func newTest(t *testing.T, mod func(*Options)) *testEngine { + t.Helper() + if _, err := exec.LookPath("git"); err != nil { + t.Skip("no git") + } + home, _ := filepath.EvalSymlinks(t.TempDir()) + home = paths.Host(home) + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + t.Setenv("GIT_CONFIG_NOSYSTEM", "1") + os.Unsetenv("GIT_CONFIG_GLOBAL") + os.Unsetenv("XDG_CONFIG_HOME") + t.Setenv("APPDATA", "") + t.Setenv("LOCALAPPDATA", "") + os.WriteFile(filepath.Join(home, ".gitconfig"), []byte("[safe]\n\tbareRepository = explicit\n"), 0o644) + proj := filepath.Join(home, "proj") + os.MkdirAll(proj, 0o755) + t.Chdir(proj) + t.Setenv("PWD", proj) + + cpus, mem := host.CPUs, host.MemGiB + t.Cleanup(func() { host.CPUs, host.MemGiB = cpus, mem }) + host.CPUs = func() int { return 64 } + host.MemGiB = func() int { return 256 } + te := &testEngine{fake: vmtest.New(), asker: &testAsker{}, proj: proj} + // The guest answers the probe as a VM with its shares would. + te.fake.ShellFunc = func(name string, o vm.ShellOpts) int { + if len(o.Args) > 2 && o.Args[0] == "sh" && o.Args[2] == ProbeScript { + io.Copy(io.Discard, o.Stdin) + io.WriteString(o.Stdout, "env-ok\n") + if o.Args[7] != "" { + io.WriteString(o.Stdout, "share-ok\n") + } + if v := te.fake.VMs[name]; v == nil || len(v.Mounts) > 0 && !v.Mounts[0].Writable { + return 1 + } + } + return 0 + } + opts := Options{ + StateDir: filepath.Join(home, ".mytool"), + AppName: "mytool", + Asker: te.asker, + Backend: te.fake, + OnEvent: func(ev Event) { + te.mu.Lock() + te.events = append(te.events, ev) + te.mu.Unlock() + }, + } + if mod != nil { + mod(&opts) + } + e, err := Open(opts) + if err != nil { + t.Fatal(err) + } + te.Engine = e + return te +} + +// ready makes the base template exist and be ready. +func (te *testEngine) ready(template string) { + te.fake.VMs[template] = &vm.Instance{Name: template, Status: vm.Stopped, CPUs: 1, Memory: 3 << 30, Disk: 10 << 30} + te.state.UpdateMeta(template, func(m *state.Metadata) { + m.Base = &state.Base{Name: template, Built: 1759000000, BuiltBy: "0.4.0"} + }) +} + +func TestOpenOptions(t *testing.T) { + if _, err := Open(Options{}); err == nil || !strings.Contains(err.Error(), "StateDir") { + t.Errorf("no state dir: %v", err) + } + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + e, err := Open(Options{StateDir: "~/.mytool", Backend: vmtest.New()}) + if err != nil { + t.Fatal(err) + } + if got, want := string(e.State()), paths.Host(filepath.Join(home, ".mytool")); got != want { + t.Errorf("state %q, want %q", got, want) + } + if e.AppName() != "agent-vm" || e.LimaHome() != e.State().LimaHome() { + t.Errorf("defaults: %q %q", e.AppName(), e.LimaHome()) + } +} + +// The first start of a project: the base is cloned, its shares set, policies +// written for the host agent, and the program told what happens. +func TestEnsureNewVM(t *testing.T) { + te := newTest(t, nil) + te.ready("base") + v, err := te.ProjectVM("") + if err != nil { + t.Fatal(err) + } + if err := v.Ensure(context.Background(), Spec{}); err != nil { + t.Fatalf("%v\n%s", err, te.text()) + } + if !v.Created() { + t.Error("not created") + } + got := te.fake.VMs[v.Name] + if got == nil || got.Status != vm.Running || got.MountType != vm.ReverseSSHFS || len(got.Mounts) != 1 || !got.Mounts[0].Writable { + t.Fatalf("VM: %+v", got) + } + if !strings.HasPrefix(te.fake.CallLog(), "clone base "+v.Name+"\nedit "+v.Name+"\nedit "+v.Name+"\nstart "+v.Name) { + t.Errorf("calls:\n%s", te.fake.CallLog()) + } + for _, want := range []string{"Creating VM '" + v.Name + "'...", "Starting VM '" + v.Name + "'..."} { + if !strings.Contains(te.text(), want) { + t.Errorf("no %q in:\n%s", want, te.text()) + } + } + pf, err := policy.Read(te.state.Policy(v.Name)) + if err != nil || pf.App != "mytool" || pf.Network == nil { + t.Errorf("policy: %+v %v", pf, err) + } + if b := te.state.Known(v.Name).Base; b == nil || b.Built != 1759000000 { + t.Errorf("base recorded: %+v", b) + } + // Again: nothing to change. + te.fake.Calls = nil + if err := v.Ensure(context.Background(), Spec{}); err != nil { + t.Fatal(err) + } + if log := te.fake.CallLog(); strings.Contains(log, "edit") || strings.Contains(log, "start") { + t.Errorf("second Ensure changed the VM:\n%s", log) + } +} + +// What a start stops on is an error that errors.Is matches, told as an +// event first, naming the program as the engine was opened. +func TestEnsureErrors(t *testing.T) { + te := newTest(t, nil) + v, _ := te.ProjectVM("") + err := v.Ensure(context.Background(), Spec{}) + if !errors.Is(err, ErrBaseMissing) || !strings.Contains(te.text(), "Run 'mytool setup' first.") || strings.Contains(te.text(), "agent-vm") { + t.Errorf("no base: %v\n%s", err, te.text()) + } + te.fake.VMs["base"] = &vm.Instance{Name: "base", Status: vm.Stopped} + if err := v.Ensure(context.Background(), Spec{}); !errors.Is(err, ErrBaseIncomplete) { + t.Errorf("base without its record: %v", err) + } + te.ready("base") + + // A project the VM must not get. + home, _ := os.UserHomeDir() + t.Chdir(home) + t.Setenv("PWD", home) + hv, _ := te.ProjectVM("") + err = hv.Ensure(context.Background(), Spec{}) + var refused *RefusedError + if !errors.Is(err, ErrRefused) || !errors.As(err, &refused) || refused.Dir != paths.Host(home) { + t.Errorf("home: %v", err) + } + + // An unreadable policy stops the start before Lima does. + te2 := newTest(t, func(o *Options) { + o.Policy = func() (*policy.File, error) { return nil, errors.New("network, line 2: nope") } + }) + te2.ready("base") + v2, _ := te2.ProjectVM("") + if err := v2.Ensure(context.Background(), Spec{}); err == nil || !strings.Contains(te2.text(), "network, line 2: nope") || strings.Contains(te2.fake.CallLog(), "start ") { + t.Errorf("bad policy: %v\n%s\n%s", err, te2.text(), te2.fake.CallLog()) + } +} + +// Questions go to the Asker; without one, or one that cannot ask, the answer +// is no. +func TestAskerAndNoAsker(t *testing.T) { + te := newTest(t, nil) + te.ready("base") + v, _ := te.ProjectVM("") + ctx := context.Background() + if err := v.Ensure(ctx, Spec{}); err != nil { + t.Fatal(err) + } + // Running, and a resource change asked: a restart is put to the Asker. + te.fake.VMs[v.Name].Status = vm.Running + te.asker.can = true + te.asker.answers = []bool{false} + if err := v.Ensure(ctx, Spec{CPUs: 2}); err != nil { + t.Fatalf("%v\n%s", err, te.text()) + } + if len(te.asker.asked) != 1 || te.asker.asked[0].Kind != QRestart || te.asker.asked[0].VM != v.Name || !strings.Contains(te.text(), "Not applied: the VM keeps its current settings.") { + t.Errorf("asked %+v\n%s", te.asker.asked, te.text()) + } + // No Asker: the same, without a question. + te2 := newTest(t, func(o *Options) { o.Asker = nil }) + te2.ready("base") + v2, _ := te2.ProjectVM("") + if err := v2.Ensure(ctx, Spec{}); err != nil { + t.Fatal(err) + } + te2.fake.VMs[v2.Name].Status = vm.Running + if err := v2.Ensure(ctx, Spec{CPUs: 2}); err != nil || !strings.Contains(te2.text(), "Not applied") { + t.Errorf("no asker: %v\n%s", err, te2.text()) + } + // --readonly asked of a running VM that is writable: refused, not silent. + te2.fake.VMs[v2.Name].Status = vm.Running + if err := v2.Ensure(ctx, Spec{ReadOnly: true}); !errors.Is(err, ErrNotApplied) { + t.Errorf("readonly on a running VM, no asker: %v", err) + } +} + +// The unsafe flags: what turns the security questions off is told. +func TestUnsafeNoPrompts(t *testing.T) { + te := newTest(t, nil) + te.ready("base") + // A hooks folder that cannot be protected is a question; with the prompts + // off, it is answered yes and said. + exec.Command("git", "-C", te.proj, "init", "-q").Run() + exec.Command("git", "-C", te.proj, "config", "core.hooksPath", ".").Run() + v, _ := te.ProjectVM("") + if err := v.Ensure(context.Background(), Spec{}); !errors.Is(err, ErrAborted) { + t.Fatalf("without an asker: %v\n%s", err, te.text()) + } + if len(te.asker.asked) != 0 { + t.Errorf("asked although it cannot: %+v", te.asker.asked) + } + if err := v.Ensure(context.Background(), Spec{UnsafeNoPrompts: "--the-flag"}); err != nil { + t.Fatalf("%v\n%s", err, te.text()) + } + if !strings.Contains(te.text(), "Continuing: --the-flag.") { + t.Errorf("not said:\n%s", te.text()) + } +} + +// Exec runs through a login zsh, naming the program in what it says. +func TestExec(t *testing.T) { + te := newTest(t, nil) + te.ready("base") + v, _ := te.ProjectVM("") + te.fake.VMs[v.Name] = &vm.Instance{Name: v.Name, Status: vm.Running} + var got vm.ShellOpts + te.fake.ShellFunc = func(_ string, o vm.ShellOpts) int { got = o; return 7 } + code, err := v.Exec(context.Background(), Exec{Args: []string{"mytool-agent", "--x"}, TTY: true}) + if err != nil || code != 7 { + t.Fatalf("%d %v", code, err) + } + if got.Workdir != paths.Guest(v.Dir) || !got.Interactive || len(got.Args) < 6 || got.Args[0] != "zsh" || got.Args[len(got.Args)-2] != "mytool-agent" || + !strings.Contains(got.Args[3], "mytool: $1 is not installed in this VM.") { + t.Errorf("shell opts: %+v", got) + } + code, err = v.Shell(context.Background(), Exec{Args: []string{"zsh", "-l"}}) + if err != nil || code != 7 || strings.Join(got.Args, " ") != "zsh -l" { + t.Errorf("shell: %d %v %q", code, err, got.Args) + } +} + +// Setup builds the template under another name, then swaps it in. +func TestSetup(t *testing.T) { + te := newTest(t, nil) + var scripts []string + te.fake.ShellFunc = func(name string, o vm.ShellOpts) int { + b, _ := io.ReadAll(o.Stdin) + scripts = append(scripts, name+": "+strings.Join(o.Args, " ")+": "+string(b)) + if _, err := policy.Read(te.state.Policy(name)); err != nil { + t.Errorf("the build boots without a policy: %v", err) + } + return 0 + } + tmpl := provision.Template{ + Name: "base-mytool", MemoryGiB: 2, DiskGiB: 8, + Scripts: []provision.Script{ + {Name: "install", Message: "Installing...", Shell: []string{"bash", "-l"}, Content: "echo hi\n", Env: []string{"A=1", "B=it's"}}, + {Name: "more", Content: "echo more\n"}, + }, + Env: []string{"T=t"}, + } + var steps []string + out := stepFunc(func(name string, fn func(o, e io.Writer) error) error { + steps = append(steps, name) + return fn(io.Discard, io.Discard) + }) + if err := te.Setup(context.Background(), tmpl, out); err != nil { + t.Fatalf("%v\n%s", err, te.text()) + } + if strings.Join(steps, ",") != "create,start,script:install,script:more" { + t.Errorf("steps %v", steps) + } + want := []string{ + "base-mytool-building: bash -l: export T=t\nexport A=1\nexport B='it'\\''s'\necho hi\n", + "base-mytool-building: bash -l: export T=t\necho more\n", + } + if len(scripts) != 2 || scripts[0] != want[0] || scripts[1] != want[1] { + t.Errorf("scripts %q", scripts) + } + if !strings.HasPrefix(te.fake.CallLog(), "delete base-mytool-building\ncreate base-mytool-building template:debian-13\nstart base-mytool-building\nshell") || + !strings.Contains(te.fake.CallLog(), "stop base-mytool-building\ndelete base-mytool\nclone base-mytool-building base-mytool\ndelete base-mytool-building") { + t.Errorf("calls:\n%s", te.fake.CallLog()) + } + if te.fake.VMs["base-mytool"] == nil || te.fake.VMs["base-mytool"].Memory != 2<<30 || !te.state.BaseReady("base-mytool") { + t.Errorf("template: %+v", te.fake.VMs["base-mytool"]) + } + // A VM is cloned from the template it names. + v, _ := te.ProjectVM("") + if err := v.Ensure(context.Background(), Spec{Base: "base-mytool"}); err != nil { + t.Fatalf("%v\n%s", err, te.text()) + } + if !strings.Contains(te.fake.CallLog(), "clone base-mytool "+v.Name) { + t.Errorf("cloned from another:\n%s", te.fake.CallLog()) + } + + // A script that fails stops the build and leaves the template. + te.fake.ShellFunc = func(string, vm.ShellOpts) int { return 3 } + err := te.Setup(context.Background(), tmpl, out) + var se *SetupError + if !errors.As(err, &se) || se.Step != "script" || se.Script != "install" || se.Told { + t.Errorf("failing script: %v", err) + } + if !te.state.BaseReady("base-mytool") || te.fake.VMs["base-mytool"] == nil { + t.Error("a failed build took the template") + } +} + +type stepFunc func(name string, fn func(stdout, stderr io.Writer) error) error + +func (f stepFunc) Step(name string, fn func(stdout, stderr io.Writer) error) error { + return f(name, fn) +} + +// A scratch VM has a name of its own and a record of its run; one whose run +// is gone is found. +func TestScratch(t *testing.T) { + te := newTest(t, nil) + te.ready("base") + v, err := te.NewScratchVM("") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(v.Name, "-scratch-") || te.state.Known(v.Name).ScratchPID != os.Getpid() { + t.Errorf("scratch VM %q %+v", v.Name, te.state.Known(v.Name)) + } + if err := v.Ensure(context.Background(), Spec{}); err != nil { + t.Fatalf("%v\n%s", err, te.text()) + } + if got := te.fake.VMs[v.Name]; got == nil || len(got.Mounts) != 0 { + t.Errorf("a scratch VM shares nothing: %+v", got) + } + dead, unknown := te.ScratchLeftovers() + if len(dead) != 0 || len(unknown) != 0 { + t.Errorf("this run's VM is a leftover: %v %v", dead, unknown) + } + te.state.UpdateMeta(v.Name, func(m *state.Metadata) { m.ScratchPID = 999999999 }) + if dead, _ = te.ScratchLeftovers(); len(dead) != 1 || dead[0] != v.Name { + t.Errorf("dead run: %v", dead) + } + if !v.Remove(context.Background()) || te.fake.VMs[v.Name] != nil { + t.Error("not removed") + } + if _, err := os.Stat(te.state.VM(v.Name)); !os.IsNotExist(err) { + t.Error("its folder is left") + } +} + +// The resources asked against the VM's: in bytes, the disk one way (it +// only grows), a VM that cannot be read counts as a change. +func TestResourcesDiffer(t *testing.T) { + inst := &vm.Instance{CPUs: 2, Memory: 4 << 30, Disk: 20 << 30} + for _, c := range []struct { + o Spec + want bool + }{ + {Spec{}, false}, + {Spec{CPUs: 2, MemoryGiB: 4, DiskGiB: 20}, false}, + {Spec{CPUs: 3}, true}, + {Spec{MemoryGiB: 5}, true}, + {Spec{DiskGiB: 10}, false}, + {Spec{DiskGiB: 30}, true}, + } { + s := &session{spec: c.o} + if got := s.resourcesDiffer(inst); got != c.want { + t.Errorf("%+v: %v", c.o, got) + } + } + if !(&session{spec: Spec{CPUs: 2}}).resourcesDiffer(nil) { + t.Error("an unknown VM is no change") + } + // Half a GiB more than asked: a change. + if !(&session{spec: Spec{MemoryGiB: 4}}).resourcesDiffer(&vm.Instance{Memory: 4<<30 + 1<<29}) { + t.Error("compared in whole GiB") + } +} + +// beforeBoot holds every check that applies: --readonly with .git +// protected refuses a protected share left writable. +func TestBeforeBootAll(t *testing.T) { + te := newTest(t, nil) + name := "p-12345678" + off := false + te.fake.VMs[name] = &vm.Instance{Name: name, Status: vm.Stopped, MountType: vm.ReverseSSHFS, Mounts: []vm.Mount{{ + Location: te.proj, Writable: true, SSHFS: &vm.SSHFS{SFTPDriver: "builtin", Cache: &off, ReadonlyNames: []string{".git", ".hg"}}}}} + s := &session{Engine: te.Engine, ctx: context.Background(), name: name, spec: Spec{ReadOnly: true}, protect: true, names: []string{".git", ".hg"}} + if err := s.beforeBoot(); err == nil || !strings.Contains(te.text(), "a writable share of "+te.proj) { + t.Errorf("--readonly, protected, writable: %v\n%s", err, te.text()) + } + s.spec.ReadOnly = false + if err := s.beforeBoot(); err != nil { + t.Errorf("writable asked: %v\n%s", err, te.text()) + } +} + +// The verdicts on a VM's shares: confined first, then the names or the +// read-only mode. +func TestShareVerdicts(t *testing.T) { + b := func(names ...string) *vm.SSHFS { return &vm.SSHFS{SFTPDriver: "builtin", ReadonlyNames: names} } + inst := func(mt string, ms ...vm.Mount) *vm.Instance { return &vm.Instance{MountType: mt, Mounts: ms} } + rs := vm.ReverseSSHFS + for _, c := range []struct { + inst *vm.Instance + names []string + unprot, notRO string + }{ + {inst(rs, vm.Mount{Location: "/p", SSHFS: b(".git", ".hg")}), []string{".git"}, "", ""}, + {inst(rs, vm.Mount{Location: "/p", SSHFS: b(".git", ".hg", "tools")}), []string{".git", ".hg"}, "", ""}, + {inst(rs, vm.Mount{Location: "/p", SSHFS: b(".git")}), []string{".git", "tools"}, "lacks the read-only names", ""}, + {inst(rs, vm.Mount{Location: "/p", SSHFS: b()}), nil, "lacks the read-only names", ""}, + {inst(rs, vm.Mount{Location: "/p", Writable: true, SSHFS: b(".git", ".hg", ".sl", ".jj")}), nil, "", "a writable share of /p"}, + {inst(rs, vm.Mount{Location: "/p", SSHFS: b("tools")}), nil, "lacks the read-only names", ""}, + {inst(rs, vm.Mount{Location: "/p", SSHFS: &vm.SSHFS{ReadonlyNames: []string{".git"}}}), nil, "not served by the builtin SFTP server", "not served by the builtin SFTP server"}, + {inst("9p", vm.Mount{Location: "/p", SSHFS: b(".git")}), nil, "the mount type", "the mount type"}, + } { + if got := unprotected(c.inst, c.names); c.unprot == "" && got != "" || !strings.Contains(got, c.unprot) { + t.Errorf("unprotected %+v %v: %q", c.inst.Mounts, c.names, got) + } + if got := notReadOnly(c.inst); c.notRO == "" && got != "" || !strings.Contains(got, c.notRO) { + t.Errorf("notReadOnly %+v: %q", c.inst.Mounts, got) + } + } +} + +func TestEventWriter(t *testing.T) { + te := newTest(t, nil) + w := te.writer(Warning, "x") + io.WriteString(w, "one\ntw") + io.WriteString(w, "o\n\nthree\n") + if got := te.text(); got != "one\ntwo\n\nthree\n" { + t.Errorf("events %q", got) + } + for _, ev := range te.events { + if ev.Level != Warning || ev.VM != "x" { + t.Errorf("event %+v", ev) + } + } +} diff --git a/internal/engine/errors.go b/internal/engine/errors.go new file mode 100644 index 00000000..3f8b4f89 --- /dev/null +++ b/internal/engine/errors.go @@ -0,0 +1,57 @@ +package engine + +import ( + "errors" + + "github.com/sylvinus/agent-vm/internal/vm" +) + +// What a call can fail on, for errors.Is. Each comes with the Event that says +// it in full. +var ( + // ErrBaseMissing: the base template does not exist; set it up first. + ErrBaseMissing = errors.New("base VM not found") + // ErrBaseIncomplete: the base exists but its setup did not complete. + ErrBaseIncomplete = errors.New("base VM setup did not complete") + // ErrBaseOld: the VM, or the base, comes from agent-vm 0.1.0, without + // the sshfs its shares need. + ErrBaseOld = errors.New("base built by agent-vm 0.1.0, without sshfs") + // ErrAborted: the user said no to a security question, or no one could + // be asked and the safe answer is no. + ErrAborted = errors.New("aborted") + // ErrNotApplied: a restart or a change the VM needs was not agreed. + ErrNotApplied = errors.New("a change was not applied") + // ErrRefused: a project or a share the engine does not give to a VM. + // The error is a *RefusedError. + ErrRefused = errors.New("refused") + // ErrFailed: the VM could not be made, configured, started or reached. + ErrFailed = errors.New("failed") + + // ErrNameTooLong, ErrLimaOverrides and ErrUnconfined are the backend's. + ErrNameTooLong = vm.ErrNameTooLong + ErrLimaOverrides = vm.ErrLimaOverrides + ErrUnconfined = vm.ErrUnconfined + // ErrNotFound: no such VM. + ErrNotFound = vm.ErrNotFound +) + +// Failure is the error of a call that stopped: its kind (one of the Err +// values above, for errors.Is) and the text the Error event said. +type Failure struct { + Kind error + Text string +} + +func (f *Failure) Error() string { return f.Text } +func (f *Failure) Unwrap() error { return f.Kind } + +// RefusedError is returned when the engine does not give a folder to a VM: +// the project is the user's home, holds credentials, or agent-vm itself. +type RefusedError struct { + // Dir is what was refused, Why the reason ("is your home directory"). + Dir, Why string + Text string +} + +func (r *RefusedError) Error() string { return r.Text } +func (r *RefusedError) Unwrap() error { return ErrRefused } diff --git a/internal/engine/events.go b/internal/engine/events.go new file mode 100644 index 00000000..90d7d354 --- /dev/null +++ b/internal/engine/events.go @@ -0,0 +1,113 @@ +package engine + +import ( + "context" + "io" + "strings" +) + +// Level of an Event. +type Level int + +const ( + // Info is progress: what is being done ("Starting VM 'x'..."). + Info Level = iota + // Warning is something to know, that does not stop what is being done, + // or what the user must know before a question. + Warning + // Error is why a start cannot go on. The call returns an error too. + Error +) + +// Event is something the engine tells the program that drives it: progress, +// warnings, errors. The text is for people, in English, and complete (a +// "Warning: " or "Error: " prefix included, no trailing newline). Nothing is +// ever written to a terminal by the engine itself. +type Event struct { + Level Level + // VM is the VM the event is about, "" for none. + VM string + // Title, when set, makes the event a boxed notice: Text is its body. + Title string + Text string +} + +// QuestionKind says what an engine asks. +type QuestionKind int + +const ( + // QHooks: git runs hooks from a folder of a share whose name is not + // protected by default; make every folder of that name in the project + // read-only for the VM? Default yes. + QHooks QuestionKind = iota + // QUnsafe: a risk to the host was warned of, go on anyway? Default no. + QUnsafe + // QBareRepo: set git's safe.bareRepository to explicit in the user's + // global git config? Default yes. + QBareRepo + // QRestart: the running VM must be stopped to apply a change; sessions + // using it are cut. The Prompt says which. + QRestart + // QStopToMove: the running VMs of an older layout must be stopped to be + // moved. Default yes. + QStopToMove +) + +// Question is something the engine cannot decide: what the user is asked. +type Question struct { + Kind QuestionKind + VM string + // Title and Detail, when set, are shown before the question (a boxed + // notice). + Title, Detail string + // Prompt is the question itself, ending with "?". + Prompt string + // Default is the answer when the user gives none. + Default bool +} + +// Asker answers the engine's questions. An engine with no Asker, or whose +// Asker cannot ask, takes every answer as no: as for a program with no +// terminal. +type Asker interface { + // CanAsk reports whether a question can be asked and seen now. Some + // questions are not asked, and a safe answer taken, when it cannot. + CanAsk() bool + // Ask asks, and returns the answer. + Ask(ctx context.Context, q Question) bool +} + +// NoAsker answers no to everything and cannot be asked. +type NoAsker struct{} + +func (NoAsker) CanAsk() bool { return false } +func (NoAsker) Ask(context.Context, Question) bool { return false } + +// eventWriter is an io.Writer that turns each line written into an Event of +// its level, for the helpers that report to a writer. +type eventWriter struct { + e *Engine + level Level + vm string + rest string +} + +func (w *eventWriter) Write(p []byte) (int, error) { + w.rest += string(p) + for { + i := strings.IndexByte(w.rest, '\n') + if i < 0 { + break + } + w.e.emit(Event{Level: w.level, VM: w.vm, Text: w.rest[:i]}) + w.rest = w.rest[i+1:] + } + return len(p), nil +} + +var _ io.Writer = (*eventWriter)(nil) + +// writer is an io.Writer for the events of level. +func (e *Engine) writer(level Level, vm string) io.Writer { + return &eventWriter{e: e, level: level, vm: vm} +} diff --git a/internal/engine/layout.go b/internal/engine/layout.go new file mode 100644 index 00000000..32a31691 --- /dev/null +++ b/internal/engine/layout.go @@ -0,0 +1,60 @@ +package engine + +import ( + "context" + "strings" + + "github.com/sylvinus/agent-vm/internal/state" + "github.com/sylvinus/agent-vm/internal/vm" + "github.com/sylvinus/agent-vm/internal/vmname" +) + +// migrationLock is the lock of the move of 0.3.0's VMs: a file in locks/, +// as the VMs' are. +const migrationLock = "migrate" + +// LayoutPending are the VMs of 0.3.0 that have not been moved yet to a folder +// each: they are not listed until they are. +func (e *Engine) LayoutPending() []string { + return e.state.LegacyVMs(e.opts.LimaHome) +} + +// migrateLayout moves 0.3.0's VMs to a folder each, vms// (see +// state.MigrateLayout), once, by the first call that changes VMs. A VM that +// runs cannot move: if the Asker agrees, it is stopped; else it stays for the +// next call. +func (e *Engine) migrateLayout(ctx context.Context) { + home := e.opts.LimaHome + if !e.state.PendingLayout(home) { + return + } + unlock, err := e.lockVM(ctx, migrationLock) + if err != nil { + e.warnf("Error: could not lock the move of the VMs: %v", err) + return + } + defer unlock() + // Another run did it while this one waited. + if !e.state.PendingLayout(home) { + return + } + if vms := e.state.LegacyVMs(home); len(vms) > 0 { + e.warnf("%s now keeps each VM in a folder of its own, %s. Moving %s...", e.app, e.state.VMs(), strings.Join(vms, ", ")) + } + ops := state.LayoutOps{ + Home: home, + Template: vmname.Template, + Log: e.writer(Warning, ""), + Running: func(name string) bool { return vm.LegacyRunning(ctx, home, name) }, + Stop: func(name string) error { return vm.StopLegacy(ctx, home, name) }, + } + if e.opts.Asker.CanAsk() { + ops.Ask = func(running []string) bool { + e.warnf("Running, they cannot move: %s.", strings.Join(running, ", ")) + return e.opts.Asker.Ask(ctx, Question{Kind: QStopToMove, Prompt: "Stop them now to move them? Sessions using them are cut.", Default: true}) + } + } + if err := e.state.MigrateLayout(ops); err != nil { + e.warnf("Error: %v", err) + } +} diff --git a/internal/cli/lock_unix.go b/internal/engine/lock_unix.go similarity index 96% rename from internal/cli/lock_unix.go rename to internal/engine/lock_unix.go index 4a6c4c0c..0684f3e6 100644 --- a/internal/cli/lock_unix.go +++ b/internal/engine/lock_unix.go @@ -1,6 +1,6 @@ //go:build !windows -package cli +package engine import ( "errors" diff --git a/internal/cli/lock_windows.go b/internal/engine/lock_windows.go similarity index 97% rename from internal/cli/lock_windows.go rename to internal/engine/lock_windows.go index c4df09bb..d4f32d9e 100644 --- a/internal/cli/lock_windows.go +++ b/internal/engine/lock_windows.go @@ -1,4 +1,4 @@ -package cli +package engine import ( "errors" diff --git a/internal/cli/probe.go b/internal/engine/probe.go similarity index 75% rename from internal/cli/probe.go rename to internal/engine/probe.go index 96fa4bdd..d61cbc5f 100644 --- a/internal/cli/probe.go +++ b/internal/engine/probe.go @@ -1,4 +1,4 @@ -package cli +package engine import ( "fmt" @@ -19,15 +19,31 @@ type probeResult struct { envOK bool } -// The guest's side of the probe, its arguments the project's path, the -// project's env file and runtime script (by their paths in the VM), and the -// token the host put in the project. -const probeScript = ` +// ProbeScript is the guest's side of the probe, its arguments the project's +// path, the project's env file and runtime script (by their paths in the VM), +// and the token the host put in the project. +const ProbeScript = ` (umask 077 && rm -f "$HOME/.agent-vm.env" && { cat; [ -z "$2" ] || [ ! -f "$2" ] || awk "{ sub(/\r\$/, \"\"); print }" "$2"; } > "$HOME/.agent-vm.env") && echo env-ok [ -z "$3" ] || [ ! -f "$3" ] || echo runtime-found [ -z "$4" ] || [ ! -f "$1/$4" ] || echo share-ok p="$1/.agent-vm-write-probe.$$"; touch "$p" 2>/dev/null || exit 1; rm -f "$p"` +// projectEnvFile and projectRuntime are the project's own env file and +// runtime script, "" when the program gave no way to name them. +func (s *session) projectEnvFile() string { + if f := s.opts.ProjectEnvFile; f != nil { + return f(s.dir) + } + return "" +} + +func (s *session) projectRuntime() string { + if f := s.opts.ProjectRuntime; f != nil { + return f(s.dir) + } + return "" +} + // probe pushes the env and checks the project share, in one round trip: // - writes ~/.agent-vm.env in the VM (mode 600, sourced by its ~/.zshenv): // payload, then the project's env, read by the VM, CRs dropped; empty @@ -39,9 +55,9 @@ p="$1/.agent-vm-write-probe.$$"; touch "$p" 2>/dev/null || exit 1; rm -f "$p"` // where the VM writes on its own disk: the guest does not see the file // put in the project just before. Not for a scratch VM, which shares // nothing. -func (s *start) probe(payload, guestEnv, guestRuntime string) probeResult { +func (s *session) probe(payload, guestEnv, guestRuntime string) probeResult { token := "" - if !s.opts.Scratch { + if !s.noShares { t := fmt.Sprintf(".agent-vm-probe.%d.%d", os.Getpid(), rand.IntN(1e9)) // Never through something already there. if f, err := os.OpenFile(filepath.Join(s.dir, t), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644); err == nil { @@ -55,11 +71,11 @@ func (s *start) probe(payload, guestEnv, guestRuntime string) probeResult { in = payload + "\n" } var out strings.Builder - b, err := s.backend() + b, err := s.Change(s.ctx) code := 1 if err == nil { code, err = b.Shell(s.ctx, s.name, vm.ShellOpts{ - Args: []string{"sh", "-c", probeScript, "sh", paths.Guest(s.dir), guestEnv, guestRuntime, token}, + Args: []string{"sh", "-c", ProbeScript, "sh", paths.Guest(s.dir), guestEnv, guestRuntime, token}, Stdin: strings.NewReader(in), Stdout: &out, Stderr: io.Discard, }) } diff --git a/internal/cli/start.go b/internal/engine/session.go similarity index 50% rename from internal/cli/start.go rename to internal/engine/session.go index a2b9e264..bcd1b3d2 100644 --- a/internal/cli/start.go +++ b/internal/engine/session.go @@ -1,4 +1,4 @@ -package cli +package engine import ( "bytes" @@ -14,33 +14,86 @@ import ( "strings" "github.com/sylvinus/agent-vm/internal/env" - "github.com/sylvinus/agent-vm/internal/guard" "github.com/sylvinus/agent-vm/internal/host" "github.com/sylvinus/agent-vm/internal/mounts" - "github.com/sylvinus/agent-vm/internal/netguard" "github.com/sylvinus/agent-vm/internal/paths" + "github.com/sylvinus/agent-vm/internal/policy" "github.com/sylvinus/agent-vm/internal/runscript" "github.com/sylvinus/agent-vm/internal/state" "github.com/sylvinus/agent-vm/internal/vm" - "github.com/sylvinus/agent-vm/internal/vmname" ) -var errStop = errors.New("stop") +// session is one start of a VM: Ensure. +type session struct { + *Engine + ctx context.Context + spec Spec + base string // the template + name string // the VM + dir string // the project, as the shell spells it -// get is the VM name, nil when there is none. -func (s *start) get(name string) (*vm.Instance, error) { - b, err := s.backend() - if err != nil { - return nil, err - } - inst, err := b.Get(s.ctx, name) - if errors.Is(err, vm.ErrNotFound) { - return nil, nil + scratch bool // a scratch VM: a name of its own, no lock + named bool // a named VM: bound to a folder, or to none + noShares bool // shares nothing: asked, scratch, or bound to none + + created bool // this run made the VM, and it is still there + vmUp bool // running when the start began, and not reset + protect bool // every .git read-only + names []string // the read-only names, when protect + notes string // hooks notes, printed once the names apply + + built []vm.Mount // see shares + builtFiles []mounts.FileMount + builtKey string + + lastErr string // the text of the last Error event +} + +// warnf tells a warning, or an error when the text says so, which the call +// then returns (see fail). +func (s *session) warnf(format string, a ...any) { + text := fmt.Sprintf(format, a...) + lvl := Warning + if strings.HasPrefix(text, "Error:") { + lvl = Error + s.lastErr = text + } + s.emit(Event{Level: lvl, VM: s.name, Text: text}) +} + +func (s *session) infof(format string, a ...any) { + s.emit(Event{Level: Info, VM: s.name, Text: fmt.Sprintf(format, a...)}) +} + +// info tells text, if there is any, as it is: it may be several lines. +func (s *session) info(text string) { + if text = strings.TrimSuffix(text, "\n"); text != "" { + s.emit(Event{Level: Info, VM: s.name, Text: text}) } - return inst, err } -func (s *start) running() bool { +// fail is the error of a call that stopped after telling why (the last +// Error event). +func (s *session) fail(kind error) error { + return &Failure{Kind: kind, Text: strings.TrimPrefix(s.lastErr, "Error: ")} +} + +// ask puts q to the Asker. +func (s *session) ask(q Question) bool { + q.VM = s.name + return s.opts.Asker.Ask(s.ctx, q) +} + +func (s *session) canAsk() bool { return s.opts.Asker.CanAsk() } + +func (s *session) warnWriter() io.Writer { return s.writer(Warning, s.name) } + +// get is the VM name, nil when there is none. +func (s *session) get(name string) (*vm.Instance, error) { + return s.Engine.get(s.ctx, name) +} + +func (s *session) running() bool { inst, err := s.get(s.name) return err == nil && inst != nil && inst.Status == vm.Running } @@ -52,8 +105,8 @@ func (s *start) running() bool { // server still serves the shares alone. // // Built once per mode and names, so its warnings show once. -func (s *start) shares(writable bool) ([]vm.Mount, []mounts.FileMount) { - if s.opts.Scratch { +func (s *session) shares(writable bool) ([]vm.Mount, []mounts.FileMount) { + if s.noShares { return []vm.Mount{}, nil } n := s.names @@ -65,8 +118,8 @@ func (s *start) shares(writable bool) ([]vm.Mount, []mounts.FileMount) { return s.built, s.builtFiles } s.built, s.builtFiles = mounts.Build(mounts.Shares{ - VM: s.name, Dir: s.dir, Writable: writable, Names: n, - Entries: s.entries(s.dir, s.io.Stderr), StateDir: string(s.state), Warn: s.io.Stderr, + Dir: s.dir, Writable: writable, Names: n, Cache: s.opts.SSHFSCache, + Entries: s.Entries(s.dir, s.warnWriter()), FileMounts: s.state.FileMounts(s.name), Warn: s.warnWriter(), }) s.builtKey = key return s.built, s.builtFiles @@ -74,8 +127,8 @@ func (s *start) shares(writable bool) ([]vm.Mount, []mounts.FileMount) { // applyShares gives the stopped VM its shares, and records the single files // of the volumes for the bind once it runs. -func (s *start) applyShares(writable bool) error { - b, err := s.backend() +func (s *session) applyShares(writable bool) error { + b, err := s.Change(s.ctx) if err != nil { return err } @@ -83,45 +136,38 @@ func (s *start) applyShares(writable bool) error { mt := vm.ReverseSSHFS if err := b.Edit(s.ctx, s.name, vm.Settings{Mounts: &ms, MountType: &mt}); err != nil { s.warnf("Error: could not set the shares of VM '%s':\n%v", s.name, err) - return errStop + return s.fail(ErrFailed) } - cache := s.state.Marker(state.FileMountsOf, s.name) - if err := os.Remove(cache); err != nil && !os.IsNotExist(err) { - s.warnf("Error: could not remove %s:\n%v", cache, err) - return errStop + var lines []string + for _, f := range files { + lines = append(lines, f.Line()) } - if len(files) > 0 { - var lines []string - for _, f := range files { - lines = append(lines, f.Line()) - } - if err := s.state.Write(state.FileMountsOf+s.name, []byte(strings.Join(lines, "\n")+"\n")); err != nil { - s.warnf("Error: could not record the single files of ~/.agent-vm/volumes for VM '%s':\n%v", s.name, err) - return errStop - } + if err := s.state.UpdateMeta(s.name, func(m *state.Metadata) { m.FileMounts = lines }); err != nil { + s.warnf("Error: could not record the single files of ~/.agent-vm/volumes for VM '%s':\n%v", s.name, err) + return s.fail(ErrFailed) } return nil } // applyResources gives the stopped VM the memory, CPUs and SSH port asked // for. The disk apart, with a warning only: it can grow, not shrink. -func (s *start) applyResources() error { - b, err := s.backend() +func (s *session) applyResources() error { + b, err := s.Change(s.ctx) if err != nil { return err } - set := vm.Settings{CPUs: s.opts.CPUs, MemoryGiB: s.opts.Memory} - if s.opts.SSHPort >= 0 { - p := s.opts.SSHPort + set := vm.Settings{CPUs: s.spec.CPUs, MemoryGiB: s.spec.MemoryGiB} + if s.spec.SSHPort != nil { + p := *s.spec.SSHPort set.SSHLocalPort = &p } if err := b.Edit(s.ctx, s.name, set); err != nil { s.warnf("Error: could not set the memory, CPUs or SSH port of VM '%s':\n%v", s.name, err) - return errStop + return s.fail(ErrFailed) } - if s.opts.Disk > 0 { - if err := b.Edit(s.ctx, s.name, vm.Settings{DiskGiB: s.opts.Disk}); err != nil { - s.warnf("Warning: cannot set the disk of VM '%s' to %d GiB: it can grow, not shrink ('agent-vm setup --disk %d' for a smaller base).\n%v", s.name, s.opts.Disk, s.opts.Disk, err) + if s.spec.DiskGiB > 0 { + if err := b.Edit(s.ctx, s.name, vm.Settings{DiskGiB: s.spec.DiskGiB}); err != nil { + s.warnf("Warning: cannot set the disk of VM '%s' to %d GiB: it can grow, not shrink ('%s setup --disk %d' for a smaller base).\n%v", s.name, s.spec.DiskGiB, s.app, s.spec.DiskGiB, err) } } return nil @@ -130,16 +176,16 @@ func (s *start) applyResources() error { // resourcesDiffer: would the resources asked change anything? Compared in // bytes; the disk one way, since it can only grow. True when the VM cannot // be read: never "no change" from missing information. -func (s *start) resourcesDiffer(inst *vm.Instance) bool { - o := s.opts +func (s *session) resourcesDiffer(inst *vm.Instance) bool { + o := s.spec switch { case inst == nil: return true case o.CPUs > 0 && o.CPUs != inst.CPUs: return true - case o.Memory > 0 && int64(o.Memory)<<30 != inst.Memory: + case o.MemoryGiB > 0 && int64(o.MemoryGiB)<<30 != inst.Memory: return true - case o.Disk > 0 && int64(o.Disk)<<30 > inst.Disk: + case o.DiskGiB > 0 && int64(o.DiskGiB)<<30 > inst.Disk: return true } return false @@ -147,83 +193,102 @@ func (s *start) resourcesDiffer(inst *vm.Instance) bool { // stopToChange stops the VM to change it, then asks what its start did not, // a running VM having been asked nothing. -func (s *start) stopToChange() error { - if !s.stopVM(s.ctx, s.name) { +func (s *session) stopToChange() error { + if !s.Stop(s.ctx, s.name) { s.warnf("Error: could not stop VM '%s': nothing was changed.", s.name) - return errStop + return s.fail(ErrFailed) } if s.vmUp { s.vmUp = false if !s.checks() { s.warnf("VM '%s' is stopped.", s.name) - return errStop + return s.fail(ErrAborted) } } return nil } +// policyNow is the policies a VM boots with now. +func (e *Engine) policyNow() (*policy.File, error) { + if e.opts.Policy != nil { + return e.opts.Policy() + } + g := policy.DefaultGuarded() + if e.opts.Guarded != nil { + g = *e.opts.Guarded + } + return policy.New(e.app, e.opts.Network, g), nil +} + // beforeBoot fails unless what Lima would boot the stopped VM with is what -// agent-vm set: every share with the read-only names when protected, no +// the engine set: every share with the read-only names when protected, no // share for a scratch VM. -func (s *start) beforeBoot() error { +func (s *session) beforeBoot() error { inst, err := s.get(s.name) if err != nil || inst == nil { s.warnf("Error: could not read Lima's config for VM '%s', so what it would boot with is not known.", s.name) - return errStop + return s.fail(ErrFailed) } // Each that applies, none in place of another: --readonly with .git // protected must hold both. var whys []string - if s.opts.Scratch && len(inst.Mounts) > 0 { - whys = append(whys, "a share, though a scratch VM gets none") + if s.noShares && len(inst.Mounts) > 0 { + if s.scratch { + whys = append(whys, "a share, though a scratch VM gets none") + } else { + whys = append(whys, "a share, though this VM is to share nothing") + } } if err := vm.Confined(inst); err != nil { whys = append(whys, err.Error()) } - if s.protect && !s.opts.Scratch { + if s.protect && !s.noShares { whys = append(whys, unprotected(inst, s.names)) } - if s.opts.ReadOnly { + if s.spec.ReadOnly { whys = append(whys, notReadOnly(inst)) } for _, why := range whys { if why != "" { s.warnf("Error: VM '%s' would not boot as asked: %s.", s.name, why) - return errStop + return s.fail(ErrUnconfined) } } - if err := policiesErr(s.state); err != nil { + // The policies in force now are what the hostagent boots with: written + // to the VM's folder, which it reads. An unreadable file stops the start + // here, where the user sees it, not in Lima's log. + pf, err := s.policyNow() + if err == nil { + err = policy.Write(s.state.Policy(s.name), pf) + } + if err != nil { s.warnf("Error: %v", err) - return errStop + return s.fail(ErrFailed) } return nil } -// policiesErr is what the hostagent would stop on in the network and -// guarded files: said here, where the user sees it, not in Lima's log. -func policiesErr(st state.Dir) error { - if _, err := netguard.Load(string(st)); err != nil { - return err - } - _, err := guard.Load(string(st), "") +// PolicyErr is what a start would stop on in the policies, for doctor. +func (e *Engine) PolicyErr() error { + _, err := e.policyNow() return err } // configErr says so when Lima cannot read inst's config. -func (s *start) configErr(inst *vm.Instance) bool { +func (s *session) configErr(inst *vm.Instance) bool { if inst == nil || inst.ConfigErr == nil { return false } - s.warnf("Error: Lima cannot read the config of VM '%s':\n %v\n'agent-vm --reset ' re-clones it.", inst.Name, inst.ConfigErr) + s.warnf("Error: Lima cannot read the config of VM '%s':\n %v\n'%s --reset ' re-clones it.", inst.Name, inst.ConfigErr, s.app) return true } -// limaOverridesOK says so when Lima's default.yaml or override.yaml would -// add to what agent-vm sets for every VM. -func (s *start) limaOverridesOK() bool { - found := vm.LimaOverrides(LimaHome(s.state)) +// LimaOverridesOK says so when Lima's default.yaml or override.yaml would +// add to what the engine sets for every VM. +func (e *Engine) LimaOverridesOK() bool { + found := vm.LimaOverrides(e.opts.LimaHome) for _, f := range found { - s.warnf("Error: %s adds to every VM's config (shares, mount type...): agent-vm makes and starts no VM while it is there. Move it out of the way.", f) + e.warnf("Error: %s adds to every VM's config (shares, mount type...): %s makes and starts no VM while it is there. Move it out of the way.", f, e.app) } return len(found) == 0 } @@ -250,6 +315,9 @@ func unprotected(inst *vm.Instance, names []string) string { return "" } +// Unprotected is unprotected for the callers outside the engine (doctor). +func Unprotected(inst *vm.Instance, names []string) string { return unprotected(inst, names) } + // notReadOnly says why inst's shares are not all read-only on the host. func notReadOnly(inst *vm.Instance) string { if err := vm.Confined(inst); err != nil { @@ -301,49 +369,63 @@ func allReadOnly(inst *vm.Instance) bool { return len(inst.Mounts) > 0 } +// AllReadOnly reports whether every share of inst is read-only (and there is +// one). +func AllReadOnly(inst *vm.Instance) bool { return allReadOnly(inst) } + // ensureRunning makes the project's VM exist and run, with its shares as // asked: cloned from the base, changed while stopped, the security checks // done first. Under the VM's lock: another run in the same folder waits for // this one to be done with the VM, and cleans up nothing of it meanwhile. -func (s *start) ensureRunning() error { +func (s *session) ensureRunning() error { // A scratch VM has a name of its own, which no other run uses. - if !s.opts.Scratch { + if !s.scratch { unlock, err := s.lockVM(s.ctx, s.name) if err != nil { s.warnf("Error: could not lock VM '%s': %v", s.name, err) - return errStop + return s.fail(ErrFailed) } defer unlock() } return s.ensureRunningLocked() } -func (s *start) ensureRunningLocked() error { - o := &s.opts - if o.Scratch && (o.Reset || o.ReadOnly) { +func (s *session) ensureRunningLocked() error { + o := &s.spec + s.noShares = o.NoShares || s.scratch + if s.scratch && (o.Reset || o.ReadOnly) { s.warnf("Error: --scratch makes a new VM that shares nothing: --reset and --readonly do not go with it.") - return errStop + return s.fail(ErrRefused) + } + if s.noShares && o.ReadOnly { + s.warnf("Error: --no-shares makes a VM that shares nothing: --readonly does not go with it.") + return s.fail(ErrRefused) } wantWritable := !o.ReadOnly - o.CPUs = host.Cap("cpus", o.CPUs, s.io.Stderr) - o.Memory = host.Cap("memory", o.Memory, s.io.Stderr) - host.WarnDiskSpace(LimaHome(s.state), o.Disk, s.io.Stderr) - b, err := s.backend() + o.CPUs = s.capHost("cpus", o.CPUs) + o.MemoryGiB = s.capHost("memory", o.MemoryGiB) + host.WarnDiskSpace(s.state.VMs(), o.DiskGiB, s.warnWriter()) + b, err := s.Change(s.ctx) if err != nil { s.warnf("Error: %v", err) - return errStop + return s.fail(ErrFailed) + } + if !s.LimaOverridesOK() { + return s.fail(ErrLimaOverrides) } - if !s.limaOverridesOK() { - return errStop + if s.named { + if err := s.checkBinding(); err != nil { + return err + } } - warnDomains(s.state, s.io.Stderr) + s.warnDomains() // Two VMs set to one port: the second would fail to start. - if o.SSHPort > 0 { + if o.SSHPort != nil && *o.SSHPort > 0 { if all, err := b.List(s.ctx); err == nil { for _, v := range all { - if v.Name != s.name && v.SSHPortConfig == o.SSHPort { - s.warnf("Error: SSH port %d is already set for VM '%s'.", o.SSHPort, v.Name) - return errStop + if v.Name != s.name && v.SSHPortConfig == *o.SSHPort { + s.warnf("Error: SSH port %d is already set for VM '%s'.", *o.SSHPort, v.Name) + return s.fail(ErrRefused) } } } @@ -352,8 +434,8 @@ func (s *start) ensureRunningLocked() error { // keys, read-write; a share holding agent-vm's own files lets the VM // change what the host runs next. agent-vm's own clone may be shared // once the security prompts are off: working on agent-vm in its VM. - if !o.Scratch { - refs := s.refs() + if !s.noShares { + refs := s.Refs() if _, off := s.promptsDisabledBy(); off { var others []mounts.Ref for _, r := range refs { @@ -363,134 +445,154 @@ func (s *start) ensureRunningLocked() error { } if _, bad := mounts.UnsafeProject(s.dir, others); !bad { if why, self := mounts.UnsafeLocation(s.dir, refs); self { - s.warnf("Warning: sharing %s with the VM: it %s. The VM can change the agent-vm this machine runs next.", s.dir, why) - s.confirmUnsafe() + s.warnf("Warning: sharing %s with the VM: it %s. The VM can change the %s this machine runs next.", s.dir, why, s.app) + s.confirmUnsafe("running-self") } refs = others } } if why, bad := mounts.UnsafeProject(s.dir, refs); bad { - s.warnf("Error: refusing to share %s with a VM: it %s.\nRun agent-vm from a project directory.", s.dir, why) - return errStop + s.warnf("Error: refusing to share %s with a VM: it %s.\nRun %s from a project directory.", s.dir, why, s.app) + return &RefusedError{Dir: s.dir, Why: why, Text: strings.TrimPrefix(s.lastErr, "Error: ")} } if why, bad := mounts.Unmountable(s.dir); bad { s.warnf("Error: project path %s:\n %s\nRename the directory, then retry.", why, s.dir) - return errStop + return &RefusedError{Dir: s.dir, Why: why, Text: strings.TrimPrefix(s.lastErr, "Error: ")} } } // A run interrupted while Lima made it leaves a folder without lima.yaml. // Under the VM's lock: no other run is making it now. - if d := filepath.Join(LimaHome(s.state), s.name); isDirNoYAML(d) { - s.warnf("Detected partial VM state at %s (no lima.yaml): cleaning up.", d) - if err := os.RemoveAll(d); err != nil { - s.warnf("Error: could not remove %s:\n%v", d, err) - return errStop - } + if !s.cleanPartial(s.ctx, s.name) { + return s.fail(ErrFailed) } - base, err := s.get(vmname.Template) + base, err := s.get(s.base) if err != nil { s.warnf("Error: could not query Lima: %v", err) - return errStop + return s.fail(ErrFailed) } if s.configErr(base) { - return errStop + return s.fail(ErrFailed) } - if _, merr := os.Stat(s.state.Path(state.BaseVersion)); base == nil || merr != nil { + if base == nil || !s.state.BaseReady(s.base) { if base != nil { - s.warnf("Error: Base VM setup did not complete. Run 'agent-vm setup' again.") - } else { - s.warnf("Error: Base VM not found. Run 'agent-vm setup' first.") + s.warnf("Error: Base VM setup did not complete. Run '%s setup' again.", s.app) + return s.fail(ErrBaseIncomplete) } - return errStop + s.warnf("Error: Base VM not found. Run '%s setup' first.", s.app) + return s.fail(ErrBaseMissing) } s.vmUp = !o.Reset && s.running() if !s.checks() { - return errStop + return s.fail(ErrAborted) } inst, err := s.get(s.name) if err != nil { s.warnf("Error: could not query Lima: %v", err) - return errStop + return s.fail(ErrFailed) } if !o.Reset && s.configErr(inst) { - return errStop + return s.fail(ErrFailed) + } + // Lima's hostagent died under it: the VM is cut off, and blocks its next + // start. + if inst != nil && inst.Orphaned { + s.warnf("Warning: VM '%s' was left running without Lima's host agent, cut off: stopping it.", s.name) + if !s.Stop(s.ctx, s.name) { + s.warnf("Error: could not stop VM '%s'. See '%s list'.", s.name, s.app) + return s.fail(ErrFailed) + } + if inst, err = s.get(s.name); err != nil { + s.warnf("Error: could not query Lima: %v", err) + return s.fail(ErrFailed) + } } if o.Reset && inst != nil { - fmt.Fprintf(s.io.Stdout, "Resetting VM '%s'...\n", s.name) - if !s.deleteVM(s.ctx, s.name) { - return errStop + s.infof("Resetting VM '%s'...", s.name) + if !s.Delete(s.ctx, s.name) { + return s.fail(ErrFailed) } inst = nil } // 0.1.0 recorded no version for its bases, and their VMs have no sshfs, // which the shares need; 0.2 migrated them in place. - if inst != nil && !o.Scratch && !exists(s.state.Marker(state.BuiltByOf, s.name)) && !exists(s.state.Marker(state.SSHFSOf, s.name)) { - s.warnf("Error: VM '%s' comes from a base built by agent-vm 0.1.0, without the sshfs its shares need. Run 'agent-vm setup', then 'agent-vm --reset ' here.", s.name) - return errStop + if inst != nil && !s.noShares && !builtWithSSHFS(s.state.Known(s.name).Base) { + s.warnf("Error: VM '%s' comes from a base built by agent-vm 0.1.0, without the sshfs its shares need. Run '%s setup', then '%s --reset ' here.", s.name, s.app, s.app) + return s.fail(ErrBaseOld) } isNew := inst == nil applyResize := false if isNew { - fmt.Fprintf(s.io.Stdout, "Creating VM '%s'...\n", s.name) - marks, err := s.cloneBase() + s.infof("Creating VM '%s'...", s.name) + cloned, err := s.cloneBase() if err != nil { s.warnf("Error: could not clone the base template into '%s':\n%v", s.name, err) if errors.Is(err, vm.ErrNameTooLong) { - s.warnf(" The VM's name comes from the project folder's: give the folder a shorter name.") + if s.named { + s.warnf(" Give the VM a shorter name.") + } else { + s.warnf(" The VM's name comes from the project folder's: give the folder a shorter name.") + } } - return errStop + // Nothing was made: what the clone left of it goes. + _ = s.state.Forget(s.name) + return &Failure{Kind: errors.Join(ErrFailed, err), Text: strings.TrimPrefix(s.lastErr, "Error: ")} } s.created = true - for _, k := range []string{state.BuiltByOf, state.SSHFSOf} { - os.Remove(s.state.Marker(k, s.name)) - } - if marks.builtBy != nil { - if err := s.state.Write(state.BuiltByOf+s.name, marks.builtBy); err != nil { - s.warnf("Error: could not record which agent-vm built the base of VM '%s': %v", s.name, err) - s.dropNew() - return errStop - } - } else if s.protect { + if !builtWithSSHFS(cloned) && s.protect { // 0.1.0's bases have no sshfs for the protected shares. - s.warnf("Error: the base VM was built by agent-vm 0.1.0, without what the shares that keep .git read-only need. Run 'agent-vm setup' to build a new one.") + s.warnf("Error: the base VM was built by agent-vm 0.1.0, without what the shares that keep .git read-only need. Run '%s setup' to build a new one.", s.app) s.dropNew() - return errStop + return s.fail(ErrBaseOld) + } + bound := "" + if s.named { + bound = s.dir + if s.noShares { + bound = noFolder + } + } + if err := s.state.UpdateMeta(s.name, func(m *state.Metadata) { + m.Base = cloned + m.Bound = bound + }); err != nil { + s.warnf("Error: could not record which base VM '%s' was cloned from: %v", s.name, err) + s.dropNew() + return s.fail(ErrFailed) } // Configured before its first start. A clone that could not be holds // nothing yet: it goes, and the next run starts over. - if s.applyShares(wantWritable) != nil || s.applyResources() != nil { + if err := s.applyShares(wantWritable); err != nil { s.dropNew() - return errStop + return err } - fmt.Fprint(s.io.Stdout, s.notes) - if marks.version != nil { - if err := s.state.Write(state.VersionOf+s.name, marks.version); err != nil { - s.warnf("Warning: could not record the base VM '%s' was cloned from: %v. Every start will say that the base has been updated.", s.name, err) - } + if err := s.applyResources(); err != nil { + s.dropNew() + return err } - } else if ((o.Disk > 0 || o.Memory > 0 || o.CPUs > 0) && s.resourcesDiffer(inst)) || (o.SSHPort >= 0 && o.SSHPort != inst.SSHPortConfig) { + s.info(s.notes) + } else if ((o.DiskGiB > 0 || o.MemoryGiB > 0 || o.CPUs > 0) && s.resourcesDiffer(inst)) || (o.SSHPort != nil && *o.SSHPort != inst.SSHPortConfig) { // Only when the request differs: a caller passing its defaults every // time would otherwise be asked every time. Declined, the VM keeps // its settings and everything below still applies. applyResize = true if s.running() { - fmt.Fprintf(s.io.Stdout, "VM '%s' is currently running. It must be stopped to apply new settings.\n", s.name) - if s.ui.CanAsk() && s.ui.AskYN("Stop the VM and apply changes?", false) { - fmt.Fprintln(s.io.Stdout, "Stopping VM...") + s.infof("VM '%s' is currently running. It must be stopped to apply new settings.", s.name) + if s.canAsk() && s.ask(Question{Kind: QRestart, Prompt: "Stop the VM and apply changes?"}) { + s.infof("Stopping VM...") if err := s.stopToChange(); err != nil { return err } } else { - fmt.Fprintln(s.io.Stdout, "Not applied: the VM keeps its current settings.") + s.infof("Not applied: the VM keeps its current settings.") applyResize = false } } } if applyResize { - fmt.Fprintln(s.io.Stdout, "Updating VM settings...") + s.infof("Updating VM settings...") if err := s.applyResources(); err != nil { return err } @@ -505,11 +607,11 @@ func (s *start) ensureRunningLocked() error { wasRunning := s.running() if inst, err = s.get(s.name); err != nil || inst == nil { s.warnf("Error: could not query Lima: %v", err) - return errStop + return s.fail(ErrFailed) } want, _ := s.shares(wantWritable) wasProtected := len(inst.Mounts) > 0 && unprotected(inst, nil) == "" - gitStale := !o.Scratch && wasProtected != s.protect + gitStale := !s.noShares && wasProtected != s.protect namesStale := s.protect && wasProtected && unprotected(inst, s.names) != "" stale := sharesDiffer(inst, want) if !o.ReadOnly && wasRunning && s.protect && (gitStale || namesStale) { @@ -518,14 +620,14 @@ func (s *start) ensureRunningLocked() error { } else { s.warnf("Warning: VM '%s' runs with an older list of read-only names (it needs %s), so the agent can still write the new ones.", s.name, strings.Join(s.names, ", ")) } - if _, off := s.promptsDisabledBy(); !off && s.ui.CanAsk() && s.ui.AskYN("Restart it now to apply them? Sessions using it are cut.", true) { + if _, off := s.promptsDisabledBy(); !off && s.canAsk() && s.ask(Question{Kind: QRestart, Prompt: "Restart it now to apply them? Sessions using it are cut.", Default: true}) { if err := s.stopToChange(); err != nil { return err } wasRunning = false - } else if !s.confirmUnsafe() { - s.warnf("Aborted. 'agent-vm stop', then run again.") - return errStop + } else if !s.confirmUnsafe("running-writable") { + s.warnf("Aborted. '%s stop', then run again.", s.app) + return s.fail(ErrAborted) } } switch { @@ -534,21 +636,21 @@ func (s *start) ensureRunningLocked() error { case !wasRunning && stale: switch { case gitStale && s.protect: - fmt.Fprintf(s.io.Stdout, "Making every .git read-only for VM '%s'...\n", s.name) + s.infof("Making every .git read-only for VM '%s'...", s.name) case gitStale: s.warnf("Making .git writable for VM '%s' (%s)...", s.name, map[bool]string{true: "--unsafe-writable-git", false: "AGENT_VM_UNSAFE_WRITABLE_GIT=1"}[o.UnsafeWritableGit]) case namesStale: - fmt.Fprintf(s.io.Stdout, "Making %s read-only for VM '%s'...\n", strings.Join(s.names, ", "), s.name) + s.infof("Making %s read-only for VM '%s'...", strings.Join(s.names, ", "), s.name) } if gitStale || namesStale { - fmt.Fprint(s.io.Stdout, s.notes) + s.info(s.notes) } if !isNew { switch { case !wantWritable && !allReadOnly(inst): - fmt.Fprintf(s.io.Stdout, "Making every share of VM '%s' read-only...\n", s.name) + s.infof("Making every share of VM '%s' read-only...", s.name) case wantWritable && allReadOnly(inst): - fmt.Fprintf(s.io.Stdout, "VM '%s' was left read-only by --readonly; making it writable again...\n", s.name) + s.infof("VM '%s' was left read-only by --readonly; making it writable again...", s.name) } } if err := s.applyShares(wantWritable); err != nil { @@ -564,124 +666,187 @@ func (s *start) ensureRunningLocked() error { return s.afterBoot(isNew, wasRunning, wantWritable) } -// baseMarkers are the base's markers as they were when it was cloned: nil -// for one that was not there. -type baseMarkers struct{ builtBy, version []byte } +// noFolder is what a named VM that shares nothing is bound to: not a folder. +const noFolder = "-" + +// checkBinding holds a named VM to the folder it was made for. One that +// shares nothing is used from any folder, and never gets shares; one that +// shares a folder is used from that folder only, or the project of another +// would get the VM that has seen this one. One with no record of what it +// belongs to is not touched. --reset makes the VM again, bound anew: the only +// way to change it. +func (s *session) checkBinding() error { + inst, err := s.get(s.name) + if err != nil { + s.warnf("Error: could not query Lima: %v", err) + return s.fail(ErrFailed) + } + if inst == nil || s.spec.Reset { + return nil + } + bound := s.state.Known(s.name).Bound + switch { + case bound == "": + s.warnf("Error: VM '%s' has no record of the folder it belongs to. '%s rm %s' deletes it.", s.name, s.app, s.name) + return s.fail(ErrRefused) + case bound == noFolder: + s.noShares = true + if s.spec.ReadOnly { + s.warnf("Error: VM '%s' shares nothing: --readonly does not go with it.", s.name) + return s.fail(ErrRefused) + } + case s.spec.NoShares: + s.warnf("Error: VM '%s' shares %s, and keeps doing so. '--reset' makes it again, with no shares.", s.name, bound) + return s.fail(ErrRefused) + case bound != s.dir: + s.warnf("Error: VM '%s' belongs to %s. Run it from there, or '%s rm %s'.", s.name, bound, s.app, s.name) + return &RefusedError{Dir: s.dir, Why: "belongs to " + bound, Text: strings.TrimPrefix(s.lastErr, "Error: ")} + } + return nil +} + +// capHost is val capped to the share of the host a VM may use. +func (e *Engine) capHost(kind string, val int) int { + return host.CapShare(e.opts.HostShare, kind, val, e.writer(Warning, "")) +} // cloneBase clones the base template into the VM, under the base's lock: a -// setup swapping the base in is not cloned from half way. The base's markers -// are read under it too, so that they are those of the base cloned. -func (s *start) cloneBase() (baseMarkers, error) { - var m baseMarkers - b, err := s.backend() +// setup swapping the base in is not cloned from half way. It returns the +// base as it is described, read under the lock too, so that it is the one +// cloned: nil when it describes nothing. +func (s *session) cloneBase() (*state.Base, error) { + b, err := s.Change(s.ctx) if err != nil { - return m, err + return nil, err } - unlock, err := s.lockVM(s.ctx, vmname.Template) + unlock, err := s.lockVM(s.ctx, s.base) if err != nil { - return m, err + return nil, err } defer unlock() - if err := b.Clone(s.ctx, vmname.Template, s.name); err != nil { - return m, err + if err := b.Clone(s.ctx, s.base, s.name); err != nil { + return nil, err } - m.builtBy, _ = os.ReadFile(s.state.Path(state.BaseBuiltBy)) - m.version, _ = os.ReadFile(s.state.Path(state.BaseVersion)) - return m, nil + return s.state.Known(s.base).Base, nil } +// builtWithSSHFS reports whether the base a VM comes from has the sshfs its +// shares need: 0.1.0 recorded no version for its bases, which have none. +func builtWithSSHFS(b *state.Base) bool { return b != nil && b.BuiltBy != "" } + +// BuiltWithSSHFS is builtWithSSHFS for the callers outside the engine +// (doctor). +func BuiltWithSSHFS(b *state.Base) bool { return builtWithSSHFS(b) } + // dropNew deletes the VM this run just made and could not set up. -func (s *start) dropNew() { - if s.deleteVM(context.WithoutCancel(s.ctx), s.name) { +func (s *session) dropNew() { + if s.Delete(context.WithoutCancel(s.ctx), s.name) { s.created = false } } -func isDirNoYAML(d string) bool { +// cleanPartial removes the folder of a VM that Lima did not finish making +// (a folder, and no VM in it), unless the --scratch run it belongs to is +// alive. False when it could not. +func (e *Engine) cleanPartial(ctx context.Context, name string) bool { + d := e.state.VM(name) if fi, err := os.Stat(d); err != nil || !fi.IsDir() { + return true + } + b, err := e.Change(ctx) + if err != nil { + return true + } + if _, err := b.Get(ctx, name); !errors.Is(err, vm.ErrNotFound) { + return true + } + if pid := e.state.Known(name).ScratchPID; pid > 0 && alive(pid) { + return true + } + e.warnf("Detected partial VM state at %s (no lima.yaml): cleaning up.", d) + if err := os.RemoveAll(d); err != nil { + e.warnf("Error: could not remove %s:\n%v", d, err) return false } - _, err := os.Stat(filepath.Join(d, "lima.yaml")) - return err != nil + return true } // boot starts the stopped VM, its config checked first. -func (s *start) boot() error { +func (s *session) boot() error { if err := s.beforeBoot(); err != nil { return err } // Windows: QEMU's folder on PATH (winget does not put it there), for the // hostagent this starts. - if !host.WindowsPrereqs(s.io.Stdout, s.io.Stderr) { - return errStop + if !host.WindowsPrereqs(s.writer(Info, s.name), s.warnWriter()) { + return s.fail(ErrFailed) } - fmt.Fprintf(s.io.Stdout, "Starting VM '%s'...\n", s.name) - b, _ := s.backend() + s.infof("Starting VM '%s'...", s.name) + b, _ := s.Change(s.ctx) var log bytes.Buffer if err := b.Start(s.ctx, s.name, &log); err != nil { - ha := paths.Host(filepath.Join(LimaHome(s.state), s.name, "ha.stderr.log")) + ha := paths.Host(filepath.Join(s.state.Lima(s.name), "ha.stderr.log")) s.warnf("Error: Failed to start VM '%s'.\n--- Lima's output ---\n%s%v\nFull log: %s", s.name, log.String(), err, ha) - host.WindowsStartHint(s.io.Stderr, ha) - return errStop + host.WindowsStartHint(s.warnWriter(), ha) + return s.fail(ErrFailed) } - s.recordPolicy() return nil } // afterBoot brings the running VM's shares in line with the mode asked, by // a write probe on the project (which pushes the env too), then installs // the terminal's terminfo, runs the runtime scripts and binds single files. -func (s *start) afterBoot(isNew, wasRunning, wantWritable bool) error { - o := s.opts +func (s *session) afterBoot(isNew, wasRunning, wantWritable bool) error { + o := s.spec guest := paths.Guest(s.dir) var guestEnv, guestRuntime, projectRuntime, payload string - if o.Scratch { + if s.noShares { if code, err := s.shellRun(nil, "", "sh", "-c", `sudo install -d -o "$(id -u)" -g "$(id -g)" "$1"`, "sh", guest); err != nil || code != 0 { s.warnf("Error: could not make %s in VM '%s'.", s.dir, s.name) - return errStop + return s.fail(ErrFailed) } // Only the shared file: the project's stays out. Read as the other // path does, a regular file only (a FIFO would block the start). - if b, ok := readRegular(s.state.Path("env")); ok { + if b, ok := readRegular(s.opts.EnvFile); ok { payload = strings.TrimRight(env.StripCR(b), "\n") } } else { - pe := env.ProjectEnvFile(s.dir) - projectRuntime = runscript.ProjectPath(s.dir) + pe := s.projectEnvFile() + projectRuntime = s.projectRuntime() // By their path in the project, the one the VM has. - if rel, ok := env.ProjectRel(s.dir, pe); ok { + if rel, ok := env.ProjectRel(s.dir, pe); ok && pe != "" { guestEnv = guest + "/" + rel } - if rel, ok := env.ProjectRel(s.dir, projectRuntime); ok { + if rel, ok := env.ProjectRel(s.dir, projectRuntime); ok && projectRuntime != "" { guestRuntime = guest + "/" + rel } - payload = strings.TrimRight(env.Payload(s.state.Path("env"), s.dir), "\n") + payload = strings.TrimRight(env.PayloadFor(s.opts.EnvFile, s.dir, pe), "\n") } probe := s.probe(payload, guestEnv, guestRuntime) - if o.Scratch && probe.writable != "true" { + if s.noShares && probe.writable != "true" { s.warnf("Error: %s is not writable in VM '%s'.", s.dir, s.name) - return errStop + return s.fail(ErrFailed) } inst, _ := s.get(s.name) want := fmt.Sprint(wantWritable) - if !o.Scratch && (probe.writable != want || !wantWritable && inst != nil && !allReadOnly(inst)) { + if !s.noShares && (probe.writable != want || !wantWritable && inst != nil && !allReadOnly(inst)) { switch { case probe.writable == "lost": s.warnf("Warning: the project share is not mounted in VM '%s': what was written in %s in the VM since then is on the VM's own disk, and hidden once the share is back.", s.name, s.dir) s.warnf("Project share is not mounted; repairing...") case wantWritable && wasRunning && inst != nil && allReadOnly(inst): - fmt.Fprintf(s.io.Stdout, "VM '%s' runs read-only (--readonly). It must be restarted to make its shares writable.\n", s.name) - if !s.ui.CanAsk() || !s.ui.AskYN("Stop the VM and make it writable? Sessions using it are cut.", false) { - s.warnf("Error: not restarted. Pass --readonly to use it as it is, or 'agent-vm stop' it first.") - return errStop + s.infof("VM '%s' runs read-only (--readonly). It must be restarted to make its shares writable.", s.name) + if !s.canAsk() || !s.ask(Question{Kind: QRestart, Prompt: "Stop the VM and make it writable? Sessions using it are cut."}) { + s.warnf("Error: not restarted. Pass --readonly to use it as it is, or '%s stop' it first.", s.app) + return s.fail(ErrNotApplied) } case !wantWritable && wasRunning: - fmt.Fprintf(s.io.Stdout, "VM '%s' was already running. It must be restarted to make its shares read-only.\n", s.name) - if !s.ui.CanAsk() || !s.ui.AskYN("Stop the VM and apply --readonly?", false) { + s.infof("VM '%s' was already running. It must be restarted to make its shares read-only.", s.name) + if !s.canAsk() || !s.ask(Question{Kind: QRestart, Prompt: "Stop the VM and apply --readonly?"}) { // Carrying on writable after --readonly was asked for is the // one outcome that must not be silent. s.warnf("Error: --readonly was requested but not applied. Aborting.") - return errStop + return s.fail(ErrNotApplied) } case wantWritable: s.warnf("Project mount is not writable; repairing...") @@ -697,22 +862,21 @@ func (s *start) afterBoot(isNew, wasRunning, wantWritable bool) error { if err := s.beforeBoot(); err != nil { return err } - b, _ := s.backend() + b, _ := s.Change(s.ctx) if err := b.Start(s.ctx, s.name, io.Discard); err != nil { s.warnf("Error: VM '%s' did not come back up after changing the project mount.", s.name) - return errStop + return s.fail(ErrFailed) } - s.recordPolicy() if probe = s.probe(payload, guestEnv, guestRuntime); probe.writable != want { switch { case probe.writable == "lost": - s.warnf("Error: the project share did not mount in VM '%s':\n %s\nLima's log: %s", s.name, s.dir, filepath.Join(LimaHome(s.state), s.name, "ha.stderr.log")) + s.warnf("Error: the project share did not mount in VM '%s':\n %s\nLima's log: %s", s.name, s.dir, filepath.Join(s.state.Lima(s.name), "ha.stderr.log")) case wantWritable: - s.warnf("Error: project directory is still not writable inside the VM:\n %s\nThe host mount failed to attach. Try 'agent-vm --reset '\nto re-clone the VM from the base template.", s.dir) + s.warnf("Error: project directory is still not writable inside the VM:\n %s\nThe host mount failed to attach. Try '%s --reset '\nto re-clone the VM from the base template.", s.dir, s.app) default: s.warnf("Error: failed to mount the project directory read-only:\n %s", s.dir) } - return errStop + return s.fail(ErrFailed) } } // --readonly is claimed once every share Lima has is read-only: the @@ -721,7 +885,7 @@ func (s *start) afterBoot(isNew, wasRunning, wantWritable bool) error { inst, _ := s.get(s.name) if inst == nil { s.warnf("Error: --readonly cannot hold: could not read Lima's config for VM '%s'.", s.name) - return errStop + return s.fail(ErrFailed) } why := notReadOnly(inst) if why == "" && !allReadOnly(inst) { @@ -729,25 +893,27 @@ func (s *start) afterBoot(isNew, wasRunning, wantWritable bool) error { } if why != "" { s.warnf("Error: --readonly cannot hold: VM '%s' runs with %s.", s.name, why) - return errStop + return s.fail(ErrUnconfined) } - fmt.Fprintln(s.io.Stdout, "Read-only: the project and every other share (enforced on the host).") + s.infof("Read-only: the project and every other share (enforced on the host).") } s.terminfo() - if _, err := os.Stat(s.state.Path("runtime.sh")); err == nil { - fmt.Fprintln(s.io.Stdout, "Running user runtime setup...") - s.runHostScript(s.state.Path("runtime.sh")) + if s.opts.RuntimeScript != "" { + if _, err := os.Stat(s.opts.RuntimeScript); err == nil { + s.infof("Running user runtime setup...") + s.runHostScript(s.opts.RuntimeScript) + } } switch { case guestRuntime != "": if probe.runtimeFound { - fmt.Fprintln(s.io.Stdout, "Running project runtime setup...") + s.infof("Running project runtime setup...") s.shellRun(nil, guest, "zsh", "-lc", `[ -f "$3" ] || exit 0; i="$(awk "$1" "$3")" && awk "$2" "$3" | "$i" -s`, "agent-vm", runscript.ShebangAWK, runscript.StripCRAWK, guestRuntime) } case projectRuntime != "" && isFile(projectRuntime): - fmt.Fprintln(s.io.Stdout, "Running project runtime setup...") + s.infof("Running project runtime setup...") s.runHostScript(projectRuntime) } s.bindFiles(isNew) @@ -759,7 +925,7 @@ func isFile(p string) bool { fi, err := os.Stat(p); return err == nil && fi.Mode // readRegular is the content of p when it is a regular file once links are // followed. Another kind (a FIFO, a device) is not read: it could block. func readRegular(p string) (string, bool) { - if !isFile(p) { + if p == "" || !isFile(p) { return "", false } b, err := os.ReadFile(p) @@ -769,7 +935,7 @@ func readRegular(p string) (string, bool) { // runHostScript runs a runtime script the host reads in the VM, piped in, // CRs dropped, under the shell it declares, from a login zsh: the VM's PATH // and ~/.agent-vm.env apply. -func (s *start) runHostScript(path string) { +func (s *session) runHostScript(path string) { b, ok := readRegular(path) if !ok { s.warnf("Warning: could not read %s (not a regular file, or unreadable): not run.", path) @@ -779,52 +945,48 @@ func (s *start) runHostScript(path string) { s.shellRun(strings.NewReader(env.StripCR(b)), paths.Guest(s.dir), "zsh", "-lc", "exec "+interp+" -s") } -// shellRun runs args in the VM, its output on the command's, and returns +// shellRun runs args in the VM, its output reported as events, and returns // the exit status. -func (s *start) shellRun(stdin io.Reader, workdir string, args ...string) (int, error) { - b, err := s.backend() +func (s *session) shellRun(stdin io.Reader, workdir string, args ...string) (int, error) { + b, err := s.Change(s.ctx) if err != nil { return 0, err } if stdin == nil { stdin = strings.NewReader("") } - return b.Shell(s.ctx, s.name, vm.ShellOpts{Workdir: workdir, Args: args, Stdin: stdin, Stdout: s.io.Stdout, Stderr: s.io.Stderr}) + return b.Shell(s.ctx, s.name, vm.ShellOpts{Workdir: workdir, Args: args, Stdin: stdin, Stdout: s.writer(Info, s.name), Stderr: s.warnWriter()}) } // terminfo puts the host's terminfo entry in the VM, for terminals Debian // does not know (xterm-ghostty, xterm-kitty), once per VM and $TERM. -func (s *start) terminfo() { - t := os.Getenv("TERM") - if t == "" || s.state.ReadMarker(state.TermOf, s.name) == t { +func (s *session) terminfo() { + t := s.opts.TERM + if t == "" || s.state.Known(s.name).Term == t { return } entry, err := exec.CommandContext(s.ctx, "infocmp", "-x", t).Output() if err != nil { return } - b, _ := s.backend() + b, _ := s.Change(s.ctx) code, err := b.Shell(s.ctx, s.name, vm.ShellOpts{Args: []string{"sudo", "tic", "-x", "-"}, Stdin: bytes.NewReader(entry), Stdout: io.Discard, Stderr: io.Discard}) if err != nil || code != 0 { s.warnf("Warning: failed to install '%s' terminfo inside VM.", t) return } - if err := s.state.Write(state.TermOf+s.name, []byte(t+"\n")); err != nil { + if err := s.state.UpdateMeta(s.name, func(m *state.Metadata) { m.Term = t }); err != nil { s.warnf("Warning: could not record that VM '%s' has the terminfo of '%s': %v", s.name, t, err) } } -// bindFiles binds the single files of ~/.agent-vm/volumes in place in the -// VM, an existing VM's staged copy refreshed first, so an edit saved by -// rename on the host shows. A bind does not survive a restart, and is -// skipped when there. -func (s *start) bindFiles(isNew bool) { - b, err := os.ReadFile(s.state.Marker(state.FileMountsOf, s.name)) - if err != nil { - return - } +// bindFiles binds the single files of the volumes in place in the VM, an +// existing VM's staged copy refreshed first, so an edit saved by rename on +// the host shows. A bind does not survive a restart, and is skipped when +// there. +func (s *session) bindFiles(isNew bool) { var entries []string - for _, line := range strings.Split(strings.TrimSuffix(string(b), "\n"), "\n") { + for _, line := range s.state.Known(s.name).FileMounts { f := strings.SplitN(line, "|", 4) if len(f) != 4 || f[1] == "" { continue @@ -833,7 +995,7 @@ func (s *start) bindFiles(isNew bool) { case isNew: case !exists(f[0]): s.warnf("Warning: Mount source '%s' no longer exists; VM will see the last-staged copy.", f[0]) - case !mounts.Stage(f[0], f[1], s.io.Stderr): + case !mounts.Stage(f[0], f[1], s.warnWriter()): s.warnf("Warning: Failed to refresh staged '%s'; VM may see stale content.", f[0]) } if f[2] != "" && f[3] != "" { @@ -843,7 +1005,7 @@ func (s *start) bindFiles(isNew bool) { if len(entries) == 0 { return } - fmt.Fprintln(s.io.Stdout, "Mounting individual files...") + s.infof("Mounting individual files...") // The paths as arguments, never in the script. code, err := s.shellRun(nil, "", append([]string{"sudo", "bash", "-c", ` set -e @@ -865,3 +1027,23 @@ func (s *start) bindFiles(isNew bool) { } func exists(p string) bool { _, err := os.Stat(p); return err == nil } + +// staleState says whether vm was cloned from an older base than the +// current one: 1, 0, or unknown with nothing to compare against. A VM with +// no record but a known base predates the record, which makes it stale. +func (e *Engine) staleStateOf(name, template string) string { + base := e.state.Known(template).Base + if base == nil || base.Built <= 0 { + return "unknown" + } + if mine := e.state.Known(name).Base; mine == nil || mine.Built != base.Built { + return "1" + } + return "0" +} + +func (s *session) staleState(name string) string { return s.staleStateOf(name, s.base) } + +// StaleState says whether the VM name was cloned from an older template than +// the current one: "1", "0", or "unknown" with nothing to compare against. +func (e *Engine) StaleState(name, template string) string { return e.staleStateOf(name, template) } diff --git a/internal/engine/setup.go b/internal/engine/setup.go new file mode 100644 index 00000000..d48f5f7c --- /dev/null +++ b/internal/engine/setup.go @@ -0,0 +1,191 @@ +package engine + +import ( + "context" + "fmt" + "io" + "path/filepath" + "strings" + "time" + + "github.com/sylvinus/agent-vm/internal/host" + "github.com/sylvinus/agent-vm/internal/policy" + "github.com/sylvinus/agent-vm/internal/state" + "github.com/sylvinus/agent-vm/internal/version" + "github.com/sylvinus/agent-vm/internal/vm" + "github.com/sylvinus/agent-vm/provision" +) + +// Output shows what the steps of Setup print, as the program likes: a window +// of the last lines, a log. Step runs fn with the writer the step's output +// goes to, and returns fn's error. +type Output interface { + // The steps are "create", "start" and "script:" for each script. + Step(name string, fn func(stdout, stderr io.Writer) error) error +} + +// plainOutput drops what the steps print. +type plainOutput struct{} + +func (plainOutput) Step(_ string, fn func(stdout, stderr io.Writer) error) error { + return fn(io.Discard, io.Discard) +} + +// SetupError is why Setup stopped. +type SetupError struct { + // Step is "create", "start", "script" (see Script), "stop" or "swap"; or + // "prepare" for what had to hold before the build. + Step string + Script string + Err error + // Told: the engine told why in an Error event already. + Told bool + // LimaLog is Lima's own log of the build VM, for a start that failed. + LimaLog string +} + +func (e *SetupError) Error() string { + if e.Script != "" { + return fmt.Sprintf("setup: %s %s: %v", e.Step, e.Script, e.Err) + } + return fmt.Sprintf("setup: %s: %v", e.Step, e.Err) +} +func (e *SetupError) Unwrap() error { return e.Err } + +// Setup builds the base template: made as t.Name+"-building" (created, +// booted, provisioned by t's scripts, stopped), then swapped in for the +// template and marked as ready. A setup that fails before the swap leaves +// the template it found. +func (e *Engine) Setup(ctx context.Context, t provision.Template, out Output) error { + if out == nil { + out = plainOutput{} + } + if t.Name == "" { + t.Name = DefaultBase + } + building := Building(t.Name) + told := func(step string, err error) error { return &SetupError{Step: step, Err: err, Told: true} } + b, err := e.Change(ctx) + if err != nil { + e.warnf("Error: %v", err) + return told("prepare", err) + } + // One setup at a time: two would build in the same VM. + unlockSetup, err := e.lockVM(ctx, t.Name+"-setup") + if err != nil { + e.warnf("Error: could not lock the base template's setup: %v", err) + return told("prepare", err) + } + defer unlockSetup() + if !e.LimaOverridesOK() { + return told("prepare", ErrLimaOverrides) + } + pf, err := e.policyNow() + if err != nil { + e.warnf("Error: %v", err) + return told("prepare", err) + } + (&session{Engine: e, ctx: ctx}).warnDomains() + // A build an earlier setup left, finished or not, is not this one's. + if !e.cleanPartial(ctx, building) { + return told("prepare", ErrFailed) + } + if !e.Delete(ctx, building) { + return told("prepare", ErrFailed) + } + // The build boots with the policies of now, as every VM does. + if err := policy.Write(e.state.Policy(building), pf); err != nil { + e.warnf("Error: %v", err) + return told("prepare", err) + } + + cpus := e.capHost("cpus", t.CPUs) + memory := e.capHost("memory", t.MemoryGiB) + host.WarnDiskSpace(e.state.VMs(), t.DiskGiB, e.writer(Warning, "")) + stopBase := func() { _ = b.Stop(ctx, building) } + + e.infof("Creating base VM...") + none := []vm.Mount{} + if err := out.Step("create", func(w, _ io.Writer) error { + return b.Create(ctx, building, "template:debian-13", vm.Settings{CPUs: cpus, MemoryGiB: memory, DiskGiB: t.DiskGiB, Mounts: &none, NoContainerd: true}, w) + }); err != nil { + return &SetupError{Step: "create", Err: err} + } + if inst, err := e.get(ctx, building); err == nil && inst != nil { + e.infof(" Resources: CPUs: %d, Memory: %d GiB, Disk: %d GiB", inst.CPUs, inst.Memory>>30, inst.Disk>>30) + } + + e.infof("Starting base VM (the first run downloads a Debian image)...") + if err := out.Step("start", func(w, _ io.Writer) error { return b.Start(ctx, building, w) }); err != nil { + return &SetupError{Step: "start", Err: err, LimaLog: filepath.Join(e.state.Lima(building), "ha.stderr.log")} + } + + for _, s := range t.Scripts { + if s.Message != "" { + e.infof("%s", s.Message) + } + shell := s.Shell + if len(shell) == 0 { + shell = []string{"bash", "-l"} + } + if err := out.Step("script:"+s.Name, func(stdout, stderr io.Writer) error { + code, err := b.Shell(ctx, building, vm.ShellOpts{Args: shell, + Stdin: strings.NewReader(provision.Exports(t.Env, s.Env) + s.Content), Stdout: stdout, Stderr: stderr}) + if err == nil && code != 0 { + err = fmt.Errorf("exit status %d", code) + } + return err + }); err != nil { + stopBase() + return &SetupError{Step: "script", Script: s.Name, Err: err} + } + } + + // Ready only once stopped: Lima clones only a stopped instance. + if !e.Stop(ctx, building) { + e.warnf("Error: the new base VM is set up but did not stop: '%s stop %s', then '%s setup' again. The base from before is untouched.", e.app, building, e.app) + return told("stop", ErrFailed) + } + if !e.swapBase(ctx, b, t.Name) { + return told("swap", ErrFailed) + } + return nil +} + +// swapBase puts the finished build in the base's place: the old base and its +// folder go, the build is cloned to the base's name and marked ready, last +// the build goes. Under the base's lock, so that no start clones the base +// meanwhile. Lima has no rename, so the disk is copied once more; the build +// is kept if the copy cannot be made, and said. +func (e *Engine) swapBase(ctx context.Context, b vm.Backend, template string) bool { + building := Building(template) + unlock, err := e.lockVM(ctx, template) + if err != nil { + e.warnf("Error: could not lock the base template: %v. The new base is VM '%s'; '%s setup' again builds another.", err, building, e.app) + return false + } + defer unlock() + if !e.cleanPartial(ctx, template) { + return false + } + // Forgets the old base's folder with it, its metadata included: from here + // on, nothing says a base is ready. + if !e.Delete(ctx, template) { + e.warnf("Error: could not replace the base VM. The new one is VM '%s'; '%s setup' again builds another.", building, e.app) + return false + } + if err := b.Clone(ctx, building, template); err != nil { + e.warnf("Error: could not copy the new base VM to '%s': %v\nThere is no base now. The new one is VM '%s'; '%s setup' again builds another.", template, err, building, e.app) + return false + } + // The record last: it says the base can be cloned. + base := &state.Base{Name: template, Built: time.Now().Unix(), BuiltBy: version.Version} + if err := e.state.UpdateMeta(template, func(m *state.Metadata) { m.Base = base }); err != nil { + e.warnf("Error: could not record when and by which agent-vm the base was built: %v\nThe base VM is not marked ready: '%s setup' again.", err, e.app) + return false + } + if !e.Delete(ctx, building) { + e.warnf("Warning: the base is ready, but VM '%s' stays: '%s rm %s' frees its disk.", building, e.app, building) + } + return true +} diff --git a/internal/engine/spec.go b/internal/engine/spec.go new file mode 100644 index 00000000..e9e7fcf6 --- /dev/null +++ b/internal/engine/spec.go @@ -0,0 +1,48 @@ +package engine + +// Spec says how a VM is wanted. Zero values leave things as they are. +type Spec struct { + // Base is the template the VM is cloned from; "base" when empty. Made + // by Setup. + Base string + // CPUs, MemoryGiB and DiskGiB are the resources asked for, 0 for none. + // CPUs and memory are capped to the host's share (Options.HostShare); + // the disk can only grow. + CPUs, MemoryGiB, DiskGiB int + // SSHPort, when set, fixes the VM's SSH port; 0 gives it back to Lima. + SSHPort *int + // Reset deletes the VM first, and makes it again from the base. + Reset bool + // ReadOnly shares nothing writable with the VM: the project and every + // other share are read-only on the host. + ReadOnly bool + // NoShares makes a VM that shares nothing: no project, no volumes. Only + // the shared env goes in, and the project's folder is made in the VM as + // an empty working folder, at the same path. For a named VM (NamedVM) it + // is said when the VM is made, and implied after: the VM never gets + // shares. A scratch VM (NewScratchVM) shares nothing whatever this says. + NoShares bool + // UnsafeWritableGit leaves every .git writable for the VM, which lets it + // run commands on the host through git's config and hooks. + UnsafeWritableGit bool + // UnsafeNoPrompts, when it holds text, turns the security questions + // off: each is answered yes, with a notice naming this text (what asked + // for it). + UnsafeNoPrompts string +} + +// templateName is the base the spec names. +func (s Spec) templateName() string { + if s.Base == "" { + return DefaultBase + } + return s.Base +} + +// DefaultBase is the name of the template Setup makes and VMs are cloned +// from, unless another is named. +const DefaultBase = "base" + +// Building is the name of the VM Setup builds a template in before swapping +// it in for the template. +func Building(template string) string { return template + "-building" } diff --git a/internal/engine/vm.go b/internal/engine/vm.go new file mode 100644 index 00000000..6289f7c9 --- /dev/null +++ b/internal/engine/vm.go @@ -0,0 +1,193 @@ +package engine + +import ( + "context" + "errors" + "io" + "os" + "strconv" + + "github.com/sylvinus/agent-vm/internal/paths" + "github.com/sylvinus/agent-vm/internal/state" + "github.com/sylvinus/agent-vm/internal/vm" + "github.com/sylvinus/agent-vm/internal/vmname" +) + +// VM is the VM of a project folder (or a scratch one): the handle to make it +// exist and run, to run commands in it, and to stop and remove it. +type VM struct { + e *Engine + // Name is the VM's name; Dir the project folder, as the shell spells it. + Name, Dir string + + scratch, named bool + created bool +} + +// ProjectVM is the VM of the folder dir ("" for the current one), named from +// the folder as `name` prints it. Nothing is made yet: see Ensure. +func (e *Engine) ProjectVM(dir string) (*VM, error) { + abs, err := vmname.AbsDir(dir) + if err != nil { + return nil, err + } + return &VM{e: e, Name: vmname.Name(abs), Dir: abs}, nil +} + +// NewScratchVM is a new VM of a name of its own that shares nothing, for dir +// ("" for the current folder): its owner is recorded so that a run that dies +// without removing it leaves it to be found (ScratchLeftovers). The caller +// removes it. +func (e *Engine) NewScratchVM(dir string) (*VM, error) { + abs, err := vmname.AbsDir(dir) + if err != nil { + return nil, err + } + name, err := vmname.Scratch(abs) + if err != nil { + return nil, err + } + // Before the clone: the VM's folder, which holds the record, exists + // before Lima makes its own in it. + if err := e.state.UpdateMeta(name, func(m *state.Metadata) { m.ScratchPID = os.Getpid() }); err != nil { + return nil, errors.New("could not record scratch VM '" + name + "': " + err.Error()) + } + return &VM{e: e, Name: name, Dir: abs, scratch: true}, nil +} + +// NamedVM is the VM the user calls name (see vmname.ValidateCustom for what +// a name is), for the folder dir ("" for the current one). It belongs to the +// folder it is made in, or to none when it is made with Spec.NoShares, and +// Ensure refuses it from another folder if it shares one. +func (e *Engine) NamedVM(name, dir string) (*VM, error) { + if err := vmname.ValidateCustom(name); err != nil { + return nil, err + } + abs, err := vmname.AbsDir(dir) + if err != nil { + return nil, err + } + return &VM{e: e, Name: name, Dir: abs, named: true}, nil +} + +// VMOf is an existing VM reached by its name, as list prints it, whatever its +// kind: to stop it, remove it, look at it. Ensure is for the VMs the other +// constructors make. +func (e *Engine) VMOf(name string) *VM { + return &VM{e: e, Name: name} +} + +// ScratchLeftovers are the scratch VMs a run could not delete (killed, its +// terminal closed), by the record in their folder: dead are those recorded +// by a run whose pid is gone (a reused pid only delays the cleanup), unknown +// those whose record holds no pid or cannot be read, which is no proof that +// no run uses them. Only names NewScratchVM makes. +func (e *Engine) ScratchLeftovers() (dead, unknown []string) { + for _, name := range e.state.Names() { + if !vmname.IsScratch(name) { + continue + } + m, err := e.state.Meta(name) + switch { + case err != nil || m.ScratchPID < 0: + unknown = append(unknown, name) + case m.ScratchPID == 0: + // No record: a project's VM that looks like one. + case !alive(m.ScratchPID): + dead = append(dead, name) + } + } + return dead, unknown +} + +// Ensure makes the VM exist and run, with its shares as spec asks: cloned +// from the base, changed while stopped, the security checks done first. Under +// the VM's lock: another run for the same VM waits for this one to be done +// with it. Progress and warnings are events; an error is also told as an +// Error event, with a Failure (or a *RefusedError) that errors.Is matches +// against Err... values. +func (v *VM) Ensure(ctx context.Context, spec Spec) error { + s := &session{Engine: v.e, ctx: ctx, spec: spec, base: spec.templateName(), name: v.Name, dir: v.Dir, scratch: v.scratch, named: v.named} + err := s.ensureRunning() + v.created = v.created || s.created + return err +} + +// Created reports whether Ensure made the VM, and it is still there: a +// start that failed after that is told so that the caller can remove it. +func (v *VM) Created() bool { return v.created } + +// Details is what is known of a VM. +type Details struct { + // Instance is the VM as the backend has it; nil when there is none. + Instance *vm.Instance + // Base is the template it was cloned from, as it was; nil when not + // known. + Base *state.Base +} + +// Details are the VM's resources and where it comes from. +func (v *VM) Details(ctx context.Context) (*Details, error) { + inst, err := v.e.get(ctx, v.Name) + if err != nil { + return nil, err + } + return &Details{Instance: inst, Base: v.e.state.Known(v.Name).Base}, nil +} + +// Exec is a command to run in the VM. +type Exec struct { + // Args is the command: its first word is looked up in the VM's PATH, or + // a path. Leading NAME=value words set the environment. It runs from the + // project folder, in a login zsh, so the PATH of the agents and + // ~/.agent-vm.env apply. + Args []string + // TTY gives it a terminal, which full-screen programs need. + TTY bool + Stdin io.Reader + Stdout, Stderr io.Writer +} + +// Exec runs x.Args in the running VM through a login zsh, from the project: +// agent PATH entries and ~/.agent-vm.env are in ~/.zshenv. The command and +// its arguments are positional parameters, never re-parsed; `env --` runs +// it, leading VAR=value assignments working and a command starting with - +// not taken for env's option. A command that is not there is said so, with +// env's status (an agent installed only as an editor extension has none); a +// path is env's to run, and to say why it cannot (126). It returns the +// command's exit status. +func (v *VM) Exec(ctx context.Context, x Exec) (int, error) { + b, err := v.e.Backend() + if err != nil { + return 1, err + } + script := `case "$1" in *=*|*/*) ;; *) command -v -- "$1" >/dev/null || { print -r -- "` + v.e.app + `: $1 is not installed in this VM." >&2; exit 127; } ;; esac; exec env -- "$@"` + return b.Shell(ctx, v.Name, vm.ShellOpts{ + Workdir: paths.Guest(v.Dir), + Args: append([]string{"zsh", "-l", "-c", script, v.e.app}, x.Args...), + Stdin: x.Stdin, Stdout: x.Stdout, Stderr: x.Stderr, Interactive: x.TTY, + }) +} + +// Shell runs x.Args by the VM user's login shell, from the project folder, +// as given: {"zsh", "-l"} is a shell. It returns the exit status. +func (v *VM) Shell(ctx context.Context, x Exec) (int, error) { + b, err := v.e.Backend() + if err != nil { + return 1, err + } + return b.Shell(ctx, v.Name, vm.ShellOpts{Workdir: paths.Guest(v.Dir), Args: x.Args, + Stdin: x.Stdin, Stdout: x.Stdout, Stderr: x.Stderr, Interactive: x.TTY}) +} + +// Stop stops the VM, and reports whether it is known to be stopped. +func (v *VM) Stop(ctx context.Context) bool { return v.e.Stop(ctx, v.Name) } + +// Remove stops and deletes the VM with all that is known of it, and reports +// whether it is gone. +func (v *VM) Remove(ctx context.Context) bool { return v.e.Delete(ctx, v.Name) } + +// ResourcesLine says what the VM has. +func ResourcesLine(inst *vm.Instance) string { + return " Resources: CPUs: " + strconv.Itoa(inst.CPUs) + ", Memory: " + strconv.FormatInt(inst.Memory>>30, 10) + " GiB, Disk: " + strconv.FormatInt(inst.Disk>>30, 10) + " GiB" +} diff --git a/internal/env/project.go b/internal/env/project.go index 7e65c969..46e0dd84 100644 --- a/internal/env/project.go +++ b/internal/env/project.go @@ -114,13 +114,21 @@ func StripCR(content string) string { // it). The project's file is only in here when AGENT_VM_PROJECT_ENV puts it // outside the project; inside, the VM reads it itself (see ProjectRel). func Payload(shared, dir string) string { + return PayloadFor(shared, dir, ProjectEnvFile(dir)) +} + +// PayloadFor is Payload with the project's env file given, "" for none: a +// program that has no AGENT_VM_PROJECT_ENV to read gives it. +func PayloadFor(shared, dir, projectFile string) string { var b strings.Builder - if s, ok := readRegular(shared); ok { - // The newline keeps a shared file without one from gluing its last - // line to the project's first. - b.WriteString(StripCR(s) + "\n") + if shared != "" { + if s, ok := readRegular(shared); ok { + // The newline keeps a shared file without one from gluing its last + // line to the project's first. + b.WriteString(StripCR(s) + "\n") + } } - if pf := ProjectEnvFile(dir); !InProject(dir, pf) { + if pf := projectFile; pf != "" && !InProject(dir, pf) { if p, ok := readRegular(pf); ok { b.WriteString(StripCR(p)) } diff --git a/internal/guard/guard.go b/internal/guard/guard.go index bc5c13a6..8511c69f 100644 --- a/internal/guard/guard.go +++ b/internal/guard/guard.go @@ -10,6 +10,7 @@ import ( "context" "errors" "fmt" + "io" "io/fs" "os" "path" @@ -19,8 +20,8 @@ import ( "time" "unicode" - "github.com/lima-vm/sshocker/pkg/reversesshfs" "github.com/sylvinus/agent-vm/internal/desktop" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/reversesshfs" ) // FileName is the list's file in agent-vm's state folder: one path a line, @@ -119,6 +120,8 @@ var maxScan = 20000 // Guard asks about the writes to guarded paths. type Guard struct { + // App names the program in the question; "agent-vm" when empty. + App string // VM is named in the question. VM string // Paths, relative, matched anywhere in a written path as whole names, @@ -151,30 +154,52 @@ type rule struct { ext string } -// Load is the guard of the VM vm: Defaults and the user's list. -func Load(stateDir, vm string) (*Guard, error) { - g := &Guard{VM: vm, Paths: map[string]string{}, Ask: desktop.Ask, Tell: desktop.Notify} - // No one to ask (a server, CI): refused without a dialog. - if os.Getenv("AGENT_VM_GUARDED_WRITES") == "deny" { - g.Ask = func(context.Context, string, string) (bool, bool) { return false, true } - } +// Config is what a Guard is made of: the policy file of a VM holds it. +type Config struct { + // Paths are the guarded paths, see Guard.Paths. + Paths map[string]string `json:"paths"` + // Deny refuses what is guarded without a dialog: no one to ask (a + // server, CI). + Deny bool `json:"deny,omitempty"` +} + +// DefaultConfig guards Defaults, and WindowsDefaults on a Windows host. +func DefaultConfig() Config { + c := Config{Paths: map[string]string{}} for p, why := range Defaults { - g.Paths[p] = why + c.Paths[p] = why } if windowsHost { for p, why := range WindowsDefaults { - g.Paths[p] = why + c.Paths[p] = why } } + return c +} + +// LoadConfig is Defaults with the user's list on top, and +// AGENT_VM_GUARDED_WRITES. +func LoadConfig(stateDir string) (Config, error) { + c := DefaultConfig() + c.Deny = os.Getenv("AGENT_VM_GUARDED_WRITES") == "deny" f, err := os.Open(stateDir + "/" + FileName) if errors.Is(err, os.ErrNotExist) { - return g, nil + return c, nil } if err != nil { - return nil, err + return Config{}, err } defer f.Close() - sc := bufio.NewScanner(f) + return c, c.ParseList(f) +} + +// ParseList adds the lines of a guarded file to c: one path a line, "!path" +// removes one of the paths already there. +func (c *Config) ParseList(r io.Reader) error { + if c.Paths == nil { + c.Paths = map[string]string{} + } + sc := bufio.NewScanner(r) for sc.Scan() { line, _, _ := strings.Cut(sc.Text(), "#") line = strings.TrimSpace(line) @@ -182,18 +207,39 @@ func Load(stateDir, vm string) (*Guard, error) { continue } if p, ok := strings.CutPrefix(line, "!"); ok { - for k := range g.Paths { + for k := range c.Paths { if strings.EqualFold(k, clean(p)) { - delete(g.Paths, k) + delete(c.Paths, k) } } continue } - if _, ok := g.Paths[clean(line)]; !ok { - g.Paths[clean(line)] = "" + if _, ok := c.Paths[clean(line)]; !ok { + c.Paths[clean(line)] = "" } } - return g, sc.Err() + return sc.Err() +} + +// New is the guard of the VM vm for c. +func New(c Config, vm string) *Guard { + g := &Guard{VM: vm, Paths: c.Paths, Ask: desktop.Ask, Tell: desktop.Notify} + if g.Paths == nil { + g.Paths = map[string]string{} + } + if c.Deny { + g.Ask = func(context.Context, string, string) (bool, bool) { return false, true } + } + return g +} + +// Load is the guard of the VM vm: Defaults and the user's list. +func Load(stateDir, vm string) (*Guard, error) { + c, err := LoadConfig(stateDir) + if err != nil { + return nil, err + } + return New(c, vm), nil } func clean(p string) string { return strings.Trim(path.Clean("/"+strings.TrimSpace(p)), "/") } @@ -345,13 +391,20 @@ func (g *Guard) Allow(root, rel string, entry bool) bool { return allow } + app := g.App + if app == "" { + app = "agent-vm" + } why := g.Paths[h.rule] if h.kind == kindTooBig { - why = "It may hold a file agent-vm guards." + why = "It may hold a file " + app + " guards." } else if why == "" { - why = "You asked agent-vm to guard it (~/.agent-vm/" + FileName + ")." + why = "You asked " + app + " to guard it." + if app == "agent-vm" { + why = "You asked agent-vm to guard it (~/.agent-vm/" + FileName + ")." + } } - title := "agent-vm: " + g.VM + title := app + ": " + g.VM msg := question(root, rel, h, why) ctx, cancel := context.WithTimeout(context.Background(), answerWithin) defer cancel() diff --git a/internal/guard/guard_test.go b/internal/guard/guard_test.go index 80e8c3eb..e0911c36 100644 --- a/internal/guard/guard_test.go +++ b/internal/guard/guard_test.go @@ -65,6 +65,21 @@ func TestAllow(t *testing.T) { } // The user adds paths (Makefile) and removes defaults. +// The program is named in the question as it was opened, agent-vm by default. +func TestAppName(t *testing.T) { + for _, c := range []struct{ app, title, why string }{ + {"", "agent-vm: p", "You asked agent-vm to guard it (~/.agent-vm/guarded)."}, + {"mytool", "mytool: p", "You asked mytool to guard it."}, + } { + var title, msg string + g := &Guard{App: c.app, VM: "p", Paths: map[string]string{"Makefile": ""}, + Ask: func(_ context.Context, ti, m string) (bool, bool) { title, msg = ti, m; return true, true }} + if !g.Allow(t.TempDir(), "Makefile", false) || title != c.title || !strings.Contains(msg, c.why) { + t.Errorf("%q: %q %q", c.app, title, msg) + } + } +} + func TestList(t *testing.T) { a := &asker{answers: []bool{false}} g := a.guard(t, "# mine\nMakefile\n!.envrc\n /scripts/deploy.sh \n") diff --git a/internal/host/host.go b/internal/host/host.go index 39868879..dfdf727e 100644 --- a/internal/host/host.go +++ b/internal/host/host.go @@ -55,6 +55,19 @@ func share(total, floor int, warn io.Writer) int { // (nothing asked) and a host that cannot be read leave it alone: guessing // low would hand a 1-CPU VM to a 64-core host. func Cap(kind string, val int, warn io.Writer) int { + return capWith(func(total, floor int) int { return share(total, floor, warn) }, kind, val, warn) +} + +// CapShare is Cap with the divisor given (0: 2): for a program that reads no +// environment variable. +func CapShare(div int, kind string, val int, warn io.Writer) int { + if div <= 0 { + div = 2 + } + return capWith(func(total, floor int) int { return max(total/div, floor) }, kind, val, warn) +} + +func capWith(share func(total, floor int) int, kind string, val int, warn io.Writer) int { if val == 0 { return 0 } @@ -70,7 +83,7 @@ func Cap(kind string, val int, warn io.Writer) int { if total <= 0 { return val } - if s := share(total, floor, warn); val > s { + if s := share(total, floor); val > s { fmt.Fprintf(warn, "Note: --%s %d exceeds this host's share (%d detected); using %d.\n", kind, val, total, s) return s } diff --git a/internal/host/host_test.go b/internal/host/host_test.go index 544f07fa..dc75fd9c 100644 --- a/internal/host/host_test.go +++ b/internal/host/host_test.go @@ -47,3 +47,22 @@ func TestCapBash(t *testing.T) { t.Error("an unknown host clamped") } } + +// QEMU's package by the host's package manager, as the .deb and .rpm +// recommend it. +func TestQemuBinaryFor(t *testing.T) { + for _, c := range []struct { + goarch string + dnf bool + bin, cmd string + }{ + {"amd64", false, "qemu-system-x86_64", "sudo apt-get install qemu-system-x86"}, + {"arm64", false, "qemu-system-aarch64", "sudo apt-get install qemu-system-arm"}, + {"amd64", true, "qemu-system-x86_64", "sudo dnf install qemu-system-x86-core"}, + {"arm64", true, "qemu-system-aarch64", "sudo dnf install qemu-system-aarch64-core"}, + } { + if bin, cmd := qemuBinaryFor(c.goarch, c.dnf); bin != c.bin || cmd != c.cmd { + t.Errorf("%s dnf=%v: %s %q", c.goarch, c.dnf, bin, cmd) + } + } +} diff --git a/internal/host/prereqs.go b/internal/host/prereqs.go index 131a9c32..2c43cd7a 100644 --- a/internal/host/prereqs.go +++ b/internal/host/prereqs.go @@ -13,15 +13,26 @@ import ( ) // qemuBinary is QEMU's emulator for this host's architecture, and the -// Debian package that has it. -func qemuBinary() (bin, pkg string) { - switch runtime.GOARCH { +// command that installs it: dnf's package where there is dnf and no apt-get, +// else Debian's. +func qemuBinary() (bin, install string) { + _, aptErr := exec.LookPath("apt-get") + _, dnfErr := exec.LookPath("dnf") + return qemuBinaryFor(runtime.GOARCH, aptErr != nil && dnfErr == nil) +} + +func qemuBinaryFor(goarch string, dnf bool) (bin, install string) { + bin, deb, rpm := "qemu-system-"+goarch, "qemu-system", "qemu-system-"+goarch+"-core" + switch goarch { case "amd64": - return "qemu-system-x86_64", "qemu-system-x86" + bin, deb, rpm = "qemu-system-x86_64", "qemu-system-x86", "qemu-system-x86-core" case "arm64": - return "qemu-system-aarch64", "qemu-system-arm" + bin, deb, rpm = "qemu-system-aarch64", "qemu-system-arm", "qemu-system-aarch64-core" + } + if dnf { + return bin, "sudo dnf install " + rpm } - return "qemu-system-" + runtime.GOARCH, "qemu-system" + return bin, "sudo apt-get install " + deb } // WSLVersion is 2 or 1 under WSL, from the kernel release, 0 elsewhere. @@ -47,9 +58,9 @@ func LinuxPrereqs(w io.Writer) bool { return true } ok := true - bin, pkg := qemuBinary() + bin, install := qemuBinary() if _, err := exec.LookPath(bin); err != nil { - fmt.Fprintf(w, "Error: Lima needs '%s' on PATH (not found).\n Install with: sudo apt-get install %s\n", bin, pkg) + fmt.Fprintf(w, "Error: Lima needs '%s' on PATH (not found).\n Install with: %s\n", bin, install) ok = false } _, err := os.Stat("/dev/kvm") diff --git a/internal/limaembed/hostagent.go b/internal/limaembed/hostagent.go index ba3e2d45..417d5c29 100644 --- a/internal/limaembed/hostagent.go +++ b/internal/limaembed/hostagent.go @@ -24,9 +24,9 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/hostagent" - "github.com/lima-vm/lima/v2/pkg/hostagent/api/server" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/api/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func newHostagentCommand() *cobra.Command { diff --git a/internal/limaembed/limaembed.go b/internal/limaembed/limaembed.go index 8ca8a939..90ab2f42 100644 --- a/internal/limaembed/limaembed.go +++ b/internal/limaembed/limaembed.go @@ -8,27 +8,36 @@ import ( "path/filepath" "runtime" "strings" + "sync/atomic" "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/debugutil" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/envutil" - "github.com/lima-vm/lima/v2/pkg/hostagent/dns" - "github.com/lima-vm/lima/v2/pkg/networks/usernet" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/sshocker/pkg/reversesshfs" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/debugutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/envutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/dns" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/usernet" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/reversesshfs" "github.com/sylvinus/agent-vm/internal/guard" - "github.com/sylvinus/agent-vm/internal/netguard" + "github.com/sylvinus/agent-vm/internal/policy" "github.com/sylvinus/agent-vm/internal/state" ) +var called atomic.Bool + +// Called reports whether Run has run: the program looks for the commands +// Lima starts it for. +func Called() bool { return called.Load() } + // Run runs args (os.Args[1:]) when they are a command Lima's libraries start // this executable for, as limactl would, and reports whether they were. Such // a command never returns: it exits. func Run(args []string) bool { + called.Store(true) i := 0 for i < len(args) && args[i] == "--debug" { i++ @@ -42,30 +51,33 @@ func Run(args []string) bool { // Lima's own port forwarder, the one patched to never take a port a // program of the host serves; not the ssh one. os.Unsetenv("LIMA_SSH_PORT_FORWARDER") - // What the guest may reach (netguard): every connection it opens is made - // here, by the instance's netstack. An unreadable policy stops the VM - // rather than leave it open. - st, err := state.Default() - if err != nil { - logrus.Fatal(err) + // The policies the VM was booted with, in its folder: the folder of the + // VMs is the one the process that started this one gave Lima, whatever + // state folder that program keeps. The instance is the last argument. An + // unreadable policy stops the VM rather than leave it open. + vms, err := dirnames.InstancesDir() + if err != nil || vms == "" { + logrus.Fatalf("no folder for the VMs ($%s): %v", dirnames.InstancesEnv, err) } - policy, err := netguard.Load(string(st)) + vmName := args[len(args)-1] + pf, err := policy.Read(filepath.Join(vms, vmName, state.PolicyFile)) if err != nil { logrus.Fatal(err) } - usernet.Outbound = policy.Allows - if len(policy.Domains) > 0 { + // What the guest may reach (netguard): every connection it opens is made + // here, by the instance's netstack. + net := pf.Network + usernet.Outbound = net.Allows + if len(net.Domains) > 0 { // Both resolvers the guest can reach: the gateway's, which every // query gets whatever the guest's iptables say, and the hostagent's. - usernet.Names, usernet.Answered = policy.Name, policy.Answered - dns.Names, dns.Answered = policy.Name, policy.Answered + usernet.Names, usernet.Answered = net.Name, net.Answered + dns.Names, dns.Answered = net.Name, net.Answered } // The user is asked before the VM changes a file their machine runs on // its own (guard): the shares' SFTP server runs here. - g, err := guard.Load(string(st), args[len(args)-1]) - if err != nil { - logrus.Fatal(err) - } + g := guard.New(pf.Guarded, vmName) + g.App = pf.App reversesshfs.Guard = g.Allow root := &cobra.Command{ Use: "agent-vm", @@ -83,7 +95,7 @@ func Run(args []string) bool { root.SetArgs(args) // Its events, as limactl's, and a notice for each port it forwards. The // instance is the last argument. - root.SetOut(newEventWatch(os.Stdout, args[len(args)-1])) + root.SetOut(newEventWatch(os.Stdout, pf.App, vmName)) err = root.Execute() server.Disconnect() osutil.HandleExitError(err) diff --git a/internal/limaembed/notify.go b/internal/limaembed/notify.go index 3e55145e..0b905982 100644 --- a/internal/limaembed/notify.go +++ b/internal/limaembed/notify.go @@ -9,7 +9,7 @@ import ( "os" "sync" - "github.com/lima-vm/lima/v2/pkg/hostagent/events" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/events" "github.com/sylvinus/agent-vm/internal/desktop" ) @@ -20,6 +20,7 @@ import ( // surprise one should not go unseen. type eventWatch struct { w io.Writer + app string // the program, named in what the user is shown vm string notify func(title, msg string) @@ -27,12 +28,15 @@ type eventWatch struct { partial []byte } -func newEventWatch(w io.Writer, vm string) *eventWatch { +func newEventWatch(w io.Writer, app, vm string) *eventWatch { n := desktop.Notify if os.Getenv("AGENT_VM_NOTIFY") == "0" { n = func(string, string) {} } - return &eventWatch{w: w, vm: vm, notify: n} + if app == "" { + app = "agent-vm" + } + return &eventWatch{w: w, app: app, vm: vm, notify: n} } func (e *eventWatch) Write(p []byte) (int, error) { @@ -62,8 +66,8 @@ func (e *eventWatch) event(line []byte) { } switch pf.Type { case events.PortForwardEventForwarding: - e.notify("agent-vm: "+e.vm, fmt.Sprintf("The VM now serves %s on this machine.", pf.HostAddr)) + e.notify(e.app+": "+e.vm, fmt.Sprintf("The VM now serves %s on this machine.", pf.HostAddr)) case events.PortForwardEventFailed: - e.notify("agent-vm: "+e.vm, fmt.Sprintf("Not forwarded: %s is in use on this machine.", pf.HostAddr)) + e.notify(e.app+": "+e.vm, fmt.Sprintf("Not forwarded: %s is in use on this machine.", pf.HostAddr)) } } diff --git a/internal/limaembed/notify_test.go b/internal/limaembed/notify_test.go index 50c3d99a..8af5112a 100644 --- a/internal/limaembed/notify_test.go +++ b/internal/limaembed/notify_test.go @@ -11,7 +11,7 @@ import ( func TestEventWatch(t *testing.T) { var out bytes.Buffer var told []string - e := newEventWatch(&out, "proj-1a2b3c4d") + e := newEventWatch(&out, "", "proj-1a2b3c4d") e.notify = func(title, msg string) { told = append(told, title+": "+msg) } in := `{"status":{"running":true}} {"status":{"portForward":{"type":"forwarding","protocol":"tcp","guestAddr":"127.0.0.1:5173","hostAddr":"127.0.0.1:5173"}}} diff --git a/internal/limaembed/share.go b/internal/limaembed/share.go index fde48e1b..268c9f50 100644 --- a/internal/limaembed/share.go +++ b/internal/limaembed/share.go @@ -9,14 +9,23 @@ import ( "os" "path/filepath" "runtime" - "strings" ) // What Lima finds in /share/lima next to limactl: the guest agent and // the templates. Put in by the Makefile; see assets/README. // //go:embed all:assets -var assets embed.FS +var embedded embed.FS + +// Embedded is what agent-vm's own build embeds: the guest agent for this +// architecture, gzipped, and the templates, at the root. +func Embedded() fs.FS { + sub, err := fs.Sub(embedded, "assets") + if err != nil { + return embedded + } + return sub +} // Share is Lima's share/lima, written out of the binary. type Share struct { @@ -32,24 +41,32 @@ func (s *Share) Templates() string { return filepath.Join(s.Dir, "templates") } -// ErrNoAssets is returned by WriteShare when agent-vm was built without -// the guest agent (`go build` instead of `make`). -var ErrNoAssets = errors.New("this agent-vm was built without Lima's guest agent: build it with make") +// ErrNoAssets is returned by WriteShare when there is no guest agent: agent-vm +// was built without it (`go build` instead of `make`), or the program that +// embeds the library gave none (Options.Assets). +var ErrNoAssets = errors.New("no Lima guest agent in the assets: build agent-vm with make, or give Options.Assets the output of cmd/agent-vm-assets") -// WriteShare writes the embedded files into dir/share/lima, each one that is -// missing or differs from the embedded copy, and returns where they are. The -// files are checked on every call: a damaged guest agent never reaches a VM. -func WriteShare(dir string) (*Share, error) { +// WriteShare writes the files of assets (Embedded when nil) into +// dir/share/lima, each one that is missing or differs from the copy, and +// returns where they are. The files are checked on every call: a damaged +// guest agent never reaches a VM. +func WriteShare(dir string, assets fs.FS) (*Share, error) { + if assets == nil { + assets = Embedded() + } agent := "lima-guestagent.Linux-" + guestArch() + ".gz" - if _, err := fs.Stat(assets, "assets/"+agent); err != nil { + if _, err := fs.Stat(assets, agent); err != nil { return nil, ErrNoAssets } share := filepath.Join(dir, "share", "lima") - err := fs.WalkDir(assets, "assets", func(p string, d fs.DirEntry, err error) error { + err := fs.WalkDir(assets, ".", func(p string, d fs.DirEntry, err error) error { if err != nil { return err } - rel := strings.TrimPrefix(strings.TrimPrefix(p, "assets"), "/") + rel := p + if p == "." { + rel = "" + } if rel == "README" { return nil } @@ -57,7 +74,7 @@ func WriteShare(dir string) (*Share, error) { if d.IsDir() { return os.MkdirAll(dst, 0o700) } - want, err := assets.ReadFile(p) + want, err := fs.ReadFile(assets, p) if err != nil { return err } diff --git a/internal/mounts/build.go b/internal/mounts/build.go index 9ee87bb6..d89629ea 100644 --- a/internal/mounts/build.go +++ b/internal/mounts/build.go @@ -33,16 +33,17 @@ func CacheOn() bool { return os.Getenv("AGENT_VM_SSHFS_CACHE") == "1" } // Shares is what Build is given. type Shares struct { - VM string Dir string // the project Writable bool // false for --readonly, every share // Names are read-only on every share (none with .git left writable). // Every share is served by the builtin SFTP server: the one confined to // its folder (vm.Confined). - Names []string - Entries []Entry // see Entries - StateDir string // where single files are staged - Warn io.Writer + Names []string + Entries []Entry // see Entries + FileMounts string // where this VM's single files are staged + // Cache turns sshfs's attribute cache on (see CacheOn). + Cache bool + Warn io.Writer } // Build is the shares of the project's VM: the project first, then the @@ -52,7 +53,7 @@ func Build(s Shares) ([]vm.Mount, []FileMount) { w := func(format string, a ...any) { fmt.Fprintf(s.Warn, format+"\n", a...) } opts := func(writable bool) *vm.SSHFS { o := &vm.SSHFS{SFTPDriver: "builtin", ReadonlyNames: append([]string(nil), s.Names...)} - if writable && !CacheOn() { + if writable && !s.Cache { off := false o.Cache = &off } @@ -75,7 +76,7 @@ func Build(s Shares) ([]vm.Mount, []FileMount) { w("Warning: Mount entry '%s' (from ~/.agent-vm/volumes) requests rw on a file; only directories support rw. Mount the parent directory instead. Skipping.", e.Line) continue } - dir := paths.Host(filepath.Join(s.StateDir, "file-mounts", s.VM, strconv.Itoa(idx))) + dir := paths.Host(filepath.Join(s.FileMounts, strconv.Itoa(idx))) staged := dir + "/" + filepath.Base(e.Src) if !Stage(e.Src, staged, s.Warn) { w("Warning: Failed to stage '%s', skipping.", e.Src) diff --git a/internal/mounts/build_test.go b/internal/mounts/build_test.go index d3ac7bd2..d1ad3643 100644 --- a/internal/mounts/build_test.go +++ b/internal/mounts/build_test.go @@ -30,8 +30,8 @@ func TestBuildBash(t *testing.T) { t.Setenv("AGENT_VM_SSHFS_CACHE", cache) var warn bytes.Buffer mounts, files := Build(Shares{ - VM: "t-00000000", Dir: gs.proj, Writable: writable, Names: []string{".git", ".hg"}, - Entries: Entries(filepath.Join(gs.state, "volumes"), gs.proj, gs.home, gs.refs(), &warn), StateDir: gs.state, Warn: &warn, + Dir: gs.proj, Writable: writable, Names: []string{".git", ".hg"}, + Entries: Entries(filepath.Join(gs.state, "volumes"), gs.proj, gs.home, gs.refs(), &warn), FileMounts: filepath.Join(gs.state, "vms", "t-00000000", "file-mounts"), Cache: CacheOn(), Warn: &warn, }) w := "true" if !writable { @@ -44,10 +44,14 @@ func TestBuildBash(t *testing.T) { if err := json.Unmarshal([]byte(jsonPart), &want); err != nil { t.Fatalf("bash JSON %q: %v", jsonPart, err) } + // 0.2 staged under file-mounts/, 0.4 under vms//file-mounts. + staged := func(s string) string { + return strings.ReplaceAll(s, "/vms/t-00000000/file-mounts/", "/file-mounts/t-00000000/") + } norm := func(ms []vm.Mount, root string) []vm.Mount { var out []vm.Mount for _, m := range ms { - m.Location = strings.ReplaceAll(m.Location, root, "ROOT") + m.Location = staged(strings.ReplaceAll(m.Location, root, "ROOT")) m.MountPoint = strings.ReplaceAll(m.MountPoint, root, "ROOT") out = append(out, m) } @@ -60,7 +64,7 @@ func TestBuildBash(t *testing.T) { } var lines []string for _, f := range files { - lines = append(lines, strings.ReplaceAll(f.Line(), gs.root, "ROOT")) + lines = append(lines, staged(strings.ReplaceAll(f.Line(), gs.root, "ROOT"))) } if strings.Join(lines, "\n") != strings.TrimSuffix(strings.ReplaceAll(cached, bs.root, "ROOT"), "\n") { t.Errorf("file mounts: %q, bash %q", lines, cached) @@ -68,7 +72,7 @@ func TestBuildBash(t *testing.T) { if strings.ReplaceAll(warn.String(), gs.root, "ROOT") != strings.ReplaceAll(r.Stderr, bs.root, "ROOT") { t.Errorf("warnings: %q, bash %q", warn.String(), r.Stderr) } - if fi, err := os.Stat(gs.state + "/file-mounts/t-00000000/0/file.txt"); err != nil || !fi.Mode().IsRegular() { + if fi, err := os.Stat(gs.state + "/vms/t-00000000/file-mounts/0/file.txt"); err != nil || !fi.Mode().IsRegular() { t.Error("the file was not staged") } } diff --git a/internal/mounts/windows_test.go b/internal/mounts/windows_test.go index ad4861c4..d1e02eb0 100644 --- a/internal/mounts/windows_test.go +++ b/internal/mounts/windows_test.go @@ -75,7 +75,7 @@ func TestWindowsVolumes(t *testing.T) { t.Errorf("warnings: %s", warn.String()) } - ms, files := Build(Shares{VM: "v", Dir: "C:/Users/me/proj", Writable: true, Entries: es, StateDir: "C:/st", Warn: &warn}) + ms, files := Build(Shares{Dir: "C:/Users/me/proj", Writable: true, Entries: es, FileMounts: "C:/st/vms/v/file-mounts", Warn: &warn}) var mounts []string for _, m := range ms[:len(ms)-1] { mounts = append(mounts, m.Location+">"+m.MountPoint) @@ -83,7 +83,7 @@ func TestWindowsVolumes(t *testing.T) { if want := "C:/Users/me/proj>/c/Users/me/proj C:/data>/c/data C:/data2>/mnt/d2 C:/data3>/c/data3 C:/data4>/c/Users/me/proj/sub C:/data6>/mnt/d6"; strings.Join(mounts, " ") != want { t.Errorf("mounts:\n got %s\n want %s", strings.Join(mounts, " "), want) } - if len(files) != 1 || files[0].BindDst != "/c/f.txt" || !strings.HasPrefix(files[0].Staging, "C:/st/file-mounts/v/") || ms[len(ms)-1].MountPoint != "/tmp/.agent-vm-file-mounts/0" { + if len(files) != 1 || files[0].BindDst != "/c/f.txt" || !strings.HasPrefix(files[0].Staging, "C:/st/vms/v/file-mounts/") || ms[len(ms)-1].MountPoint != "/tmp/.agent-vm-file-mounts/0" { t.Errorf("single file: %+v %+v", files, ms[len(ms)-1]) } } diff --git a/internal/netguard/netguard.go b/internal/netguard/netguard.go index 51e75a17..4e87a79b 100644 --- a/internal/netguard/netguard.go +++ b/internal/netguard/netguard.go @@ -9,6 +9,7 @@ import ( "bufio" "errors" "fmt" + "io" "net" "net/netip" "os" @@ -29,14 +30,14 @@ const resolvedFor = 30 * time.Minute // internet only. type Policy struct { // Open turns isolation off (AGENT_VM_UNSAFE_OPEN_NETWORK=1). - Open bool + Open bool `json:"open,omitempty"` // Allow are reachable whatever else: an address or network, every port // when Port is 0. - Allow []Rule + Allow []Rule `json:"allow,omitempty"` // Domains, when any, are the only names of the internet the VMs may // reach, subdomains included: an address is reachable once agent-vm's // DNS gave it for one of them. - Domains []string + Domains []string `json:"domains,omitempty"` mu sync.Mutex resolved map[netip.Addr]time.Time @@ -46,23 +47,35 @@ type Policy struct { // Rule is an allow line: a network, and a port or 0 for every one. type Rule struct { - Net netip.Prefix - Port uint16 + Net netip.Prefix `json:"net"` + Port uint16 `json:"port,omitempty"` } // Load reads the policy from the state folder's network file (none: the // default), and AGENT_VM_UNSAFE_OPEN_NETWORK. func Load(stateDir string) (*Policy, error) { - p := &Policy{Open: os.Getenv("AGENT_VM_UNSAFE_OPEN_NETWORK") == "1"} + open := os.Getenv("AGENT_VM_UNSAFE_OPEN_NETWORK") == "1" f, err := os.Open(stateDir + "/" + FileName) if errors.Is(err, os.ErrNotExist) { - return p, nil + return &Policy{Open: open}, nil } if err != nil { return nil, err } defer f.Close() - sc := bufio.NewScanner(f) + p, err := Parse(f, stateDir+"/"+FileName) + if err != nil { + return nil, err + } + p.Open = open + return p, nil +} + +// Parse reads the lines of a network file; name is the file's, for the +// messages. +func Parse(r io.Reader, name string) (*Policy, error) { + p := &Policy{} + sc := bufio.NewScanner(r) for n := 1; sc.Scan(); n++ { line, _, _ := strings.Cut(sc.Text(), "#") fields := strings.Fields(line) @@ -70,23 +83,23 @@ func Load(stateDir string) (*Policy, error) { continue } if len(fields) != 2 { - return nil, fmt.Errorf("%s/%s, line %d: want 'allow
' or 'domain '", stateDir, FileName, n) + return nil, fmt.Errorf("%s, line %d: want 'allow
' or 'domain '", name, n) } switch fields[0] { case "allow": rules, err := parseAllow(fields[1]) if err != nil { - return nil, fmt.Errorf("%s/%s, line %d: %v", stateDir, FileName, n, err) + return nil, fmt.Errorf("%s, line %d: %v", name, n, err) } p.Allow = append(p.Allow, rules...) case "domain": d := strings.ToLower(strings.TrimSuffix(strings.TrimPrefix(fields[1], "*."), ".")) if d == "" || strings.ContainsAny(d, "/:*") { - return nil, fmt.Errorf("%s/%s, line %d: %q is not a domain name", stateDir, FileName, n, fields[1]) + return nil, fmt.Errorf("%s, line %d: %q is not a domain name", name, n, fields[1]) } p.Domains = append(p.Domains, d) default: - return nil, fmt.Errorf("%s/%s, line %d: unknown %q (allow, domain)", stateDir, FileName, n, fields[0]) + return nil, fmt.Errorf("%s, line %d: unknown %q (allow, domain)", name, n, fields[0]) } } return p, sc.Err() diff --git a/internal/policy/policy.go b/internal/policy/policy.go new file mode 100644 index 00000000..70d3742e --- /dev/null +++ b/internal/policy/policy.go @@ -0,0 +1,131 @@ +// Package policy is what a VM boots with, in vms//policy.json: the +// network rules and the guarded files. Lima starts agent-vm again as the +// VM's hostagent, where both are enforced; the file is how they reach it, and +// the record of what the VM booted with. +package policy + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "sort" + + "crypto/sha256" + "encoding/hex" + + "github.com/sylvinus/agent-vm/internal/guard" + "github.com/sylvinus/agent-vm/internal/netguard" +) + +// SchemaVersion is the version of the file this agent-vm reads and writes. +const SchemaVersion = 1 + +// The policies as values. The zero Network lets a VM reach the internet and +// nothing of this machine; the zero Guarded guards nothing (DefaultGuarded is +// agent-vm's list). +type ( + Network = netguard.Policy + Rule = netguard.Rule + Guarded = guard.Config +) + +// DefaultGuarded is agent-vm's list of files the host runs, which the VM may +// not write unasked. +func DefaultGuarded() Guarded { return guard.DefaultConfig() } + +// New is a File for the policies given. +func New(app string, n *Network, g Guarded) *File { + if n == nil { + n = &Network{} + } + return &File{Schema: SchemaVersion, App: app, Network: n, Guarded: g} +} + +// File is the policy file. +type File struct { + Schema int `json:"schema"` + // App names the program in what the user is shown. + App string `json:"app,omitempty"` + Network *netguard.Policy `json:"network"` + Guarded guard.Config `json:"guarded"` +} + +// Current is the policies in force now, from agent-vm's files in the state +// folder and the environment: an unreadable file is an error. +func Current(stateDir, app string) (*File, error) { + n, err := netguard.Load(stateDir) + if err != nil { + return nil, err + } + g, err := guard.LoadConfig(stateDir) + if err != nil { + return nil, err + } + return New(app, n, g), nil +} + +// Write replaces the file at path, whole or not at all. +func Write(path string, f *File) error { + b, err := json.MarshalIndent(f, "", " ") + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + tmp, err := os.CreateTemp(filepath.Dir(path), ".tmp-policy-*") + if err != nil { + return err + } + _, werr := tmp.Write(append(b, '\n')) + if err := errors.Join(werr, tmp.Chmod(0o644), tmp.Close()); err != nil { + os.Remove(tmp.Name()) + return err + } + if err := os.Rename(tmp.Name(), path); err != nil { + os.Remove(tmp.Name()) + return err + } + return nil +} + +// Read is the file at path. A hostagent that cannot read its policy must not +// run the VM open: the error stops it. +func Read(path string) (*File, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var f File + if err := json.Unmarshal(b, &f); err != nil { + return nil, fmt.Errorf("%s: %w", path, err) + } + if f.Schema != SchemaVersion { + return nil, fmt.Errorf("%s: schema %d, this agent-vm reads %d", path, f.Schema, SchemaVersion) + } + if f.Network == nil { + return nil, fmt.Errorf("%s: no network policy", path) + } + return &f, nil +} + +// Fingerprint identifies the policies, the order of the rules and the +// comments of the files they came from not counting. +func (f *File) Fingerprint() string { + c := *f + n := &netguard.Policy{Open: f.Network.Open, Domains: append([]string(nil), f.Network.Domains...), Allow: append([]netguard.Rule(nil), f.Network.Allow...)} + sort.Strings(n.Domains) + sort.Slice(n.Allow, func(i, j int) bool { + a, b := n.Allow[i], n.Allow[j] + if a.Net.String() != b.Net.String() { + return a.Net.String() < b.Net.String() + } + return a.Port < b.Port + }) + c.Network = n + b, _ := json.Marshal(c) + sum := sha256.Sum256(b) + return hex.EncodeToString(sum[:]) +} diff --git a/internal/policy/policy_test.go b/internal/policy/policy_test.go new file mode 100644 index 00000000..d631ca2f --- /dev/null +++ b/internal/policy/policy_test.go @@ -0,0 +1,95 @@ +package policy + +import ( + "net/netip" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestCurrentWriteRead(t *testing.T) { + st := t.TempDir() + os.WriteFile(filepath.Join(st, "network"), []byte("allow 10.0.0.5:8080 # a\nallow 192.168.1.0/24\nallow localhost:11434\ndomain Example.com\n"), 0o644) + os.WriteFile(filepath.Join(st, "guarded"), []byte("Makefile\n!.envrc\n"), 0o644) + t.Setenv("AGENT_VM_GUARDED_WRITES", "deny") + t.Setenv("AGENT_VM_UNSAFE_OPEN_NETWORK", "") + + f, err := Current(st, "agent-vm") + if err != nil { + t.Fatal(err) + } + path := filepath.Join(t.TempDir(), "vm", "policy.json") + if err := Write(path, f); err != nil { + t.Fatal(err) + } + if ents, _ := os.ReadDir(filepath.Dir(path)); len(ents) != 1 { + t.Errorf("left behind: %v", ents) + } + got, err := Read(path) + if err != nil { + t.Fatal(err) + } + if got.Fingerprint() != f.Fingerprint() || got.App != "agent-vm" || !got.Guarded.Deny { + t.Errorf("round trip: %+v", got) + } + // What the hostagent decides with is what was written. + if !got.Network.Allows("tcp", netip.MustParseAddrPort("10.0.0.5:8080")) || got.Network.Allows("tcp", netip.MustParseAddrPort("10.0.0.5:22")) || + !got.Network.Allows("tcp", netip.MustParseAddrPort("127.0.0.1:11434")) || !got.Network.Name("a.example.com") || got.Network.Name("other.org") { + t.Errorf("network: %+v", got.Network) + } + if _, ok := got.Guarded.Paths["Makefile"]; !ok { + t.Error("user's guarded path lost") + } + if _, ok := got.Guarded.Paths[".envrc"]; ok { + t.Error("removed default kept") + } + if _, ok := got.Guarded.Paths[".vscode/tasks.json"]; !ok { + t.Error("default lost") + } +} + +func TestFingerprint(t *testing.T) { + st := t.TempDir() + write := func(network string) string { + os.WriteFile(filepath.Join(st, "network"), []byte(network), 0o644) + f, err := Current(st, "agent-vm") + if err != nil { + t.Fatal(err) + } + return f.Fingerprint() + } + a := write("allow 10.0.0.1\nallow 10.0.0.2\n") + if write("# c\nallow 10.0.0.2\nallow 10.0.0.1 # x\n") != a { + t.Error("order or comments changed it") + } + if write("allow 10.0.0.1\n") == a { + t.Error("a rule less, the same") + } + if write("allow 10.0.0.1\nallow 10.0.0.2\ndomain a.org\n") == a { + t.Error("a domain more, the same") + } + t.Setenv("AGENT_VM_UNSAFE_OPEN_NETWORK", "1") + if write("allow 10.0.0.1\nallow 10.0.0.2\n") == a { + t.Error("open network, the same") + } +} + +func TestReadRefuses(t *testing.T) { + dir := t.TempDir() + p := filepath.Join(dir, "policy.json") + if _, err := Read(p); !os.IsNotExist(err) { + t.Errorf("missing: %v", err) + } + for body, want := range map[string]string{ + "{": "policy.json", + `{"schema":2,"network":{}}`: "schema 2", + `{"schema":1}`: "no network policy", + `{"schema":1,"network":{"allow":[{"net":"nope"}]}}`: "policy.json", + } { + os.WriteFile(p, []byte(body), 0o644) + if _, err := Read(p); err == nil || !strings.Contains(err.Error(), want) { + t.Errorf("%s: %v", body, err) + } + } +} diff --git a/internal/state/layout.go b/internal/state/layout.go new file mode 100644 index 00000000..0704cfab --- /dev/null +++ b/internal/state/layout.go @@ -0,0 +1,333 @@ +package state + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" +) + +// 0.3.0 kept its VMs in lima/ and a marker file per VM and fact in the +// state folder, named as 0.2 named them. MigrateLayout moves them to +// vms/; 0.2's own migration still names its markers so. +const ( + legacyVersionOf = ".agent-vm-version-" // the base's build time it was cloned from + legacyBuiltByOf = ".agent-vm-built-by-" // the agent-vm version that built that base + legacySSHFSOf = ".agent-vm-sshfs-" // 0.1 migration done + legacyTermOf = ".agent-vm-term-" // the $TERM whose terminfo it has + legacyFileMountsOf = ".agent-vm-file-mounts-" // single files of ~/.agent-vm/volumes + legacyMountsOf = ".agent-vm-mounts-" // 0.2's record of its shares + legacyCacheOf = ".agent-vm-sshfs-cache-" // 0.2's record of sshfs's cache + legacyScratchOf = ".agent-vm-scratch-" // a --scratch run, with its pid + legacyPolicyOf = ".agent-vm-policy-" // fingerprint of the policies it booted with + legacyBaseVersion = ".agent-vm-base-version" + legacyBaseBuiltBy = ".agent-vm-base-built-by" +) + +// legacyKinds, longest first: sshfs-cache- is also sshfs- followed by a name. +var legacyKinds = []string{ + legacyCacheOf, legacyFileMountsOf, legacyVersionOf, legacyBuiltByOf, legacyScratchOf, + legacyMountsOf, legacyPolicyOf, legacySSHFSOf, legacyTermOf, +} + +// LegacyRename moves the 0.3.0-layout markers and staged files of the VM +// from to its new name to: for 0.2's migration, which runs before +// MigrateLayout. Its shares are set again before it next boots, from where +// they now are. +func (d Dir) LegacyRename(from, to string) error { + var last error + for _, kind := range legacyKinds { + if err := os.Rename(d.Path(kind+from), d.Path(kind+to)); err != nil && !os.IsNotExist(err) { + last = err + } + } + if err := os.Rename(d.Path(filepath.Join("file-mounts", from)), d.Path(filepath.Join("file-mounts", to))); err != nil && !os.IsNotExist(err) { + last = err + } + return last +} + +// LayoutOps are what MigrateLayout asks of the caller. +type LayoutOps struct { + // Home is the Lima home the VMs are in: lima/ of the state folder, or + // where AGENT_VM_LIMA_HOME put it. + Home string + // Template is the base template's VM name. + Template string + // Running reports whether the VM name, still in lima/, runs. Its + // hostagent holds sockets in its folder: it cannot move. + Running func(name string) bool + // Stop stops the VM name in lima/. + Stop func(name string) error + // Ask asks whether to stop the running VMs, which cuts their sessions. + // Nil when there is no one to ask: they are left for next time. + Ask func(running []string) bool + // Log gets what was done and what was not. + Log io.Writer +} + +// LegacyVMs are the VMs still in the Lima home home, those with a lima.yaml. +func (d Dir) LegacyVMs(home string) []string { + ents, _ := os.ReadDir(home) + var out []string + for _, e := range ents { + if !e.IsDir() || strings.HasPrefix(e.Name(), ".") || strings.HasPrefix(e.Name(), "_") { + continue + } + if _, err := os.Stat(filepath.Join(home, e.Name(), "lima.yaml")); err == nil { + out = append(out, e.Name()) + } + } + return out +} + +// legacyMarkers are the markers in the state folder, by VM name, each a +// kind and its file. Those of the base template are under template. +func (d Dir) legacyMarkers(template string, known func(string) bool) map[string]map[string]string { + ents, _ := os.ReadDir(string(d)) + out := map[string]map[string]string{} + add := func(vm, kind, file string) { + if out[vm] == nil { + out[vm] = map[string]string{} + } + out[vm][kind] = file + } + for _, e := range ents { + n := e.Name() + switch { + case n == legacyBaseVersion || n == legacyBaseBuiltBy: + add(template, n, n) + continue + case !strings.HasPrefix(n, ".agent-vm-") || e.IsDir(): + continue + } + for _, k := range legacyKinds { + vm, ok := strings.CutPrefix(n, k) + if !ok || vm == "" { + continue + } + // sshfs-cache-x is the cache record of x, unless a VM is named + // cache-x: then it is the sshfs record of that one. + if k == legacyCacheOf && known("cache-"+vm) && !known(vm) { + k, vm = legacySSHFSOf, "cache-"+vm + } + add(vm, k, n) + break + } + } + return out +} + +// PendingLayout reports whether MigrateLayout has anything to move: VMs in +// the Lima home home, or markers of the old layout. +func (d Dir) PendingLayout(home string) bool { + if len(d.LegacyVMs(home)) > 0 { + return true + } + ents, _ := os.ReadDir(string(d)) + for _, e := range ents { + n := e.Name() + if n == legacyBaseVersion || n == legacyBaseBuiltBy { + return true + } + for _, k := range legacyKinds { + if strings.HasPrefix(n, k) && !e.IsDir() { + return true + } + } + } + _, err := os.Stat(d.Path(FileMountsDir)) + return err == nil +} + +// MigrateLayout moves 0.3.0's VMs to vms//lima, builds each one's +// metadata.json from its markers, moves its staged files, and removes the +// markers. A VM still running cannot move: it is asked to stop when Ask +// says so, else left, with its markers, for next time. The folders are +// renamed: a copy would break the disk blocks shared with the base. Run +// under the lock of the migration; interrupted, it picks up where it +// stopped. +func (d Dir) MigrateLayout(ops LayoutOps) error { + logf := func(format string, a ...any) { + if ops.Log != nil { + fmt.Fprintf(ops.Log, format+"\n", a...) + } + } + pending := d.LegacyVMs(ops.Home) + var running []string + for _, name := range pending { + if ops.Running != nil && ops.Running(name) { + running = append(running, name) + } + } + left := map[string]bool{} + if len(running) > 0 { + if ops.Ask != nil && ops.Ask(running) { + for _, name := range running { + if err := ops.Stop(name); err != nil { + logf("Could not stop %s: %v. It stays where it is, for next time.", name, err) + left[name] = true + } + } + } else { + for _, name := range running { + logf("%s is running and stays where it is: stop it, or run agent-vm in a terminal to be asked. It moves next time.", name) + left[name] = true + } + } + } + var failed []string + for _, name := range pending { + if left[name] { + continue + } + src, dst := filepath.Join(ops.Home, name), d.Lima(name) + if _, err := os.Lstat(dst); err == nil { + logf("Error: %s exists already: the VM in %s is not moved. Remove one of them.", dst, src) + failed = append(failed, name) + left[name] = true + continue + } + if err := os.MkdirAll(d.VM(name), 0o755); err != nil { + return err + } + if err := os.Rename(src, dst); err != nil { + var le *os.LinkError + if errors.As(err, &le) && errors.Is(le.Err, syscall.EXDEV) { + return fmt.Errorf("cannot move %s to %s: they are on different file systems (the Lima home %s and the state folder must be on one), and a copy would not share the disk blocks of the base template", src, dst, ops.Home) + } + return fmt.Errorf("moving %s to %s: %w", src, dst, err) + } + logf("Moved %s.", name) + } + // The markers: a VM in vms/ gets its metadata from them, one that is + // nowhere loses them. The VMs left behind keep theirs. + known := func(n string) bool { return d.HasLima(n) || slicesContains(pending, n) } + for vm, files := range d.legacyMarkers(ops.Template, known) { + if left[vm] { + continue + } + if !d.HasLima(vm) { + for _, f := range files { + os.Remove(d.Path(f)) + } + continue + } + if err := d.migrateMarkers(vm, ops.Template, files); err != nil { + logf("Error: could not write the metadata of %s: %v", vm, err) + failed = append(failed, vm) + continue + } + for _, f := range files { + if err := os.Remove(d.Path(f)); err != nil && !os.IsNotExist(err) { + logf("Warning: could not remove %s: %v", d.Path(f), err) + } + } + } + // Staged files: of the VMs there are, the rest is gone with their VM. + if ents, err := os.ReadDir(d.Path(FileMountsDir)); err == nil { + for _, e := range ents { + src := filepath.Join(d.Path(FileMountsDir), e.Name()) + if left[e.Name()] || slicesContains(failed, e.Name()) { + continue + } + if !d.HasLima(e.Name()) { + os.RemoveAll(src) + continue + } + if _, err := os.Lstat(d.FileMounts(e.Name())); err == nil { + os.RemoveAll(src) + } else if err := os.Rename(src, d.FileMounts(e.Name())); err != nil { + return fmt.Errorf("moving %s to %s: %w", src, d.FileMounts(e.Name()), err) + } + } + os.Remove(d.Path(FileMountsDir)) // when empty + } + if len(failed) > 0 { + return fmt.Errorf("could not move: %s", strings.Join(failed, ", ")) + } + return nil +} + +func slicesContains(s []string, v string) bool { + for _, x := range s { + if x == v { + return true + } + } + return false +} + +// migrateMarkers writes vm's metadata from its markers. +func (d Dir) migrateMarkers(vm, template string, files map[string]string) error { + read := func(kind string) (string, bool) { + f, ok := files[kind] + if !ok { + return "", false + } + b, err := os.ReadFile(d.Path(f)) + return strings.TrimSpace(string(b)), err == nil + } + var m Metadata + var base Base + haveBase := false + if v, ok := read(legacyBaseVersion); ok { + haveBase = true + base.Name = template + base.Built = parseBuilt(v) + if by, ok := read(legacyBaseBuiltBy); ok && builtByRe.MatchString(by) { + base.BuiltBy = by + } + } + if v, ok := read(legacyVersionOf); ok { + haveBase = true + base.Name = template + base.Built = parseBuilt(v) + } + if by, ok := read(legacyBuiltByOf); ok && builtByRe.MatchString(by) { + haveBase = true + base.Name = template + base.BuiltBy = by + } + if _, ok := read(legacySSHFSOf); ok && base.BuiltBy == "" && vm != template { + haveBase = true + base.Name = template + base.BuiltBy = "0.1.0" + } + if haveBase { + m.Base = &base + } + if t, ok := read(legacyTermOf); ok { + m.Term = t + } + if p, ok := read(legacyScratchOf); ok { + // A record without a process number is kept as one: the run it + // belonged to is not known. + if n, err := strconv.Atoi(p); err == nil && n > 0 { + m.ScratchPID = n + } else { + m.ScratchPID = -1 + } + } + if f, ok := read(legacyFileMountsOf); ok { + for _, line := range strings.Split(f, "\n") { + if line != "" { + m.FileMounts = append(m.FileMounts, line) + } + } + } + return d.UpdateMeta(vm, func(cur *Metadata) { *cur = m }) +} + +// parseBuilt is a build time in Unix seconds. A record that is there but +// unreadable still says the base was built: 1. +func parseBuilt(s string) int64 { + if n, err := strconv.ParseInt(s, 10, 64); err == nil && n > 0 && strings.Trim(s, "0123456789") == "" { + return n + } + return 1 +} diff --git a/internal/state/layout_test.go b/internal/state/layout_test.go new file mode 100644 index 00000000..0f7ad20b --- /dev/null +++ b/internal/state/layout_test.go @@ -0,0 +1,239 @@ +package state + +import ( + "bytes" + "errors" + "os" + "path/filepath" + "strings" + "testing" +) + +// legacyVM makes 0.3.0's VM name: lima//lima.yaml and a marker per fact. +func legacyVM(t *testing.T, d Dir, name string, markers map[string]string) { + t.Helper() + dir := filepath.Join(d.LimaHome(), name) + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + os.WriteFile(filepath.Join(dir, "lima.yaml"), []byte("cpus: 2\n"), 0o644) + os.WriteFile(filepath.Join(dir, "diffdisk"), []byte("disk of "+name), 0o644) + for kind, content := range markers { + if err := d.Write(kind+name, []byte(content)); err != nil { + t.Fatal(err) + } + } +} + +func TestMigrateLayout(t *testing.T) { + d := Dir(t.TempDir()) + legacyVM(t, d, "base", nil) + d.Write(legacyBaseVersion, []byte("1760000000\n")) + d.Write(legacyBaseBuiltBy, []byte("0.3.0\n")) + legacyVM(t, d, "proj-1234abcd", map[string]string{ + legacyVersionOf: "1750000000\n", legacyBuiltByOf: "0.2.1\n", legacyTermOf: "xterm-ghostty\n", + legacyFileMountsOf: "/h/f|/s/file-mounts/proj-1234abcd/0/f|/tmp/.agent-vm-file-mounts/0/f|/h/f\n", + legacyPolicyOf: "abc\n", legacyMountsOf: "x\n", legacyCacheOf: "y\n", + }) + // 0.1.0's VM, migrated in place by 0.2: no built-by, but sshfs. + legacyVM(t, d, "old-0000aaaa", map[string]string{legacySSHFSOf: "1\n", legacyVersionOf: "1740000000\n"}) + // A run that did not finish, and one with no pid. + legacyVM(t, d, "x-scratch-0000aaaa", map[string]string{legacyScratchOf: "4242\n"}) + legacyVM(t, d, "y-scratch-0000bbbb", map[string]string{legacyScratchOf: "?\n"}) + // Markers of no VM, and staged files of VMs there are, and are not. + d.Write(legacyTermOf+"gone", []byte("t\n")) + d.Write(legacyVersionOf+"gone", []byte("1\n")) + os.MkdirAll(d.Path("file-mounts/proj-1234abcd/0"), 0o755) + os.WriteFile(d.Path("file-mounts/proj-1234abcd/0/f"), []byte("staged"), 0o644) + os.MkdirAll(d.Path("file-mounts/gone/0"), 0o755) + // What is not agent-vm's stays. + os.MkdirAll(filepath.Join(d.LimaHome(), "_config"), 0o755) + d.Write(".agent-vm-lima-migrated", []byte("x\n")) + d.Write("env", []byte("A=1\n")) + if !d.PendingLayout(d.LimaHome()) { + t.Fatal("nothing pending") + } + + var log bytes.Buffer + if err := d.MigrateLayout(LayoutOps{Home: d.LimaHome(), Template: "base", Log: &log}); err != nil { + t.Fatalf("%v\n%s", err, log.String()) + } + if d.PendingLayout(d.LimaHome()) { + ents, _ := os.ReadDir(string(d)) + t.Errorf("pending after: %v", ents) + } + + if b, _ := os.ReadFile(filepath.Join(d.Lima("proj-1234abcd"), "diffdisk")); string(b) != "disk of proj-1234abcd" { + t.Errorf("disk: %q", b) + } + if _, err := os.Stat(filepath.Join(d.LimaHome(), "proj-1234abcd")); err == nil { + t.Error("left in lima/") + } + if _, err := os.Stat(filepath.Join(d.LimaHome(), "_config")); err != nil { + t.Error("Lima's own config moved") + } + + m := d.Known("proj-1234abcd") + if m.Schema != 1 || m.Base == nil || *m.Base != (Base{Name: "base", Built: 1750000000, BuiltBy: "0.2.1"}) || m.Term != "xterm-ghostty" || + len(m.FileMounts) != 1 || !strings.HasPrefix(m.FileMounts[0], "/h/f|") { + t.Errorf("project VM: %+v", m) + } + if b := d.Known("base").Base; b == nil || *b != (Base{Name: "base", Built: 1760000000, BuiltBy: "0.3.0"}) || !d.BaseReady("base") { + t.Errorf("base: %+v", b) + } + if b := d.Known("old-0000aaaa").Base; b == nil || b.BuiltBy != "0.1.0" || b.Built != 1740000000 { + t.Errorf("0.1.0 VM: %+v", b) + } + if d.Known("x-scratch-0000aaaa").ScratchPID != 4242 || d.Known("y-scratch-0000bbbb").ScratchPID != -1 { + t.Error("scratch records") + } + if b, _ := os.ReadFile(filepath.Join(d.FileMounts("proj-1234abcd"), "0", "f")); string(b) != "staged" { + t.Errorf("staged files: %q", b) + } + + // Gone: the markers, the staged files of no VM, the old file-mounts. + ents, _ := os.ReadDir(string(d)) + var left []string + for _, e := range ents { + left = append(left, e.Name()) + } + if got := strings.Join(left, " "); got != ".agent-vm-lima-migrated env lima vms" { + t.Errorf("state folder: %s", got) + } + if _, err := os.Stat(d.VM("gone")); err == nil { + t.Error("a folder for a VM that is not there") + } + + // Done once: a second run changes nothing. + log.Reset() + if err := d.MigrateLayout(LayoutOps{Home: d.LimaHome(), Template: "base", Log: &log}); err != nil || log.Len() != 0 { + t.Errorf("second run: %v %q", err, log.String()) + } +} + +// A running VM cannot move: it waits, with its markers, unless it can be +// stopped. +func TestMigrateLayoutRunning(t *testing.T) { + d := Dir(t.TempDir()) + legacyVM(t, d, "up-1234abcd", map[string]string{legacyBuiltByOf: "0.3.0\n", legacyVersionOf: "5\n"}) + legacyVM(t, d, "down-1234abcd", map[string]string{legacyBuiltByOf: "0.3.0\n", legacyVersionOf: "5\n"}) + running := func(n string) bool { return n == "up-1234abcd" } + var log bytes.Buffer + + // No one to ask. + if err := d.MigrateLayout(LayoutOps{Home: d.LimaHome(), Template: "base", Running: running, Log: &log}); err != nil { + t.Fatal(err) + } + if !d.HasLima("down-1234abcd") || d.HasLima("up-1234abcd") || !strings.Contains(log.String(), "up-1234abcd is running") { + t.Errorf("not asked: %q", log.String()) + } + if _, err := os.Stat(d.Path(legacyBuiltByOf + "up-1234abcd")); err != nil { + t.Error("the markers of the VM left behind went") + } + if !d.PendingLayout(d.LimaHome()) { + t.Error("nothing pending, a VM is left") + } + + // Asked, and no. + if err := d.MigrateLayout(LayoutOps{Home: d.LimaHome(), Template: "base", Running: running, Ask: func([]string) bool { return false }}); err != nil || d.HasLima("up-1234abcd") { + t.Errorf("declined: %v", err) + } + + // Asked, yes, but it does not stop. + log.Reset() + boom := errors.New("boom") + err := d.MigrateLayout(LayoutOps{Home: d.LimaHome(), Template: "base", Running: running, Ask: func([]string) bool { return true }, Stop: func(string) error { return boom }, Log: &log}) + if err != nil || d.HasLima("up-1234abcd") || !strings.Contains(log.String(), "boom") { + t.Errorf("not stopped: %v %q", err, log.String()) + } + + // Asked, yes, stopped. + var stopped []string + err = d.MigrateLayout(LayoutOps{Home: d.LimaHome(), Template: "base", Running: running, Ask: func(r []string) bool { return len(r) == 1 }, + Stop: func(n string) error { stopped = append(stopped, n); return nil }}) + if err != nil || !d.HasLima("up-1234abcd") || strings.Join(stopped, ",") != "up-1234abcd" { + t.Errorf("stopped: %v %v", err, stopped) + } + if d.Known("up-1234abcd").Base == nil || d.PendingLayout(d.LimaHome()) { + t.Error("metadata of the moved VM, or still pending") + } +} + +// Interrupted after the rename: the markers are still there, the VM is +// moved, and the next run finishes. +func TestMigrateLayoutResumes(t *testing.T) { + d := Dir(t.TempDir()) + legacyVM(t, d, "p-1234abcd", map[string]string{legacyBuiltByOf: "0.3.0\n", legacyVersionOf: "7\n"}) + if err := os.MkdirAll(d.VM("p-1234abcd"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.Rename(filepath.Join(d.LimaHome(), "p-1234abcd"), d.Lima("p-1234abcd")); err != nil { + t.Fatal(err) + } + if !d.PendingLayout(d.LimaHome()) { + t.Fatal("markers not pending") + } + if err := d.MigrateLayout(LayoutOps{Home: d.LimaHome(), Template: "base"}); err != nil { + t.Fatal(err) + } + if b := d.Known("p-1234abcd").Base; b == nil || b.Built != 7 || d.PendingLayout(d.LimaHome()) { + t.Errorf("resumed: %+v", b) + } +} + +// A VM in both places is not touched. +func TestMigrateLayoutConflict(t *testing.T) { + d := Dir(t.TempDir()) + legacyVM(t, d, "p-1234abcd", map[string]string{legacyVersionOf: "7\n"}) + os.MkdirAll(d.Lima("p-1234abcd"), 0o755) + os.WriteFile(filepath.Join(d.Lima("p-1234abcd"), "lima.yaml"), []byte("new"), 0o644) + var log bytes.Buffer + err := d.MigrateLayout(LayoutOps{Home: d.LimaHome(), Template: "base", Log: &log}) + if err == nil || !strings.Contains(err.Error(), "p-1234abcd") || !strings.Contains(log.String(), "exists already") { + t.Errorf("%v %q", err, log.String()) + } + if b, _ := os.ReadFile(filepath.Join(d.Lima("p-1234abcd"), "lima.yaml")); string(b) != "new" { + t.Error("the VM there was overwritten") + } + if !d.HasLima("p-1234abcd") { + t.Error("lost") + } + if _, err := os.Stat(filepath.Join(d.LimaHome(), "p-1234abcd", "lima.yaml")); err != nil { + t.Error("the old one went") + } +} + +// 0.2's migration names markers as 0.3.0 did; they follow the VM before +// this one runs. +func TestLegacyRename(t *testing.T) { + d := Dir(t.TempDir()) + for _, kind := range legacyKinds { + d.Write(kind+"agent-vm-a", []byte("1\n")) + } + os.MkdirAll(d.Path("file-mounts/agent-vm-a"), 0o755) + if err := d.LegacyRename("agent-vm-a", "a"); err != nil { + t.Fatal(err) + } + for _, kind := range legacyKinds { + if d.Read(kind+"a") != "1" || d.Read(kind+"agent-vm-a") != "" { + t.Errorf("%s did not follow", kind) + } + } + if _, err := os.Stat(d.Path("file-mounts/a")); err != nil { + t.Error("staged files did not follow") + } +} + +// sshfs-cache-x is the cache record of x, unless a VM is named cache-x. +func TestLegacyMarkerAmbiguity(t *testing.T) { + d := Dir(t.TempDir()) + d.Write(legacyCacheOf+"x", []byte("1\n")) + got := d.legacyMarkers("base", func(string) bool { return false }) + if got["x"][legacyCacheOf] == "" { + t.Errorf("no VM named cache-x: %v", got) + } + got = d.legacyMarkers("base", func(n string) bool { return n == "cache-x" }) + if got["cache-x"][legacySSHFSOf] == "" || got["x"] != nil { + t.Errorf("a VM named cache-x: %v", got) + } +} diff --git a/internal/state/state.go b/internal/state/state.go index 9986809c..b7dfa0e9 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -1,14 +1,15 @@ // Package state is agent-vm's folder, ~/.agent-vm (AGENT_VM_STATE_DIR): the -// user's files (env, volumes, runtime.sh, setup.sh) and agent-vm's markers, -// one file per VM and fact, named as 0.2 named them. +// user's files (env, volumes, runtime.sh, setup.sh) and one folder per VM, +// vms/, with what agent-vm knows of it. package state import ( + "encoding/json" "errors" + "fmt" "os" "path/filepath" "regexp" - "strconv" "strings" "time" @@ -41,30 +42,34 @@ func (d Dir) Path(name string) string { return paths.Host(filepath.Join(string(d), name)) } -// The markers of a VM. +// The files of a VM's folder. const ( - VersionOf = ".agent-vm-version-" // the base's build time it was cloned from - BuiltByOf = ".agent-vm-built-by-" // the agent-vm version that built that base - SSHFSOf = ".agent-vm-sshfs-" // 0.1 migration done - TermOf = ".agent-vm-term-" // the $TERM whose terminfo it has - FileMountsOf = ".agent-vm-file-mounts-" // single files of ~/.agent-vm/volumes - MountsOf = ".agent-vm-mounts-" // 0.2's record of its shares: only removed now - CacheOf = ".agent-vm-sshfs-cache-" // 0.2's record of sshfs's cache: only removed now - ScratchOf = ".agent-vm-scratch-" // a --scratch run, with its pid - PolicyOf = ".agent-vm-policy-" // fingerprint of the network and guarded policies it booted with + MetaFile = "metadata.json" + PolicyFile = "policy.json" + // LimaDir is Lima's own folder for the VM. + LimaDir = "lima" + // FileMountsDir holds the single files of ~/.agent-vm/volumes, staged. + FileMountsDir = "file-mounts" ) -// The base template's markers. BaseVersion, written last by setup, says it -// can be cloned. -const ( - BaseVersion = ".agent-vm-base-version" - BaseBuiltBy = ".agent-vm-base-built-by" -) +// VMs is the folder of every VM. +func (d Dir) VMs() string { return d.Path("vms") } -// Marker is the path of marker kind (one of the ...Of) for vm. -func (d Dir) Marker(kind, vm string) string { - return d.Path(kind + vm) -} +// VM is the folder of the VM name: nothing else of it is kept anywhere. +func (d Dir) VM(name string) string { return d.Path(filepath.Join("vms", name)) } + +// Lima is Lima's folder for the VM name (its lima.yaml, disk, sockets). +func (d Dir) Lima(name string) string { return filepath.Join(d.VM(name), LimaDir) } + +// FileMounts is where the staged single files of the VM name are. +func (d Dir) FileMounts(name string) string { return filepath.Join(d.VM(name), FileMountsDir) } + +// Policy is the file with the policies the VM name boots with. +func (d Dir) Policy(name string) string { return filepath.Join(d.VM(name), PolicyFile) } + +// LimaHome is Lima's own home: what is shared by every VM (_config with the +// SSH key, _networks), apart from the VMs. +func (d Dir) LimaHome() string { return d.Path("lima") } // Read is the content of name in the folder, without its trailing newline, // "" when there is none. @@ -78,12 +83,18 @@ func (d Dir) Read(name string) string { // Write replaces name in the folder, made if need be, through a temporary // file renamed over it: a reader sees the old content or the new, never part -// of it. The temporary name does not start like a marker's. +// of it. func (d Dir) Write(name string, data []byte) error { - if err := os.MkdirAll(string(d), 0o755); err != nil { + return writeAtomic(string(d), d.Path(name), data) +} + +// writeAtomic replaces path, in the folder dir made if need be, through a +// temporary file of dir. +func writeAtomic(dir, path string, data []byte) error { + if err := os.MkdirAll(dir, 0o755); err != nil { return err } - tmp, err := os.CreateTemp(string(d), ".tmp-"+name+"-*") + tmp, err := os.CreateTemp(dir, ".tmp-"+filepath.Base(path)+"-*") if err != nil { return err } @@ -92,56 +103,124 @@ func (d Dir) Write(name string, data []byte) error { os.Remove(tmp.Name()) return err } - if err := os.Rename(tmp.Name(), d.Path(name)); err != nil { + if err := os.Rename(tmp.Name(), path); err != nil { os.Remove(tmp.Name()) return err } return nil } -// ReadMarker is Read for a marker of vm. -func (d Dir) ReadMarker(kind, vm string) string { - return d.Read(kind + vm) +// WriteVM replaces name in the folder of the VM vm, made if need be. +func (d Dir) WriteVM(vm, name string, data []byte) error { + return writeAtomic(d.VM(vm), filepath.Join(d.VM(vm), name), data) } -var markers = []string{VersionOf, BuiltByOf, SSHFSOf, TermOf, FileMountsOf, MountsOf, CacheOf, ScratchOf, PolicyOf} +// SchemaVersion is the version of metadata.json this agent-vm reads and +// writes. +const SchemaVersion = 1 + +// Metadata is what agent-vm knows of a VM, in vms//metadata.json. Only +// written under the VM's lock. +type Metadata struct { + Schema int `json:"schema"` + // Base is the template the VM was cloned from. In the template's own + // file, it is the template itself, written last by setup: it says the + // template can be cloned. + Base *Base `json:"base,omitempty"` + // Term is the $TERM whose terminfo the VM has. + Term string `json:"term,omitempty"` + // Bound is the folder a named VM that shares it belongs to, "-" for one + // that shares nothing. + Bound string `json:"bound,omitempty"` + // ScratchPID is the process of the --scratch run the VM belongs to. + ScratchPID int `json:"scratch_pid,omitempty"` + // FileMounts are the single files of ~/.agent-vm/volumes to bind in the + // VM: see mounts.FileMount. + FileMounts []string `json:"file_mounts,omitempty"` +} -// Rename moves what is known of the VM from to its new name to: its -// markers, and its staged single files (its shares are set again before it -// next boots, from where they now are). -func (d Dir) Rename(from, to string) error { - var last error - for _, kind := range markers { - if err := os.Rename(d.Marker(kind, from), d.Marker(kind, to)); err != nil && !os.IsNotExist(err) { - last = err - } +// Base is a template, as built or as cloned. +type Base struct { + // Name is the template's VM name. + Name string `json:"name"` + // Built is when it was built, in Unix seconds. + Built int64 `json:"built"` + // BuiltBy is the agent-vm version that built it. Empty for 0.1.0's, which + // has no sshfs: the shares need one. "0.1.0" in a VM migrated in place + // by 0.2, which has. + BuiltBy string `json:"built_by"` +} + +// Meta is the metadata of the VM vm. A VM with no file, and one whose file +// cannot be read, have nothing known: the zero Metadata, and for the second, +// an error to say so. +func (d Dir) Meta(vm string) (Metadata, error) { + p := filepath.Join(d.VM(vm), MetaFile) + b, err := os.ReadFile(p) + if errors.Is(err, os.ErrNotExist) { + return Metadata{}, nil } - if err := os.Rename(d.Path(filepath.Join("file-mounts", from)), d.Path(filepath.Join("file-mounts", to))); err != nil && !os.IsNotExist(err) { - last = err + if err != nil { + return Metadata{}, err + } + var m Metadata + if err := json.Unmarshal(b, &m); err != nil { + return Metadata{}, fmt.Errorf("%s: %w", p, err) } - return last + if m.Schema > SchemaVersion { + return Metadata{}, fmt.Errorf("%s: written by a newer agent-vm (schema %d, this one reads %d)", p, m.Schema, SchemaVersion) + } + return m, nil } -// Forget removes what is known of vm, once deleted. Deleting the base -// template retires the marker that says it can be cloned. -func (d Dir) Forget(vm, template string) error { - var last error - for _, kind := range markers { - if err := os.Remove(d.Marker(kind, vm)); err != nil && !os.IsNotExist(err) { - last = err - } - } - if err := os.RemoveAll(d.Path(filepath.Join("file-mounts", vm))); err != nil { - last = err +// Known is the metadata of vm, nothing known when it cannot be read. +func (d Dir) Known(vm string) Metadata { + m, _ := d.Meta(vm) + return m +} + +// UpdateMeta changes the metadata of vm with fn and writes it back. The +// caller holds the VM's lock. A file that cannot be read is replaced. +func (d Dir) UpdateMeta(vm string, fn func(*Metadata)) error { + m := d.Known(vm) + fn(&m) + m.Schema = SchemaVersion + b, err := json.MarshalIndent(m, "", " ") + if err != nil { + return err } - if vm == template { - for _, f := range []string{BaseVersion, BaseBuiltBy} { - if err := os.Remove(d.Path(f)); err != nil && !os.IsNotExist(err) { - last = err - } + return d.WriteVM(vm, MetaFile, append(b, '\n')) +} + +// Forget removes the folder of vm, and with it all that is known of it: once +// the VM is deleted. +func (d Dir) Forget(vm string) error { + return os.RemoveAll(d.VM(vm)) +} + +// Names are the folders of vms/, which hold a VM or what is left of one. +func (d Dir) Names() []string { + ents, _ := os.ReadDir(d.VMs()) + var out []string + for _, e := range ents { + if e.IsDir() && !strings.HasPrefix(e.Name(), ".") { + out = append(out, e.Name()) } } - return last + return out +} + +// HasLima reports whether Lima has made the VM: its lima.yaml is there. A +// folder without one is a creation that did not finish. +func (d Dir) HasLima(vm string) bool { + _, err := os.Stat(filepath.Join(d.Lima(vm), "lima.yaml")) + return err == nil +} + +// BaseReady reports whether the template can be cloned: setup finished. +func (d Dir) BaseReady(template string) bool { + b := d.Known(template).Base + return b != nil && b.Built > 0 } var builtByRe = regexp.MustCompile(`^[0-9A-Za-z.+-]+$`) @@ -150,19 +229,14 @@ var builtByRe = regexp.MustCompile(`^[0-9A-Za-z.+-]+$`) // that built it, and the day, "-" when not known. The base template // describes itself. 0.1.0 recorded no version: a base without one was built // by it. -func (d Dir) BaseLabel(vm, template string) string { - verFile, byFile := VersionOf+vm, BuiltByOf+vm - if vm == template { - verFile, byFile = BaseVersion, BaseBuiltBy - } - by := d.Read(byFile) - if !builtByRe.MatchString(by) { - by = "0.1.0" - } - day := "-" - if built := d.Read(verFile); built != "" { - if sec, err := strconv.ParseInt(built, 10, 64); err == nil && sec >= 0 && strings.Trim(built, "0123456789") == "" { - day = time.Unix(sec, 0).Format("2006-01-02") +func (d Dir) BaseLabel(vm string) string { + by, day := "0.1.0", "-" + if b := d.Known(vm).Base; b != nil { + if builtByRe.MatchString(b.BuiltBy) { + by = b.BuiltBy + } + if b.Built > 0 { + day = time.Unix(b.Built, 0).Format("2006-01-02") } } return by + " " + day diff --git a/internal/state/state_test.go b/internal/state/state_test.go index 949ba354..47941abf 100644 --- a/internal/state/state_test.go +++ b/internal/state/state_test.go @@ -3,6 +3,7 @@ package state import ( "os" "path/filepath" + "strings" "testing" "github.com/sylvinus/agent-vm/internal/paths" @@ -42,7 +43,7 @@ func TestDefaultRelative(t *testing.T) { func TestReadWrite(t *testing.T) { d := Dir(filepath.Join(t.TempDir(), "state")) - if d.Read("x") != "" || d.ReadMarker(VersionOf, "vm") != "" { + if d.Read("x") != "" { t.Error("read of nothing") } if err := d.Write("x", []byte("one\n")); err != nil { @@ -54,12 +55,9 @@ func TestReadWrite(t *testing.T) { if d.Read("x") != "two" { t.Errorf("Read = %q", d.Read("x")) } - if err := d.Write(VersionOf+"vm", []byte("5\n")); err != nil || d.ReadMarker(VersionOf, "vm") != "5" { - t.Errorf("marker: %q, %v", d.ReadMarker(VersionOf, "vm"), err) - } ents, _ := os.ReadDir(string(d)) for _, e := range ents { - if e.Name() != "x" && e.Name() != VersionOf+"vm" { + if e.Name() != "x" { t.Errorf("left behind: %s", e.Name()) } } @@ -71,26 +69,103 @@ func TestReadWrite(t *testing.T) { } } -// Every marker follows its VM, the policy one included. -func TestRenameForget(t *testing.T) { +func TestLayout(t *testing.T) { + d := Dir("/s") + for got, want := range map[string]string{ + d.VMs(): "/s/vms", + d.VM("a"): "/s/vms/a", + d.Lima("a"): "/s/vms/a/lima", + d.FileMounts("a"): "/s/vms/a/file-mounts", + d.Policy("a"): "/s/vms/a/policy.json", + d.LimaHome(): "/s/lima", + d.Path("locks/vm1"): "/s/locks/vm1", + } { + if filepath.ToSlash(strings.TrimPrefix(got, filepath.VolumeName(got))) != want { + t.Errorf("%q, want %q", got, want) + } + } +} + +func TestMeta(t *testing.T) { d := Dir(t.TempDir()) - for _, kind := range markers { - d.Write(kind+"a", []byte("1\n")) + if m, err := d.Meta("a"); err != nil || m.Base != nil || m.Schema != 0 { + t.Errorf("no file: %+v, %v", m, err) + } + err := d.UpdateMeta("a", func(m *Metadata) { + m.Base = &Base{Name: "base", Built: 1760000000, BuiltBy: "0.4.0"} + m.Term = "xterm" + }) + if err != nil { + t.Fatal(err) } - if err := d.Rename("a", "b"); err != nil { + if err := d.UpdateMeta("a", func(m *Metadata) { m.FileMounts = []string{"x|y|z|w"} }); err != nil { t.Fatal(err) } - for _, kind := range markers { - if d.ReadMarker(kind, "b") != "1" || d.ReadMarker(kind, "a") != "" { - t.Errorf("%s did not follow", kind) - } + m, err := d.Meta("a") + if err != nil || m.Schema != 1 || m.Base.BuiltBy != "0.4.0" || m.Term != "xterm" || len(m.FileMounts) != 1 { + t.Errorf("read back: %+v, %v", m, err) + } + if got := d.BaseLabel("a"); got != "0.4.0 2025-10-09" { + t.Errorf("label %q", got) + } + // Nothing left beside it, whatever the writes. + ents, _ := os.ReadDir(d.VM("a")) + if len(ents) != 1 || ents[0].Name() != MetaFile { + t.Errorf("folder: %v", ents) + } + + // An unreadable file: nothing known, said by the error. + os.WriteFile(filepath.Join(d.VM("a"), MetaFile), []byte("{"), 0o644) + if m, err := d.Meta("a"); err == nil || !strings.Contains(err.Error(), MetaFile) || m.Base != nil { + t.Errorf("corrupt: %+v, %v", m, err) + } + if d.Known("a").Base != nil || d.BaseLabel("a") != "0.1.0 -" || d.BaseReady("a") { + t.Error("corrupt file known") + } + os.WriteFile(filepath.Join(d.VM("a"), MetaFile), []byte(`{"schema": 2}`), 0o644) + if _, err := d.Meta("a"); err == nil || !strings.Contains(err.Error(), "newer") { + t.Errorf("newer schema: %v", err) + } + // A write replaces what cannot be read. + if err := d.UpdateMeta("a", func(m *Metadata) { m.Term = "t" }); err != nil || d.Known("a").Term != "t" { + t.Errorf("replace: %v", err) } - if err := d.Forget("b", "base"); err != nil { +} + +func TestBaseReady(t *testing.T) { + d := Dir(t.TempDir()) + if d.BaseReady("base") { + t.Error("ready with nothing") + } + d.UpdateMeta("base", func(m *Metadata) { m.Base = &Base{Name: "base"} }) + if d.BaseReady("base") { + t.Error("ready with no build time") + } + d.UpdateMeta("base", func(m *Metadata) { m.Base.Built = 5 }) + if !d.BaseReady("base") { + t.Error("not ready") + } +} + +func TestForgetNames(t *testing.T) { + d := Dir(t.TempDir()) + d.UpdateMeta("a", func(m *Metadata) { m.Term = "x" }) + os.MkdirAll(d.Lima("b"), 0o755) + os.WriteFile(filepath.Join(d.Lima("b"), "lima.yaml"), nil, 0o644) + os.WriteFile(filepath.Join(d.VMs(), "file"), nil, 0o644) + if got := strings.Join(d.Names(), " "); got != "a b" { + t.Errorf("names %q", got) + } + if d.HasLima("a") || !d.HasLima("b") { + t.Error("HasLima") + } + if err := d.Forget("a"); err != nil { t.Fatal(err) } - for _, kind := range markers { - if _, err := os.Stat(d.Marker(kind, "b")); err == nil { - t.Errorf("%s left", kind) - } + if err := d.Forget("a"); err != nil { + t.Errorf("twice: %v", err) + } + if got := strings.Join(d.Names(), " "); got != "b" { + t.Errorf("after forget %q", got) } } diff --git a/internal/vm/driver_hcs_windows.go b/internal/vm/driver_hcs_windows.go index eed6a6d9..e97cde76 100644 --- a/internal/vm/driver_hcs_windows.go +++ b/internal/vm/driver_hcs_windows.go @@ -7,4 +7,4 @@ package vm // Import hcs driver to register it in the registry on windows. -import _ "github.com/lima-vm/lima/v2/pkg/driver/hcs" +import _ "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/hcs" diff --git a/internal/vm/driver_qemu.go b/internal/vm/driver_qemu.go index 8e554fdd..fd83ccdc 100644 --- a/internal/vm/driver_qemu.go +++ b/internal/vm/driver_qemu.go @@ -7,4 +7,4 @@ package vm // Import qemu driver to register it in the registry on all platforms. -import _ "github.com/lima-vm/lima/v2/pkg/driver/qemu" +import _ "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/qemu" diff --git a/internal/vm/driver_vz_darwin.go b/internal/vm/driver_vz_darwin.go index cd2628d5..87137a79 100644 --- a/internal/vm/driver_vz_darwin.go +++ b/internal/vm/driver_vz_darwin.go @@ -7,4 +7,4 @@ package vm // Import vz driver to register it in the registry on darwin. -import _ "github.com/lima-vm/lima/v2/pkg/driver/vz" +import _ "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/vz" diff --git a/internal/vm/driver_wsl2_windows.go b/internal/vm/driver_wsl2_windows.go index 23550413..291c1337 100644 --- a/internal/vm/driver_wsl2_windows.go +++ b/internal/vm/driver_wsl2_windows.go @@ -7,4 +7,4 @@ package vm // Import wsl2 driver to register it in the registry on windows. -import _ "github.com/lima-vm/lima/v2/pkg/driver/wsl2" +import _ "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/wsl2" diff --git a/internal/vm/lima.go b/internal/vm/lima.go index 6e0bc61c..d8d879e0 100644 --- a/internal/vm/lima.go +++ b/internal/vm/lima.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "io/fs" "os" "os/exec" "path/filepath" @@ -22,40 +23,49 @@ import ( "github.com/mattn/go-isatty" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/instance" - "github.com/lima-vm/lima/v2/pkg/limatmpl" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/networks/reconcile" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/sshutil" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/yqutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatmpl" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/reconcile" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/sshutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + limaversion "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/yqutil" "github.com/sylvinus/agent-vm/internal/limaembed" "github.com/sylvinus/agent-vm/internal/paths" ) +// limaPatches is the patch level of the Lima this is written for +// (patches/lima): a tree without the patches has no such constant, and does +// not compile here. A new patch changes it, and TestPatchLevel. +const limaPatches = limaversion.AgentVMPatches + // Lima runs VMs with the Lima built into agent-vm, as limactl's commands // do, in this process; its hostagent is this executable again (see // limaembed.Run). type Lima struct { stateDir string + // assets are the guest agent and the templates; the binary's own when + // nil. + assets fs.FS once sync.Once share *limaembed.Share err error } -// MaxName is the longest VM name whose sockets fit under the Lima home -// home (UNIX_PATH_MAX: 104 bytes on macOS), and Lima's 76 at most. Lima -// resolves the home's symlinks: so does this, as far as it exists. -func MaxName(home string) int { - real, rest := home, "" +// MaxName is the longest VM name whose sockets fit under the folder of the +// VMs, vms (UNIX_PATH_MAX: 104 bytes on macOS), and Lima's 76 at most. Lima +// resolves the folder's symlinks: so does this, as far as it exists. +func MaxName(vms string) int { + real, rest := vms, "" for { if p, err := filepath.EvalSymlinks(real); err == nil { real = filepath.Join(p, rest) @@ -63,22 +73,35 @@ func MaxName(home string) int { } parent := filepath.Dir(real) if parent == real { - real = home + real = vms break } rest = filepath.Join(filepath.Base(real), rest) real = parent } - // //, shorter than UnixPathMax (its NUL). - n := osutil.UnixPathMax - 1 - len(real) - len("/") - len("/"+filenames.LongestSock) + // //lima/, shorter than UnixPathMax (its NUL). + n := osutil.UnixPathMax - 1 - len(real) - len("/") - len("/"+dirnames.InstanceSubdir+"/"+filenames.LongestSock) return min(n, 76) } -// NewLima keeps its VMs in home (LIMA_HOME), and writes Lima's guest agent -// and templates under stateDir. -func NewLima(home, stateDir string) (*Lima, error) { +// WithAssets gives l the guest agent and templates to write under its state +// folder, in place of those embedded in this binary. +func (l *Lima) WithAssets(assets fs.FS) *Lima { + l.assets = assets + return l +} + +// NewLima keeps Lima's own files in home (LIMA_HOME), its VMs in +// vms//lima (LIMA_INSTANCES_DIR; in home, as Lima does, when vms is +// empty), and writes Lima's guest agent and templates under stateDir. +func NewLima(home, vms, stateDir string) (*Lima, error) { limaHomeMu.Lock() defer limaHomeMu.Unlock() + // A program not stamped with Lima's version at build time (-X) has the + // one its tree is made from, not "", which Lima warns about. + if limaversion.Version == "" { + limaversion.Version = limaversion.AgentVMUpstream + } // A value other than the one set earlier here is the user's. if cur := os.Getenv("LIMA_HOME"); cur != limaHomeSet { userLimaHome = cur @@ -87,6 +110,11 @@ func NewLima(home, stateDir string) (*Lima, error) { return nil, err } limaHomeSet = home + if vms == "" { + os.Unsetenv(dirnames.InstancesEnv) + } else if err := os.Setenv(dirnames.InstancesEnv, vms); err != nil { + return nil, err + } return &Lima{stateDir: stateDir}, nil } @@ -115,7 +143,7 @@ func UserLimaHome() string { // agent-vm sets, nothing else. func (l *Lima) prepare() (*limaembed.Share, error) { l.once.Do(func() { - l.share, l.err = limaembed.WriteShare(l.stateDir) + l.share, l.err = limaembed.WriteShare(l.stateDir, l.assets) if l.err == nil { l.err = os.Setenv("LIMA_TEMPLATES_PATH", l.share.Templates()) } @@ -222,6 +250,9 @@ func fromLima(inst *limatype.Instance) *Instance { out.Status = Broken out.ConfigErr = errors.Join(inst.Errors...) } + // "driver is running but host agent is not". Not the other way around: a + // hostagent without its driver is also one booting the VM. + out.Orphaned = out.Status == Broken && inst.Config != nil && inst.HostAgentPID == 0 && inst.DriverPID > 0 if c := inst.Config; c != nil { if c.SSH.LocalPort != nil { out.SSHPortConfig = *c.SSH.LocalPort @@ -505,12 +536,18 @@ func (l *Lima) Stop(ctx context.Context, name string) error { if err != nil { return err } - if inst.Status != limatype.StatusRunning { - return nil + switch { + case inst.Status == limatype.StatusRunning: + return withLog(io.Discard, func() error { + return errors.Join(instance.StopGracefully(ctx, inst, false), reconcile.Reconcile(ctx, "")) + }) + case fromLima(inst).Orphaned: + return withLog(io.Discard, func() error { + instance.StopForcibly(inst) + return reconcile.Reconcile(ctx, "") + }) } - return withLog(io.Discard, func() error { - return errors.Join(instance.StopGracefully(ctx, inst, false), reconcile.Reconcile(ctx, "")) - }) + return nil } func (l *Lima) Delete(ctx context.Context, name string) error { diff --git a/internal/vm/lima_e2e_test.go b/internal/vm/lima_e2e_test.go index 36c71bc6..fe961a28 100644 --- a/internal/vm/lima_e2e_test.go +++ b/internal/vm/lima_e2e_test.go @@ -14,7 +14,9 @@ import ( "testing" "time" - "github.com/lima-vm/lima/v2/pkg/instance" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance" + + "github.com/sylvinus/agent-vm/internal/policy" ) // A real VM, with a protected share: go test -tags e2e ./internal/vm/ @@ -66,6 +68,14 @@ func TestLimaBoot(t *testing.T) { } defer allowed.Close() os.WriteFile(filepath.Join(stateDir, "network"), []byte("allow "+allowed.Addr().String()+"\n"), 0o644) + pf, err := policy.Current(stateDir, "agent-vm") + if err != nil { + t.Fatal(err) + } + // Where the host agent looks: the VM's folder, in the folder of the VMs. + if err := policy.Write(filepath.Join(os.Getenv("LIMA_INSTANCES_DIR"), name, "policy.json"), pf); err != nil { + t.Fatal(err) + } var log bytes.Buffer start := time.Now() @@ -124,32 +134,20 @@ func TestLimaBoot(t *testing.T) { if !connect("1.1.1.1:443") { t.Error("the VM did not reach the internet") } - // 0.2's migration, the VM running: stopped through its hostagent, moved. - old := os.Getenv("LIMA_HOME") - // Next to it: no longer, for the sockets. - now := filepath.Join(filepath.Dir(old), "n") - if err := MoveOld(ctx, old, now, name, "e2e-00000000"); err != nil { + // Stopped, then started again with the same policy file. + if err := l.Stop(ctx, name); err != nil { t.Fatal(err) } - name = "e2e-00000000" - l2, err := NewLima(now, t.TempDir()) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { _ = l2.Delete(context.Background(), name) }) - if inst, err := l2.Get(ctx, name); err != nil || inst.Status != Stopped || len(inst.Mounts) != 1 { - t.Errorf("moved: %v %+v", err, inst) + if inst, _ := l.Get(ctx, name); inst.Status != Stopped { + t.Errorf("after stop: %s", inst.Status) } - if err := l2.Start(ctx, name, &log); err != nil { - t.Fatalf("start in the new home: %v\n%s", err, log.String()) + if err := l.Start(ctx, name, &log); err != nil { + t.Fatalf("second start: %v\n%s", err, log.String()) } if code, out := run("cat from-vm"); code != 0 || out != "hi\na\nb\n" { - t.Errorf("after the move: %d %q", code, out) + t.Errorf("after the restart: %d %q", code, out) } - if err := l2.Stop(ctx, name); err != nil { + if err := l.Stop(ctx, name); err != nil { t.Fatal(err) } - if inst, _ := l2.Get(ctx, name); inst.Status != Stopped { - t.Errorf("after stop: %s", inst.Status) - } } diff --git a/internal/vm/lima_test.go b/internal/vm/lima_test.go index 87283e13..9cec9b0f 100644 --- a/internal/vm/lima_test.go +++ b/internal/vm/lima_test.go @@ -5,28 +5,50 @@ import ( "errors" "io" "os" + "os/exec" "path/filepath" "reflect" + "runtime" + "strconv" "strings" "sync" "testing" + "time" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" - "github.com/lima-vm/lima/v2/pkg/hostagent/events" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/portfwd" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/events" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/portfwd" + limaversion "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" "github.com/sylvinus/agent-vm/internal/limaembed" "github.com/sylvinus/agent-vm/internal/paths" ) +// newTestLima is a Lima with its VMs in vms//lima, as agent-vm keeps +// them: in dirnames.InstancesEnv. func newTestLima(t *testing.T) *Lima { + t.Helper() + return newLima(t, t.TempDir()) +} + +// newLegacyLima keeps its VMs in its Lima home, as 0.2 and 0.3.0 did. +func newLegacyLima(t *testing.T) *Lima { + t.Helper() + return newLima(t, "") +} + +func newLima(t *testing.T, vms string) *Lima { t.Helper() home := t.TempDir() t.Setenv("LIMA_HOME", home) t.Setenv("LIMA_TEMPLATES_PATH", "") - l, err := NewLima(home, t.TempDir()) + // NewLima sets the VMs' folder for Lima's packages: put it back. + t.Setenv(dirnames.InstancesEnv, "") + l, err := NewLima(home, vms, t.TempDir()) if err != nil { t.Fatal(err) } @@ -56,7 +78,7 @@ func TestLimaLifecycle(t *testing.T) { t.Fatalf("created: %+v", base) } // Lima's containerd stays out: Docker brings its own when chosen. - if y, _ := os.ReadFile(filepath.Join(os.Getenv("LIMA_HOME"), "base", "lima.yaml")); !strings.Contains(string(y), "containerd:\n user: false\n system: false") { + if y, _ := os.ReadFile(filepath.Join(os.Getenv(dirnames.InstancesEnv), "base", "lima", "lima.yaml")); !strings.Contains(string(y), "containerd:\n user: false\n system: false") { t.Errorf("containerd in lima.yaml:\n%s", y) } @@ -76,7 +98,7 @@ func TestLimaLifecycle(t *testing.T) { t.Errorf("name over 76: %v", err) } // MaxName is exactly what fits here. - if m := MaxName(os.Getenv("LIMA_HOME")); m < 76 { + if m := MaxName(os.Getenv(dirnames.InstancesEnv)); m < 76 { fits := strings.Repeat("n", m-9) + "-12345678" if err := l.Clone(ctx, "base", fits); err != nil { t.Errorf("a name of MaxName (%d): %v", m, err) @@ -86,7 +108,7 @@ func TestLimaLifecycle(t *testing.T) { t.Errorf("a name of MaxName+1: %v", err) } } else { - t.Errorf("MaxName = %d: the test's Lima home is long, it should be less than 76", m) + t.Errorf("MaxName = %d: the test's folder for VMs is long, it should be less than 76", m) } proj := t.TempDir() @@ -223,7 +245,7 @@ func TestLimaEphemeralPorts(t *testing.T) { t.Fatal(err) } // A forward of the VM's own, which Start keeps. - yamlPath := filepath.Join(os.Getenv("LIMA_HOME"), "p-12345678", "lima.yaml") + yamlPath := filepath.Join(os.Getenv(dirnames.InstancesEnv), "p-12345678", "lima", "lima.yaml") orig, _ := os.ReadFile(yamlPath) custom := "\nportForwards:\n- guestPort: 40000\n hostPort: 40001\n" if err := os.WriteFile(yamlPath, append(orig, custom...), 0o644); err != nil { @@ -291,6 +313,61 @@ func TestSharesKeptWindows(t *testing.T) { } } +// A driver running without its hostagent: orphaned, and Stop kills it. A +// hostagent without its driver is not: it may be booting the VM. +func TestLimaStopOrphaned(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("no sleep to stand for the driver") + } + ctx := context.Background() + l := newTestLima(t) + none := []Mount{} + if err := l.Create(ctx, "o", "template:debian-13", Settings{Mounts: &none}, io.Discard); err != nil { + t.Fatal(err) + } + inst, err := l.Get(ctx, "o") + if err != nil { + t.Fatal(err) + } + driver := exec.Command("sleep", "60") + if err := driver.Start(); err != nil { + t.Fatal(err) + } + exited := make(chan struct{}) + go func() { driver.Wait(); close(exited) }() + t.Cleanup(func() { driver.Process.Kill(); <-exited }) + pid := []byte(strconv.Itoa(driver.Process.Pid)) + + ha := filepath.Join(inst.Dir, filenames.HostAgentPID) + os.WriteFile(ha, pid, 0o644) + if got, _ := l.Get(ctx, "o"); got.Status != Broken || got.Orphaned { + t.Fatalf("a hostagent without its driver: %+v", got) + } + l.Stop(ctx, "o") + select { + case <-exited: + t.Fatal("Stop killed a hostagent without its driver") + case <-time.After(200 * time.Millisecond): + } + os.Remove(ha) + + os.WriteFile(filepath.Join(inst.Dir, filenames.PIDFile(inst.VMType)), pid, 0o644) + if got, _ := l.Get(ctx, "o"); got.Status != Broken || !got.Orphaned { + t.Fatalf("a driver without its hostagent: %+v", got) + } + if err := l.Stop(ctx, "o"); err != nil { + t.Fatal(err) + } + select { + case <-exited: + case <-time.After(10 * time.Second): + t.Fatal("the driver still runs") + } + if got, _ := l.Get(ctx, "o"); got.Status != Stopped || got.Orphaned { + t.Errorf("after Stop: %+v", got) + } +} + func TestConfined(t *testing.T) { b := &SSHFS{SFTPDriver: "builtin"} for _, c := range []struct { @@ -310,6 +387,35 @@ func TestConfined(t *testing.T) { } } +// The engine is written for Lima as patches/lima makes it: its patch level is +// named here, and a patch changes it with what the engine does of it. +func TestPatchLevel(t *testing.T) { + if limaPatches != "lima-7" { + t.Errorf("patch level %q", limaPatches) + } +} + +// A program not stamped with Lima's version gets the one its tree is made +// from, not "". +func TestLimaVersionDefault(t *testing.T) { + saved := limaversion.Version + t.Cleanup(func() { limaversion.Version = saved }) + limaversion.Version = "" + if _, err := NewLima(t.TempDir(), "", t.TempDir()); err != nil { + t.Fatal(err) + } + if limaversion.Version != limaversion.AgentVMUpstream || limaversion.Version == "" { + t.Errorf("version %q", limaversion.Version) + } + limaversion.Version = "v9.9.9" + if _, err := NewLima(t.TempDir(), "", t.TempDir()); err != nil { + t.Fatal(err) + } + if limaversion.Version != "v9.9.9" { + t.Errorf("a stamped version replaced: %q", limaversion.Version) + } +} + // The user's own LIMA_HOME is what NewLima found, not the one it sets. func TestUserLimaHome(t *testing.T) { oldSet, oldUser := limaHomeSet, userLimaHome @@ -321,14 +427,14 @@ func TestUserLimaHome(t *testing.T) { t.Errorf("before NewLima: %q", got) } ours := t.TempDir() - if _, err := NewLima(ours, t.TempDir()); err != nil { + if _, err := NewLima(ours, "", t.TempDir()); err != nil { t.Fatal(err) } if got := UserLimaHome(); got != user { t.Errorf("after NewLima: %q, want %q", got, user) } // A second one, as tests and migrations make, keeps it. - if _, err := NewLima(ours, t.TempDir()); err != nil { + if _, err := NewLima(ours, "", t.TempDir()); err != nil { t.Fatal(err) } if got := UserLimaHome(); got != user { @@ -337,7 +443,7 @@ func TestUserLimaHome(t *testing.T) { limaHomeSet, userLimaHome = "", "" os.Unsetenv("LIMA_HOME") - if _, err := NewLima(ours, t.TempDir()); err != nil { + if _, err := NewLima(ours, "", t.TempDir()); err != nil { t.Fatal(err) } if got := UserLimaHome(); got != "" { diff --git a/internal/vm/main_test.go b/internal/vm/main_test.go index 127b1331..b71c07f2 100644 --- a/internal/vm/main_test.go +++ b/internal/vm/main_test.go @@ -5,7 +5,7 @@ import ( "runtime" "testing" - limaversion "github.com/lima-vm/lima/v2/pkg/version" + limaversion "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" "github.com/sylvinus/agent-vm/internal/limaembed" ) diff --git a/internal/vm/migrate.go b/internal/vm/migrate.go index 60adaf15..a11521be 100644 --- a/internal/vm/migrate.go +++ b/internal/vm/migrate.go @@ -14,11 +14,12 @@ import ( continuityfs "github.com/containerd/continuity/fs" - "github.com/lima-vm/lima/v2/pkg/instance" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) // OldVM is a VM agent-vm 0.2 left in another Lima home. @@ -53,23 +54,60 @@ func FindOld(old, prefix string) ([]OldVM, error) { return out, nil } -// withLimaHome runs fn with LIMA_HOME set to home, as Lima's packages read -// it, and puts it back. +// withLimaHome runs fn with LIMA_HOME set to home and the VMs in it, as +// Lima's packages read them in the layout of 0.2 and 0.3.0, and puts the +// environment back. func withLimaHome(home string, fn func() error) error { saved, had := os.LookupEnv("LIMA_HOME") + savedVMs, hadVMs := os.LookupEnv(dirnames.InstancesEnv) if err := os.Setenv("LIMA_HOME", home); err != nil { return err } + os.Unsetenv(dirnames.InstancesEnv) defer func() { if had { os.Setenv("LIMA_HOME", saved) } else { os.Unsetenv("LIMA_HOME") } + if hadVMs { + os.Setenv(dirnames.InstancesEnv, savedVMs) + } else { + os.Unsetenv(dirnames.InstancesEnv) + } }() return fn() } +// LegacyRunning reports whether the VM name, kept in the Lima home home as +// 0.3.0 did, is running or half there: either way it cannot move. +func LegacyRunning(ctx context.Context, home, name string) bool { + running := true + _ = withLimaHome(home, func() error { + inst, err := store.Inspect(ctx, name) + running = err != nil || inst.Status != limatype.StatusStopped && inst.Config != nil + return nil + }) + return running +} + +// StopLegacy stops the VM name kept in the Lima home home as 0.3.0 did, +// through its hostagent, whichever Lima started it; by force when the +// hostagent is gone. +func StopLegacy(ctx context.Context, home, name string) error { + return withLimaHome(home, func() error { + inst, err := store.Inspect(ctx, name) + if err != nil { + return err + } + if inst.HostAgentPID == 0 { + instance.StopForcibly(inst) + return nil + } + return instance.StopGracefully(ctx, inst, false) + }) +} + // MoveOld moves the VM name from the Lima home old to the Lima home now, as // to: stopped first, through its hostagent, whichever Lima started it; then // its folder moved as Lima's rename moves one (no lock, pid, socket or diff --git a/internal/vm/migrate_test.go b/internal/vm/migrate_test.go index 473628ae..75ada4c3 100644 --- a/internal/vm/migrate_test.go +++ b/internal/vm/migrate_test.go @@ -9,9 +9,35 @@ import ( "testing" ) +// 0.3.0's VMs, in the Lima home: stopped ones may move, and a hostagent that +// died is stopped by force. +func TestLegacyRunning(t *testing.T) { + ctx := context.Background() + l := newLegacyLima(t) + home := os.Getenv("LIMA_HOME") + none := []Mount{} + if err := l.Create(ctx, "p-12345678", "template:debian-13", Settings{CPUs: 1, Mounts: &none}, io.Discard); err != nil { + t.Fatal(err) + } + if LegacyRunning(ctx, home, "p-12345678") { + t.Error("a stopped VM is running") + } + if !LegacyRunning(ctx, home, "missing") { + t.Error("a VM that cannot be read is not known to be stopped") + } + // No hostagent: stopped by force, which cleans up what a crash leaves. + if err := StopLegacy(ctx, home, "p-12345678"); err != nil { + t.Errorf("stop of a stopped VM: %v", err) + } + // The environment is as it was. + if os.Getenv("LIMA_HOME") != home { + t.Error("LIMA_HOME changed") + } +} + func TestMoveOld(t *testing.T) { ctx := context.Background() - l := newTestLima(t) + l := newLegacyLima(t) old := os.Getenv("LIMA_HOME") off := false proj := t.TempDir() @@ -54,7 +80,7 @@ func TestMoveOld(t *testing.T) { t.Error("a VM that is not agent-vm's was touched") } - l2, err := NewLima(now, t.TempDir()) + l2, err := NewLima(now, "", t.TempDir()) if err != nil { t.Fatal(err) } diff --git a/internal/vm/migrate_unix_test.go b/internal/vm/migrate_unix_test.go index e3dc0e7f..185eb6a9 100644 --- a/internal/vm/migrate_unix_test.go +++ b/internal/vm/migrate_unix_test.go @@ -22,7 +22,7 @@ func device(p string) (uint64, bool) { // To another file system: copied, then removed (rename cannot). func TestMoveOldAcrossFS(t *testing.T) { ctx := context.Background() - l := newTestLima(t) + l := newLegacyLima(t) old := os.Getenv("LIMA_HOME") from, _ := device(old) other := "" @@ -61,7 +61,7 @@ func TestMoveOldAcrossFS(t *testing.T) { if l, err := os.Readlink(filepath.Join(moved, "link")); err != nil || l != "lima.yaml" { t.Errorf("link: %q %v", l, err) } - l2, err := NewLima(now, t.TempDir()) + l2, err := NewLima(now, "", t.TempDir()) if err != nil { t.Fatal(err) } diff --git a/internal/vm/networkconfined.go b/internal/vm/networkconfined.go index c6fbf892..1c164ef0 100644 --- a/internal/vm/networkconfined.go +++ b/internal/vm/networkconfined.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) // ErrNetworkOpen is returned by Start for a VM with a network besides its own diff --git a/internal/vm/networkconfined_test.go b/internal/vm/networkconfined_test.go index 4bc33d24..409965ee 100644 --- a/internal/vm/networkconfined_test.go +++ b/internal/vm/networkconfined_test.go @@ -8,7 +8,7 @@ import ( "path/filepath" "testing" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) func TestNetworkConfined(t *testing.T) { @@ -39,7 +39,7 @@ func TestLimaStartRefusesNetworks(t *testing.T) { if err := l.Create(ctx, "n-12345678", "template:debian-13", Settings{Mounts: &none}, io.Discard); err != nil { t.Fatal(err) } - yamlPath := filepath.Join(os.Getenv("LIMA_HOME"), "n-12345678", "lima.yaml") + yamlPath := filepath.Join(os.Getenv("LIMA_INSTANCES_DIR"), "n-12345678", "lima", "lima.yaml") orig, _ := os.ReadFile(yamlPath) if err := os.WriteFile(yamlPath, append(orig, "\nnetworks:\n- lima: user-v2\n"...), 0o644); err != nil { t.Fatal(err) diff --git a/internal/vm/vm.go b/internal/vm/vm.go index 8104ac7b..03342ae8 100644 --- a/internal/vm/vm.go +++ b/internal/vm/vm.go @@ -61,6 +61,10 @@ type Instance struct { Mounts []Mount // ConfigErr is why the backend cannot read the VM's config (Broken). ConfigErr error + // Orphaned: Broken, the VM still running without the backend's process + // that serves it (Lima's hostagent). Nothing reaches it; Stop ends it by + // force. + Orphaned bool } // Mount is a host folder shared with the guest. @@ -140,7 +144,8 @@ type Backend interface { // gets the backend's progress. It refuses a VM whose shares are not // confined to their folders (see Confined). Start(ctx context.Context, name string, log io.Writer) error - // Stop shuts down the running VM name. + // Stop shuts down the running VM name, and kills an orphaned one. Any + // other VM is left as it is. Stop(ctx context.Context, name string) error // Delete removes the VM name, running or not. Delete(ctx context.Context, name string) error diff --git a/internal/vm/vmtest/fake.go b/internal/vm/vmtest/fake.go index 92f21f02..36ec3427 100644 --- a/internal/vm/vmtest/fake.go +++ b/internal/vm/vmtest/fake.go @@ -177,8 +177,8 @@ func (f *Fake) Stop(_ context.Context, name string) error { if err := f.call("stop %s", name); err != nil { return err } - if v, ok := f.VMs[name]; ok && !f.Sticky[name] { - v.Status = vm.Stopped + if v, ok := f.VMs[name]; ok && !f.Sticky[name] && (v.Status == vm.Running || v.Orphaned) { + v.Status, v.Orphaned = vm.Stopped, false } return nil } diff --git a/internal/vmname/vmname.go b/internal/vmname/vmname.go index c243c534..f263f3a4 100644 --- a/internal/vmname/vmname.go +++ b/internal/vmname/vmname.go @@ -88,6 +88,36 @@ func Scratch(dir string) (string, error) { return fit(name, "-scratch-"+hex.EncodeToString(b[:])), nil } +// MaxCustom is the longest name a user gives a VM. Lowercase, because macOS +// folders ignore case; 40 keeps .localhost (the address of a VM's +// editor) a DNS label, with room to spare. +const MaxCustom = 40 + +var ( + customRe = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`) + hashSuffixRe = regexp.MustCompile(`-[0-9a-f]{8}$`) +) + +// ValidateCustom says why name cannot be the name a user gives a VM, nil when +// it can: lowercase letters, digits and single dashes; at most MaxCustom +// characters; not ending in a dash and 8 hexadecimal digits, which every +// project's and scratch VM's name does, so that names cannot meet; not the +// base template's (Template) nor a name starting with "base-", the other +// templates' and the setup lock's. +func ValidateCustom(name string) error { + switch { + case !customRe.MatchString(name): + return fmt.Errorf("a name is lowercase letters, digits and single dashes, starting and ending with a letter or a digit (got: '%s')", name) + case len(name) > MaxCustom: + return fmt.Errorf("a name has at most %d characters (got: %d)", MaxCustom, len(name)) + case hashSuffixRe.MatchString(name): + return fmt.Errorf("a name does not end with a dash and 8 hexadecimal digits, as the VMs of projects do (got: '%s')", name) + case name == Template || strings.HasPrefix(name, Template+"-"): + return fmt.Errorf("'%s' is reserved for the base templates", name) + } + return nil +} + // IsScratch reports whether name looks like one Scratch makes. It only // looks like it: a project folder named "x-scratch" gets a VM named // "x-scratch-", which matches. Deleting a VM unasked takes more than diff --git a/internal/vmname/vmname_test.go b/internal/vmname/vmname_test.go index a08a3d04..20181a27 100644 --- a/internal/vmname/vmname_test.go +++ b/internal/vmname/vmname_test.go @@ -127,6 +127,51 @@ func TestScratchBash(t *testing.T) { } } +func TestValidateCustom(t *testing.T) { + for name, want := range map[string]string{ + "agent2": "", + "sandbox": "", + "a": "", + "my-vm-2": "", + "1a": "", + strings.Repeat("a", 40): "", + strings.Repeat("a", 41): "at most 40", + "": "lowercase", + "Agent": "lowercase", + "a_b": "lowercase", + "a.b": "lowercase", + "a b": "lowercase", + "-a": "lowercase", + "a-": "lowercase", + "a--b": "lowercase", + "é": "lowercase", + "a/b": "lowercase", + "proj-1a2b3c4d": "8 hexadecimal", + "x-scratch-0000aaaa": "8 hexadecimal", + "proj-1A2B3C4D": "lowercase", + "proj-1a2b3c4": "", + "proj-1a2b3c4de": "", + "base": "reserved", + "base-krunkit": "reserved", + "base-building": "reserved", + "basement": "", + "database": "", + "the-base": "", + "a-12345678-b": "", + "deadbeef": "", + "aaaa-deadbeef": "8 hexadecimal", + "aaaa-deadbeee-deadbeeff": "", + } { + err := ValidateCustom(name) + switch { + case want == "" && err != nil: + t.Errorf("%q: %v", name, err) + case want != "" && (err == nil || !strings.Contains(err.Error(), want)): + t.Errorf("%q: %v, want %q", name, err, want) + } + } +} + func TestIsScratch(t *testing.T) { for name, want := range map[string]bool{ "proj-scratch-0123abcd": true, diff --git a/packaging/nfpm.yaml b/packaging/nfpm.yaml new file mode 100644 index 00000000..71c74548 --- /dev/null +++ b/packaging/nfpm.yaml @@ -0,0 +1,48 @@ +# The .deb and .rpm of a Linux build, made by release.sh (linux_packages) with +# nfpm, run in the folder of that build's tarball. The variables come from +# release.sh. QEMU and ssh are recommended, not required: apt and dnf install +# recommends by default; RHEL and its rebuilds name QEMU qemu-kvm, and a QEMU +# from elsewhere on the PATH does too. `agent-vm setup` says what is missing. +name: agent-vm +arch: ${GOARCH} +platform: linux +version: ${VERSION} +release: "1" +section: utils +priority: optional +maintainer: Sylvain Zimmer +description: | + Run AI coding agents in a disposable Linux VM per project. + The VM gets the project directory and nothing else of the host's. +homepage: https://www.agent-vm.org/ +license: MIT + +contents: + - src: agent-vm + dst: /usr/bin/agent-vm + file_info: + mode: 0755 + - src: README.md + dst: /usr/share/doc/agent-vm/README.md + - src: CHANGELOG.md + dst: /usr/share/doc/agent-vm/CHANGELOG.md + - src: LICENSE + dst: /usr/share/doc/agent-vm/LICENSE + - src: NOTICE + dst: /usr/share/doc/agent-vm/NOTICE + - src: THIRD_PARTY_LICENSES + dst: /usr/share/doc/agent-vm/THIRD_PARTY_LICENSES + - src: runtime.example.sh + dst: /usr/share/doc/agent-vm/runtime.example.sh + +overrides: + deb: + recommends: + - ${QEMU_DEB} + - qemu-utils + - openssh-client + rpm: + recommends: + - ${QEMU_RPM} + - qemu-img + - openssh-clients diff --git a/patches/README.md b/patches/README.md index 5b7abe01..2a3777cd 100644 --- a/patches/README.md +++ b/patches/README.md @@ -1,7 +1,11 @@ # Patches to Lima, sshocker, pkg/sftp and gvisor-tap-vsock agent-vm builds Lima, sshocker, pkg/sftp and gvisor-tap-vsock from `third_party/`: each one is its upstream commit -(`third_party/SOURCES`) with the patches of this folder applied, in name order. Every patch is a +(`third_party/SOURCES`) with the patches of this folder applied, in name order. The result is a set of packages +of agent-vm's module: the sync drops each tree's `go.mod` and `go.sum`, rewrites the import paths to +`github.com/sylvinus/agent-vm/third_party/` (patches stay written against upstream's paths), and adds an +`agentvm_patches.go` with `AgentVMPatches`, the number of the last patch (`"lima-7"`), which agent-vm refers to. +Because they are packages and not replaced modules, a program importing agent-vm builds them patched. Every patch is a change meant for upstream, kept small enough to be sent as a PR on its own. Never edit `third_party/` by hand: change a patch, then `scripts/third-party-sync `. CI runs `scripts/third-party-sync all --check`. @@ -34,6 +38,7 @@ It runs inside the Lima hostagent, which with vz also runs the VM: a panic there | 18 | When the first sshfs mount fails, Lima retries with `-o nonempty` in place of the share's options. | The share is mounted again without `cache=no` (sshfs's attribute cache back on). | lima 0003 | | 19 | The host's event for a removal the guest asked for is relayed back as a host deletion. | The guest's next removal of a file it created again is taken for the relayed one and not done: the file stays on the host. | sshocker 0009 | | 20 | The client sets any mode bit, setuid, setgid and sticky included. | A file the host runs carries the privileges of its owner. | sshocker 0006 | +| 21 | `instance.StartWithPaths` starts the hostagent with `exec.CommandContext`: os/exec kills it when the caller's context ends, also after Start returned. | The end of the agent-vm command that started a VM kills its hostagent: QEMU runs on without it (Broken, every later start fails), vz VMs die. | lima 0006 | ## The patches @@ -44,6 +49,8 @@ It runs inside the Lima hostagent, which with vz also runs the VM: a panic there | `lima/0003-hostagent-keep-sshfs-options-on-retry` | lima-vm/lima | not sent | Bug 18: the retry adds `nonempty` to the options. No test: it needs a guest whose first mount fails. | | `lima/0004-portfwd-not-next-to-a-host-program` | lima-vm/lima | not sent | A guest port is forwarded only when no host program uses it: with `SO_REUSEADDR`, a listener on 127.0.0.1:P is allowed next to a host program's 0.0.0.0:P on macOS and BSD, and takes its local connections. The port is tried on 127.0.0.1, ::1, 0.0.0.0 and :: first (with `SO_EXCLUSIVEADDRUSE` on Windows); one in use gives a `failed` port-forward event. Tests `TestForwardNotNextToHostProgram`, `TestForwardFreePort`. | | `lima/0005-network-isolation-hooks` | lima-vm/lima | not sent | Hooks for agent-vm's network policy: `usernet.Outbound` and `usernet.AllowHostService` (the hostagent's DNS ports), `dns.Names`/`dns.Answered` (a name refused is NXDOMAIN, never looked up). QEMU gets its own netstack per instance, as vz has, so the policy applies to both, and its guest's DNS is the gateway's. QEMU is handed the connected socket, or on Windows, which cannot pass it, connects itself (`stream` netdev, QEMU 7.2 or later). `usernet.Names` and `usernet.Answered` reach the gateway's DNS server through gvisor-tap-vsock 0002, and a host service is allowed for its network and port on 127.0.0.1 only. Tests `TestNamesAnswered`, `TestOutboundHostServices`; the rest is covered by agent-vm's e2e (`internal/vm/lima_e2e_test.go`). | +| `lima/0006-instance-hostagent-outlives-start-context` | lima-vm/lima | not sent; the issue to open is the patch's header | Bug 21: `exec.Command`. No Lima test (its unit tests start no hostagent): agent-vm's e2e (`internal/cli/e2e_test.go`) fails without it. The header also reports the QEMU driver's "send on closed channel" panic that follows, not fixed here. | +| `lima/0007-instances-dir` | lima-vm/lima | not sent | `$LIMA_INSTANCES_DIR`: when set, the instance NAME is in `$LIMA_INSTANCES_DIR/NAME/lima`, and the embedding program keeps its own files for NAME beside it (agent-vm: `metadata.json`, `policy.json`). `dirnames.InstanceName` is the name of an instance dir, which templates' `{{.Name}}` read (it is no longer the base of the dir); `store.Instances` lists the folders holding an instance folder; Clone makes the parent folder. A `$LIMA_HOME` per VM would name every instance alike (hostnames, SSH aliases, the Windows drivers' distro and system names) and put the name twice in each socket path. Tests `TestInstancesDir`, `TestInstancesDirRelative`, `TestInstancesDirUnset`. | | `sshocker/0001-rooted-server-readonly-names` | lima-vm/sshocker | fork branch `readonly-names` | The builtin server serves only the mounted directory, and `ReadonlyNames`. | | `sshocker/0002-expect-remove` | lima-vm/sshocker | fork branch `expect-remove` | `ExpectRemove`, for 0002 of Lima. | | `sshocker/0003-append-fix` | lima-vm/sshocker | not sent | Bug 1. Tests `TestRootedOpenAppend`, `TestRootedOpenMode`. No pkg/sftp dependency: send first. | @@ -61,8 +68,8 @@ It runs inside the Lima hostagent, which with vz also runs the VM: a panic there | `gvisor-tap-vsock/0001-outbound-policy` | containers/gvisor-tap-vsock | not sent | `Configuration.Outbound`: the TCP and UDP forwarders refuse a destination it denies, checked after NAT (the gateway's address is the host's loopback by then). No test of its own: agent-vm's e2e connects to denied and allowed addresses. | | `gvisor-tap-vsock/0002-dns-policy` | containers/gvisor-tap-vsock | not sent | `Configuration.DNSNames` and `DNSAnswered`: the gateway's DNS server answers NXDOMAIN, over UDP and TCP, for a name `DNSNames` refuses, without asking the host's resolver, and tells `DNSAnswered` the IPv4 addresses it gave, so that `Outbound` can allow those only. Test `TestPolicy`. | -Lima's `0b63ae0` of the fork (`replace` of sshocker) is gone: agent-vm's `go.mod` replaces all -three modules. FSETSTAT through a read handle keeps the path checks (0004): a read handle can be +Lima's `0b63ae0` of the fork (`replace` of sshocker) is gone: the four trees are packages of +agent-vm's module. FSETSTAT through a read handle keeps the path checks (0004): a read handle can be opened through a symlink into `.git` (`TestRootedFsetstatReadHandle`). Every test was checked to fail with its fix removed. Keep that property when rebasing: revert the @@ -80,24 +87,23 @@ To change a patch: clone upstream at the commit of `SOURCES`, `git am` (or `git patches before it, make the change, and write the patch back with `git format-patch` or `git diff`. Rebasing on a new upstream: the same, from the new commit, then `--ref`. -Tests run from agent-vm's module root, so that each library is built with the others patched (run -inside `third_party/`, Go would use the unpatched dependencies of its own `go.mod`): +Tests run from agent-vm's module root (`make test-third-party`): ```bash -go test -vet=off -race github.com/pkg/sftp/... # pkg/sftp's vet fails on upstream code -go test -race github.com/lima-vm/sshocker/pkg/... -go test $(go list github.com/lima-vm/lima/v2/pkg/... | grep -v /mcp) -go test github.com/containers/gvisor-tap-vsock/pkg/... -tests/sftp/run # through real sshfs, see tests/sftp/run +go test -vet=off -race ./third_party/sftp/... # pkg/sftp's vet fails on upstream code +go test -race ./third_party/sshocker/pkg/... +go test ./third_party/lima/pkg/... +go test ./third_party/gvisor-tap-vsock/pkg/... +tests/sftp/run # through real sshfs, see tests/sftp/run ``` -Lima's MCP packages and sshocker's command are not part of agent-vm's build, so their -dependencies are not in its `go.sum`: they are left out above. gvisor-tap-vsock is synced -without its `vendor` and `tools` folders. +The folders that would add requirements nothing in agent-vm uses are left out (the last column of +`SOURCES`): Lima's MCP server, sshocker's command, and gvisor-tap-vsock's commands, `vendor`, +`tools` and test suites. Lima's `gomodjail` annotations went with its `go.mod`, and with them +`make gomodjail`, which agent-vm did not run. -`go test -fuzz` only fuzzes the main modules: `tests/fuzz/go.work` makes the vendored sshocker and -pkg/sftp ones (not Lima: its MCP server's requirements would take part in the build). `make fuzz` -runs every fuzzer of both and of agent-vm, `FUZZTIME` each. +A sync raises agent-vm's `go.mod` to the requirements of the upstream `go.mod` it drops, then runs +`go mod tidy`. `make fuzz` runs every fuzzer of the four trees and of agent-vm, `FUZZTIME` each. ### Through real sshfs @@ -131,7 +137,7 @@ pkg/sftp merges rarely and some of these patches add API, so split what needs no another's code. - **sshocker, after a pkg/sftp release:** the rest of 0004 and 0005. `O_APPEND` waits too: without sequential processing two appends can land in either order. -- **Lima:** once sshocker has released, #5531 needs no `replace`. +- **Lima:** once sshocker has released, #5531 needs no vendored sshocker. Each patch merged upstream is dropped from here when `SOURCES` moves past it. Never push, tag or open a PR without the owner's go-ahead. diff --git a/patches/lima/0006-instance-hostagent-outlives-start-context.patch b/patches/lima/0006-instance-hostagent-outlives-start-context.patch new file mode 100644 index 00000000..a62fd053 --- /dev/null +++ b/patches/lima/0006-instance-hostagent-outlives-start-context.patch @@ -0,0 +1,76 @@ +Subject: [PATCH] instance: do not kill the hostagent when Start's context ends + +StartWithPaths starts the hostagent with exec.CommandContext(ctx, ...). +The hostagent is meant to outlive the call ("leave the hostagent process +running" when Start returns), but os/exec kills it as soon as ctx is +done, also long after Start returned. + +limactl does not show it: its process exits right after Start, before +anything cancels the context, and a Ctrl-C kills limactl without +cancelling it. A Go program that uses pkg/instance does: it cancels the +context it passed (a deferred cancel, a signal handler, a request scope) +while it keeps running, and the hostagent is killed then. When the +cancel comes just before the program exits, os/exec's watcher races the +exit: the instance breaks sometimes, depending on the machine. + +What follows depends on the driver: + +- qemu: QEMU is a child of the hostagent, not killed with it. It keeps + running, reparented to init, unreachable (no hostagent, so no SSH + forward or usernet), and holds the lock on its PID file. The instance + is Broken: "qemu driver is running but host agent is not". The next + start fails: QEMU exits at once ("cannot create PID file: Cannot lock + pid file"), the QEMU driver panics (below), and limactl reports "host + agent process has exited: exit status 2". `limactl stop -f` recovers. +- vz: the VM runs in the hostagent, so it is killed with it, without a + shutdown (not tested). + +Reproduce, from a Go program, on Linux with the qemu driver: + + ctx, cancel := context.WithCancel(context.Background()) + err := instance.Start(ctx, inst, false, false) // nil, instance Running + cancel() + time.Sleep(time.Second) + // store.Inspect: Broken; `ps -o ppid= -C qemu-system-x86_64`: 1 + +Found in agent-vm, which embeds Lima: on Linux hosts its VMs became +Broken a few seconds to minutes after a start, when the command that +started them ended (closed terminal, SIGTERM, or a plain exit losing the +race). Tested there: with this patch the VM stays Running after the +starting program ends, cancelled context included, and the next start +works. + +The fix: exec.Command. ctx still bounds the wait for the hostagent to +come up (waitHostAgentStart, watchHostAgentEvents). A start cancelled +during that wait leaves the hostagent running, as an interrupted limactl +start does (the hostagent has its own process group, +BackgroundSysProcAttr, and the terminal's Ctrl-C does not reach it). + +No unit test: Lima's unit tests never start a hostagent. agent-vm's e2e +test (internal/cli/e2e_test.go) fails without it. + +Separate bug, to report on its own: the panic of the QEMU driver, +"panic: send on closed channel" in LimaQemuDriver.Start.func5 +(pkg/driver/qemu/qemu_driver.go). When QEMU exits early, the goroutine +waiting on it closes qWaitCh and cancels the driver's context; the +ConfigureDriver goroutine then fails on that context and sends its +error on the closed qWaitCh. Already seen in #4645 (closed on its +Colima-specific part, the panic itself left unfixed). Sending with a +select on a done channel, or one closer for both goroutines, fixes it. + +Assisted-by: Claude Opus 5.5 +--- +diff --git a/pkg/instance/start.go b/pkg/instance/start.go +--- a/pkg/instance/start.go ++++ b/pkg/instance/start.go +@@ -291,7 +291,9 @@ + args = append(args, "--progress") + } + args = append(args, inst.Name) +- haCmd = exec.CommandContext(ctx, limactl, args...) ++ // Not exec.CommandContext: the hostagent outlives this call, and os/exec ++ // would kill it whenever ctx ends, even after Start returned. ++ haCmd = exec.Command(limactl, args...) + + haCmd.SysProcAttr = executil.BackgroundSysProcAttr + diff --git a/patches/lima/0007-instances-dir.patch b/patches/lima/0007-instances-dir.patch new file mode 100644 index 00000000..af7f36bd --- /dev/null +++ b/patches/lima/0007-instances-dir.patch @@ -0,0 +1,278 @@ +Subject: [PATCH] dirnames: keep the instances in $LIMA_INSTANCES_DIR//lima + +A program that embeds Lima keeps its own files for each instance (its +settings, markers, staged files). With every instance at $LIMA_HOME/ +they sit in another folder, and deleting an instance has to remove them +apart, so that one can outlive its instance. + +When $LIMA_INSTANCES_DIR is set, the instance is kept in +$LIMA_INSTANCES_DIR//lima, and the program owns the rest of +$LIMA_INSTANCES_DIR/. $LIMA_HOME keeps what is shared (_config, +_networks, _disks, _templates). Unset, nothing changes. + +A $LIMA_HOME per instance would do the same, but every instance would +then be named by the same folder: the guest hostnames and the SSH aliases +come from the instance name, and the Windows drivers name their WSL2 +distro and HCS system after it. Naming the home after the instance +instead puts its name twice in the path of every socket, which the +sockets' length limit cannot afford (104 characters on macOS). + +- dirnames.InstanceDir returns the new path, and dirnames.InstanceName is + the name of an instance dir: the name is no longer the dir's base. + Templates' {{.Name}} read it. +- store.Instances lists the folders that hold an instance folder, and + store.Validate follows. +- Clone makes the parent folder of the new instance. + +Tests TestInstancesDir, TestInstancesDirRelative, TestInstancesDirUnset. + +--- +diff -ruN a/pkg/instance/clone.go b/pkg/instance/clone.go +--- a/pkg/instance/clone.go 2026-10-20 00:21:33.801616607 +0200 ++++ b/pkg/instance/clone.go 2026-10-20 00:22:24.894177976 +0200 +@@ -53,6 +53,10 @@ + newInstName, maxSockName, osutil.UnixPathMax, len(maxSockName)) + } + ++ // Not LimaDir's own subfolder when $LIMA_INSTANCES_DIR is set. ++ if err = os.MkdirAll(filepath.Dir(newInstDir), 0o700); err != nil { ++ return nil, err ++ } + if err = os.Mkdir(newInstDir, 0o700); err != nil { + return nil, err + } +diff -ruN a/pkg/limatype/dirnames/dirnames.go b/pkg/limatype/dirnames/dirnames.go +--- a/pkg/limatype/dirnames/dirnames.go 2026-10-20 00:21:33.801616607 +0200 ++++ b/pkg/limatype/dirnames/dirnames.go 2026-10-20 00:21:49.238043200 +0200 +@@ -77,12 +77,49 @@ + return filepath.Join(limaDir, filenames.TemplatesDir), nil + } + ++// InstancesEnv is the environment variable that moves the instances out of ++// $LIMA_HOME. When it is set, the instance NAME is in ++// $LIMA_INSTANCES_DIR/NAME/InstanceSubdir, and a program embedding Lima can ++// keep its own files for NAME in $LIMA_INSTANCES_DIR/NAME. $LIMA_HOME keeps ++// the rest (_config, _networks, _disks, _templates). ++const InstancesEnv = "LIMA_INSTANCES_DIR" ++ ++// InstanceSubdir is the instance's folder in $LIMA_INSTANCES_DIR/NAME. ++const InstanceSubdir = "lima" ++ ++// InstancesDir returns the absolute path of $LIMA_INSTANCES_DIR, or "" when ++// it is not set and the instances are in LimaDir. ++func InstancesDir() (string, error) { ++ dir := os.Getenv(InstancesEnv) ++ if dir == "" { ++ return "", nil ++ } ++ if !filepath.IsAbs(dir) { ++ return "", fmt.Errorf("$%s must be an absolute path, got %#q", InstancesEnv, dir) ++ } ++ if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) { ++ return dir, nil ++ } ++ realdir, err := filepath.EvalSymlinks(dir) ++ if err != nil { ++ return "", fmt.Errorf("cannot evaluate symlinks in %#q: %w", dir, err) ++ } ++ return realdir, nil ++} ++ + // InstanceDir returns the instance dir. + // InstanceDir does not check whether the instance exists. + func InstanceDir(name string) (string, error) { + if err := ValidateInstName(name); err != nil { + return "", err + } ++ instances, err := InstancesDir() ++ if err != nil { ++ return "", err ++ } ++ if instances != "" { ++ return filepath.Join(instances, name, InstanceSubdir), nil ++ } + limaDir, err := LimaDir() + if err != nil { + return "", err +@@ -91,6 +128,14 @@ + return dir, nil + } + ++// InstanceName returns the name of the instance whose dir is instDir. ++func InstanceName(instDir string) string { ++ if os.Getenv(InstancesEnv) != "" { ++ return filepath.Base(filepath.Dir(instDir)) ++ } ++ return filepath.Base(instDir) ++} ++ + // ValidateInstName checks if the name is a valid instance name. For this it needs to + // be a valid identifier, and not end in .yml or .yaml (case insensitively). + func ValidateInstName(name string) error { +diff -ruN a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go +--- a/pkg/limayaml/defaults.go 2026-10-20 00:21:33.936930219 +0200 ++++ b/pkg/limayaml/defaults.go 2026-10-20 00:22:17.833097176 +0200 +@@ -955,7 +955,7 @@ + func executeGuestTemplate(format, instDir string, user limatype.User, param map[string]string) (bytes.Buffer, error) { + tmpl, err := template.New("").Parse(format) + if err == nil { +- name := filepath.Base(instDir) ++ name := dirnames.InstanceName(instDir) + data := map[string]any{ + "Name": name, + "Hostname": hostname.FromInstName(name), // TODO: support customization +@@ -985,7 +985,7 @@ + } + data := map[string]any{ + "Dir": instDir, +- "Name": filepath.Base(instDir), ++ "Name": dirnames.InstanceName(instDir), + // TODO: add hostname fields for the host and the guest + "UID": currentUser.Uid, + "User": currentUser.Username, +@@ -994,8 +994,8 @@ + "GlobalTempDir": globalTempDir, + "TempDir": os.TempDir(), + +- "Instance": filepath.Base(instDir), // DEPRECATED, use `{{.Name}}` +- "LimaHome": limaHome, // DEPRECATED, use `{{.Dir}}` instead of `{{.LimaHome}}/{{.Instance}}` ++ "Instance": dirnames.InstanceName(instDir), // DEPRECATED, use `{{.Name}}` ++ "LimaHome": limaHome, // DEPRECATED, use `{{.Dir}}` instead of `{{.LimaHome}}/{{.Instance}}` + } + var out bytes.Buffer + err = tmpl.Execute(&out, data) +diff -ruN a/pkg/store/instances_dir_test.go b/pkg/store/instances_dir_test.go +--- a/pkg/store/instances_dir_test.go 1970-01-01 01:00:00.000000000 +0100 ++++ b/pkg/store/instances_dir_test.go 2026-10-20 00:22:42.400466431 +0200 +@@ -0,0 +1,71 @@ ++// SPDX-FileCopyrightText: Copyright The Lima Authors ++// SPDX-License-Identifier: Apache-2.0 ++ ++package store ++ ++import ( ++ "os" ++ "path/filepath" ++ "testing" ++ ++ "gotest.tools/v3/assert" ++ ++ "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" ++ "github.com/lima-vm/lima/v2/pkg/limatype/filenames" ++) ++ ++func TestInstancesDir(t *testing.T) { ++ home := t.TempDir() ++ instances := t.TempDir() ++ t.Setenv("LIMA_HOME", home) ++ t.Setenv(dirnames.InstancesEnv, instances) ++ realInstances, err := filepath.EvalSymlinks(instances) ++ assert.NilError(t, err) ++ ++ dir, err := dirnames.InstanceDir("vm1") ++ assert.NilError(t, err) ++ assert.Equal(t, dir, filepath.Join(realInstances, "vm1", dirnames.InstanceSubdir)) ++ assert.Equal(t, dirnames.InstanceName(dir), "vm1") ++ ++ // A folder of the caller's without an instance folder is not an instance. ++ assert.NilError(t, os.MkdirAll(filepath.Join(instances, "unfinished"), 0o755)) ++ assert.NilError(t, os.MkdirAll(filepath.Join(instances, "vm1", dirnames.InstanceSubdir), 0o755)) ++ assert.NilError(t, os.MkdirAll(filepath.Join(instances, "vm2", dirnames.InstanceSubdir), 0o755)) ++ // An instance in LIMA_HOME is not seen. ++ assert.NilError(t, os.MkdirAll(filepath.Join(home, "vm3"), 0o755)) ++ ++ names, err := Instances() ++ assert.NilError(t, err) ++ assert.DeepEqual(t, names, []string{"vm1", "vm2"}) ++ ++ // Validate looks for lima.yaml in the instance folder. ++ assert.ErrorContains(t, Validate(), filenames.LimaYAML) ++ for _, n := range names { ++ assert.NilError(t, os.WriteFile(filepath.Join(instances, n, dirnames.InstanceSubdir, filenames.LimaYAML), nil, 0o644)) ++ } ++ assert.NilError(t, Validate()) ++ ++ // LIMA_HOME keeps the rest. ++ cfg, err := dirnames.LimaConfigDir() ++ assert.NilError(t, err) ++ assert.Equal(t, filepath.Dir(cfg), func() string { r, _ := filepath.EvalSymlinks(home); return r }()) ++} ++ ++func TestInstancesDirRelative(t *testing.T) { ++ t.Setenv(dirnames.InstancesEnv, "relative/path") ++ _, err := dirnames.InstanceDir("vm1") ++ assert.ErrorContains(t, err, "absolute") ++} ++ ++func TestInstancesDirUnset(t *testing.T) { ++ home := t.TempDir() ++ t.Setenv("LIMA_HOME", home) ++ t.Setenv(dirnames.InstancesEnv, "") ++ realHome, err := filepath.EvalSymlinks(home) ++ assert.NilError(t, err) ++ ++ dir, err := dirnames.InstanceDir("vm1") ++ assert.NilError(t, err) ++ assert.Equal(t, dir, filepath.Join(realHome, "vm1")) ++ assert.Equal(t, dirnames.InstanceName(dir), "vm1") ++} +diff -ruN a/pkg/store/store.go b/pkg/store/store.go +--- a/pkg/store/store.go 2026-10-20 00:21:33.813917845 +0200 ++++ b/pkg/store/store.go 2026-10-20 00:22:01.926530032 +0200 +@@ -29,17 +29,16 @@ + + // Validate checks the LimaDir. + func Validate() error { +- limaDir, err := dirnames.LimaDir() +- if err != nil { +- return err +- } + names, err := Instances() + if err != nil { + return err + } + for _, name := range names { + // Each instance directory needs to have limayaml +- instDir := filepath.Join(limaDir, name) ++ instDir, err := dirnames.InstanceDir(name) ++ if err != nil { ++ return err ++ } + yamlPath := filepath.Join(instDir, filenames.LimaYAML) + if _, err := os.Stat(yamlPath); err != nil { + return err +@@ -48,12 +47,20 @@ + return nil + } + +-// Instances returns the names of the instances under LimaDir. ++// Instances returns the names of the instances under LimaDir, or under ++// $LIMA_INSTANCES_DIR when it is set (see dirnames.InstancesEnv): there, a ++// folder without an instance folder in it is not one. + func Instances() ([]string, error) { +- limaDir, err := dirnames.LimaDir() ++ instancesDir, err := dirnames.InstancesDir() + if err != nil { + return nil, err + } ++ limaDir := instancesDir ++ if limaDir == "" { ++ if limaDir, err = dirnames.LimaDir(); err != nil { ++ return nil, err ++ } ++ } + limaDirList, err := os.ReadDir(limaDir) + if err != nil { + if errors.Is(err, os.ErrNotExist) { +@@ -69,6 +76,11 @@ + if !f.IsDir() { + continue + } ++ if instancesDir != "" { ++ if st, err := os.Stat(filepath.Join(limaDir, f.Name(), dirnames.InstanceSubdir)); err != nil || !st.IsDir() { ++ continue ++ } ++ } + names = append(names, f.Name()) + } + return names, nil diff --git a/policy/policy.go b/policy/policy.go new file mode 100644 index 00000000..b87f539f --- /dev/null +++ b/policy/policy.go @@ -0,0 +1,59 @@ +// Package policy holds what a VM may reach and what it may not change +// unasked, as values, and the parsers of agent-vm's files that make them. +// A program that embeds agentvm gives them as Options.Network and +// Options.Guarded; the engine writes them to each VM's folder when it boots, +// where the VM's host agent reads them. +// +// This package is experimental until a program other than agent-vm has used +// it. +package policy + +import ( + "io" + "net/netip" + + "github.com/sylvinus/agent-vm/internal/guard" + "github.com/sylvinus/agent-vm/internal/netguard" +) + +// Network is what the VMs may reach. The zero Network is the default: the +// internet only, never the host, its local networks or the other VMs. +// +// - Open turns the isolation off: the VMs reach the host and its networks. +// Unsafe; for a program that has to. +// - Allow lists addresses and networks reachable whatever else, on every +// port or one. +// - Domains, when any, are the only names of the internet the VMs may reach, +// subdomains included. Experimental: do not rely on it alone to confine a +// VM. +type Network = netguard.Policy + +// Rule is an allow line: a network, and a port or 0 for every one. +type Rule = netguard.Rule + +// NewRule allows addr, a port of it (0: every port). +func NewRule(prefix netip.Prefix, port uint16) Rule { return Rule{Net: prefix, Port: port} } + +// Guarded are the files the host runs without asking (direnv's .envrc, the +// editor's tasks, git hooks' config) that a VM may not write unasked: Paths, +// relative, each with why it matters ("" for one the program added), matched +// anywhere in a written path as whole names. Deny refuses what is guarded +// without a dialog, for a program no one is at. +type Guarded = guard.Config + +// DefaultGuarded is agent-vm's list. +func DefaultGuarded() Guarded { return guard.DefaultConfig() } + +// ParseNetwork reads agent-vm's network file: `allow 10.0.0.5:8080`, `allow +// 192.168.1.0/24`, `allow localhost:11434` and `domain example.com` lines, +// with comments. name is the file's, for the messages. +func ParseNetwork(r io.Reader, name string) (*Network, error) { + return netguard.Parse(r, name) +} + +// ParseGuarded adds the lines of agent-vm's guarded file to g (the paths +// already there, DefaultGuarded's say, stay): one path a line, `!path` +// removes one. +func ParseGuarded(r io.Reader, g *Guarded) error { + return g.ParseList(r) +} diff --git a/agent-vm.setup.sh b/provision/agent-vm.setup.sh similarity index 100% rename from agent-vm.setup.sh rename to provision/agent-vm.setup.sh diff --git a/provision/provision.go b/provision/provision.go new file mode 100644 index 00000000..2df043c2 --- /dev/null +++ b/provision/provision.go @@ -0,0 +1,170 @@ +// Package provision describes how a base template is built: the VM every +// project's VM is cloned from. A Template is a name, the resources of the VM +// it is built in, and scripts run in it as it boots for the first time. +// AgentVM is agent-vm's own: dev tools, Docker, Chromium and the coding agents. +// +// This package is experimental until a program other than agent-vm has used +// it. +package provision + +import ( + _ "embed" + "fmt" + "regexp" + "strings" +) + +// Script is a program run in the build VM, fed through its standard input. +type Script struct { + // Name labels it in messages and errors. + Name string + // Message is told before it runs ("Installing packages..."). + Message string + // Shell is the command that runs the content, in the VM, as the user + // of the VM: {"bash", "-l"}. + Shell []string + // Content is the script, written to the standard input of Shell, after + // the Env of its Template as `export` lines. + Content string + // Env are more KEY=value pairs exported before Content, after the + // Template's. + Env []string +} + +// Template is a base template to build. +// +// What the engine expects of the guest the scripts make: a user who can +// sudo; zsh, through which it runs commands (a login shell, from the project's +// folder) and the runtime scripts; and, for the env file it pushes +// (~/.agent-vm.env, written on each start) to reach those commands, a +// ~/.zshenv that sources it. AgentVM does all of it. +type Template struct { + // Name is the template's VM name: "base" by default for the engine. + // Reserved: "base" and names starting with "base-". + Name string + // CPUs, MemoryGiB and DiskGiB are the resources of the build VM, and so + // of every VM cloned from it unless they ask for others. 0: the engine's + // defaults (1 CPU, 3 GiB of memory, 10 GiB of disk). + CPUs, MemoryGiB, DiskGiB int + // Scripts run in order. A failing one stops the build, and the template + // from before is kept. + Scripts []Script + // Env are KEY=value pairs exported before every script. + Env []string + // Preinstall records the software the scripts install, by name, for + // the programs that show it. The scripts are what installs. + Preinstall []string +} + +// Software is what agent-vm.setup.sh installs, by name (it reads +// AGENT_VM_INSTALL_). +var Software = []string{"python", "node", "ruby", "rust", "golang", "docker", "chromium", "gh", "claude", "opencode", "codex", "vibe", "pi", + "code-server", "code-claude", "code-codex", "code-vibe", "mcp-chrome", "mcp-playwright"} + +// DefaultSoftware is the default set. Opt-in: Ruby, Rust, Go, Pi (released +// several times a week), Playwright MCP (a second browser-driving server, +// whose tools cost context in every agent), and code-server with its +// extensions (an editor most use on the host). +var DefaultSoftware = []string{"python", "node", "docker", "chromium", "gh", "claude", "opencode", "codex", "vibe", "mcp-chrome"} + +//go:embed agent-vm.setup.sh +var agentVMScript string + +// AgentVMScript is agent-vm's setup script, for the programs that run it +// themselves. +func AgentVMScript() string { return agentVMScript } + +// AgentVM is agent-vm's template: its setup script with the software named +// installed (all of DefaultSoftware if none is named). Names are those of +// Software; "default", "all" and "none" are the sets. +func AgentVM(preinstall ...string) (Template, error) { + in := map[string]bool{} + if len(preinstall) == 0 { + preinstall = []string{"default"} + } + for _, f := range preinstall { + switch { + case f == "": + case f == "all": + set(in, Software...) + case f == "default": + set(in, DefaultSoftware...) + case f == "none": + case f == "code-claude" || f == "code-codex" || f == "code-vibe": + // An extension needs the editor. + set(in, f, "code-server") + case contains(Software, f): + set(in, f) + default: + return Template{}, fmt.Errorf("unknown software %q (names are lowercase): %s, default, all, none", f, strings.Join(Software, ", ")) + } + } + if r := NodeNeededBy(in); r != "" { + in["node"] = true + } + return AgentVMChoices(in), nil +} + +// AgentVMChoices is agent-vm's template for the software chosen, by name, +// as is: Node.js is not added when a choice needs it (see NodeNeededBy). +func AgentVMChoices(in map[string]bool) Template { + s := Script{Name: "agent-vm.setup.sh", Message: "Installing packages inside VM...", Shell: []string{"bash", "-l"}, Content: agentVMScript} + t := Template{Name: "base"} + for _, n := range Software { + v := 0 + if in[n] { + v = 1 + t.Preinstall = append(t.Preinstall, n) + } + s.Env = append(s.Env, fmt.Sprintf("AGENT_VM_INSTALL_%s=%d", strings.ToUpper(strings.ReplaceAll(n, "-", "_")), v)) + } + t.Scripts = []Script{s} + return t +} + +// NodeNeededBy says why the choices need Node.js, if they do: Codex and Pi +// install with npm, Chrome DevTools MCP runs with npx. +func NodeNeededBy(in map[string]bool) string { + switch { + case in["codex"]: + return "Codex CLI requires npm" + case in["pi"]: + return "Pi requires npm" + case in["chromium"] && in["mcp-chrome"] && (in["claude"] || in["opencode"] || in["vibe"] || in["code-claude"] || in["code-codex"] || in["code-vibe"]): + return "Chrome DevTools MCP uses npx" + } + return "" +} + +func set(in map[string]bool, names ...string) { + for _, n := range names { + in[n] = true + } +} + +func contains(s []string, v string) bool { + for _, x := range s { + if x == v { + return true + } + } + return false +} + +var plain = regexp.MustCompile(`^[A-Za-z0-9_./:@%+=,-]*$`) + +// Exports are the pairs as `export` lines, for the standard input of a +// script: a value with anything a shell reads is single-quoted. +func Exports(pairs ...[]string) string { + var b strings.Builder + for _, ps := range pairs { + for _, p := range ps { + k, v, _ := strings.Cut(p, "=") + if !plain.MatchString(v) { + v = "'" + strings.ReplaceAll(v, "'", `'\''`) + "'" + } + fmt.Fprintf(&b, "export %s=%s\n", k, v) + } + } + return b.String() +} diff --git a/release.sh b/release.sh index 42764026..4adfa5cd 100755 --- a/release.sh +++ b/release.sh @@ -30,7 +30,11 @@ # SHA256SUMS. Each tarball holds that platform's binary, agent-vm, built # here from the tag, agent-vm.sh, and LICENSE, NOTICE and # THIRD_PARTY_LICENSES: the curl installer takes the one of the -# machine, unpacks it and runs `agent-vm.sh install`. The release is +# machine, unpacks it and runs `agent-vm.sh install`. The Linux builds +# also come as agent-vm_X.Y.Z_.deb and agent-vm-X.Y.Z-1..rpm +# (packaging/nfpm.yaml; nfpm is built with Go at a pinned version, or +# AGENT_VM_NFPM names a binary), listed in SHA256SUMS after the tarballs. +# The release is # created as a draft, its assets are downloaded back and compared with # the local ones, and only then is it published; then the published # assets are downloaded once more and checked against SHA256SUMS (and @@ -153,9 +157,45 @@ package() { "$src/README.md" "$src/CHANGELOG.md" "$src/runtime.example.sh" "$stage/" [ "$os" != windows ] || [ -z "${AGENT_VM_WINDOWS_PFX:-}" ] || sign_windows "$stage/$bin" "$out" make_tarball "$out/agent-vm-$version-$p.tar.gz" "$out/stage-$p" "agent-vm-$version" + [ "$os" != linux ] || linux_packages "$ref" "$version" "$out" "$arch" "$stage" done } +# nfpm, which makes the .deb and .rpm: AGENT_VM_NFPM names a binary, else the +# pinned version is built with Go (once, into $WORK/bin). +NFPM_VERSION=v2.48.0 +nfpm_bin() { + if [ -n "${AGENT_VM_NFPM:-}" ]; then echo "$AGENT_VM_NFPM"; return; fi + local bin="$WORK/bin/nfpm" + if [ ! -x "$bin" ]; then + ( cd "$WORK" && GOWORK=off GOBIN="$WORK/bin" go install "github.com/goreleaser/nfpm/v2/cmd/nfpm@$NFPM_VERSION" ) >"$WORK/nfpm-build.log" 2>&1 \ + || { tail -n 20 "$WORK/nfpm-build.log" >&2; die "could not build nfpm $NFPM_VERSION (or set AGENT_VM_NFPM)"; } + fi + echo "$bin" +} + +# linux_packages : in outdir, +# agent-vm__.deb and agent-vm--1..rpm, from +# packaging/nfpm.yaml and the files of the tarball's folder. The dates inside +# are the commit's, so the same commit gives the same packages. +linux_packages() { + local ref="$1" version="$2" out="$3" goarch="$4" stage="$5" nfpm rpmarch qdeb qrpm + case "$goarch" in + amd64) rpmarch=x86_64; qdeb=qemu-system-x86; qrpm=qemu-system-x86-core ;; + arm64) rpmarch=aarch64; qdeb=qemu-system-arm; qrpm=qemu-system-aarch64-core ;; + *) die "no .deb or .rpm for $goarch" ;; + esac + nfpm="$(nfpm_bin)" + echo " packaging linux-$goarch (.deb, .rpm)" + local vars=(GOARCH="$goarch" VERSION="$version" QEMU_DEB="$qdeb" QEMU_RPM="$qrpm" + SOURCE_DATE_EPOCH="$(git -C "$REPO_DIR" log -1 --format=%ct "$ref")") + ( cd "$stage" \ + && env "${vars[@]}" "$nfpm" package -f "$out/src/packaging/nfpm.yaml" -p deb -t "$out/agent-vm_${version}_${goarch}.deb" \ + && env "${vars[@]}" "$nfpm" package -f "$out/src/packaging/nfpm.yaml" -p rpm -t "$out/agent-vm-${version}-1.${rpmarch}.rpm" ) \ + >"$out/nfpm-$goarch.log" 2>&1 || { tail -n 20 "$out/nfpm-$goarch.log" >&2; die "packaging linux-$goarch failed: $out/nfpm-$goarch.log"; } + printf '%s\n' "agent-vm_${version}_${goarch}.deb" "agent-vm-${version}-1.${rpmarch}.rpm" >> "$out/packages.list" +} + # make_tarball : of as a tarball owned by # root (numeric ids), with no extended attributes and no AppleDouble files # (macOS's bsdtar adds both), and a gzip header without a date. GNU tar and @@ -412,6 +452,17 @@ for f in LICENSE NOTICE THIRD_PARTY_LICENSES; do done ok "LICENSE, NOTICE and THIRD_PARTY_LICENSES are there" +# flake.nix's vendorHash is of the Go dependencies: it notes the checksum of +# go.mod and go.sum it was made for. +if [ -f flake.nix ]; then + ! grep -q '^[[:space:]]*vendorHash = pkgs.lib.fakeHash;' flake.nix \ + || die "flake.nix's vendorHash is pkgs.lib.fakeHash: build with Nix and put the hash it prints there (see the comment)" + mods="$(cat go.mod go.sum | sha256_of /dev/stdin)" + grep -q "go.mod+go.sum: $mods\$" flake.nix \ + || die "flake.nix's vendorHash is not for this go.mod and go.sum: update it and its 'go.mod+go.sum: $mods' line (see the comment there)" + ok "flake.nix's vendorHash is for this go.mod and go.sum" +fi + # --- 3. tests ------------------------------------------------------------------ # CI is the authority: it runs on Linux, macOS and Windows. # --bypass-checks skips the workflow only: the checks above decide what the @@ -440,7 +491,7 @@ case "$ci" in running) die "the CI workflow is still running on ${head:0:12}" ;; *) die "unexpected CI state for ${head:0:12}: $ci" ;; esac -for f in agent-vm.sh agent-vm.setup.sh runtime.example.sh release.sh tests/*.sh scripts/*; do +for f in agent-vm.sh provision/agent-vm.setup.sh runtime.example.sh release.sh tests/*.sh scripts/*; do bash -n "$f" || die "syntax error in $f" done sh -n www/public/install.sh || die "syntax error in www/public/install.sh" @@ -490,6 +541,16 @@ for p in $PLATFORMS; do assets+=("$WORK/$f") ok "$f: $(du -h "$WORK/$f" | cut -f1), sha256 $sha" done +# The Linux packages, after the tarballs: the installer reads only the +# tarballs' lines of SHA256SUMS. +if [ -f "$WORK/packages.list" ]; then + while read -r f; do + sha="$(sha256_of "$WORK/$f")" + printf '%s %s\n' "$sha" "$f" >> "$WORK/SHA256SUMS" + assets+=("$WORK/$f") + ok "$f: $(du -h "$WORK/$f" | cut -f1), sha256 $sha" + done < "$WORK/packages.list" +fi if [ -n "${AGENT_VM_MINISIGN_KEY:-}" ]; then # Prompts for the key's password, on the terminal. if [ -z "$DRY_RUN" ]; then @@ -529,6 +590,7 @@ SMOKE_FAILED="" echo echo "Homebrew formula, per platform (each tarball's agent-vm is the binary):" while read -r sha f; do + case "$f" in *.tar.gz) ;; *) continue ;; esac echo " url \"$BASE/$f\"" echo " sha256 \"$sha\"" done < "$WORK/SHA256SUMS" @@ -545,7 +607,7 @@ if [ -n "${AGENT_VM_TAP:-}" ]; then # sha256 after it that tarball's. A `bottle do` block has sha256 lines # of its own, after no url: left alone. awk -v base="$BASE" -v version="$VERSION" -v sums="$WORK/SHA256SUMS" ' - BEGIN { while ((getline l < sums) > 0) { split(l, a, " "); p = a[2]; sub(/^agent-vm-[0-9.]+-/, "", p); sub(/\.tar\.gz$/, "", p); sha[p] = a[1] } } + BEGIN { while ((getline l < sums) > 0) { split(l, a, " "); p = a[2]; if (p !~ /\.tar\.gz$/) continue; sub(/^agent-vm-[0-9.]+-/, "", p); sub(/\.tar\.gz$/, "", p); sha[p] = a[1] } } /^[[:space:]]*url / { for (p in sha) if (index($0, "-" p ".tar.gz")) { sub(/url .*/, "url \"" base "/agent-vm-" version "-" p ".tar.gz\""); want = p diff --git a/scripts/licenses b/scripts/licenses index ca5e0bb2..bd53522e 100755 --- a/scripts/licenses +++ b/scripts/licenses @@ -46,8 +46,14 @@ trap 'rm -f "$list"' EXIT modules darwin amd64 1 ./cmd/agent-vm modules windows amd64 0 ./cmd/agent-vm modules windows arm64 0 ./cmd/agent-vm - modules linux arm64 0 github.com/lima-vm/lima/v2/cmd/lima-guestagent - modules linux amd64 0 github.com/lima-vm/lima/v2/cmd/lima-guestagent + modules linux arm64 0 ./third_party/lima/cmd/lima-guestagent + modules linux amd64 0 ./third_party/lima/cmd/lima-guestagent + # Packages of the main module, with licenses of their own: listed under + # their upstream names. + echo "github.com/lima-vm/lima/v2 ./third_party/lima $ROOT/third_party/lima" + echo "github.com/lima-vm/sshocker ./third_party/sshocker $ROOT/third_party/sshocker" + echo "github.com/pkg/sftp ./third_party/sftp $ROOT/third_party/sftp" + echo "github.com/containers/gvisor-tap-vsock ./third_party/gvisor-tap-vsock $ROOT/third_party/gvisor-tap-vsock" } | awk 'NF == 3 && !seen[$1]++' | LC_ALL=C sort > "$list" [ -s "$list" ] || { echo "licenses: go list found no module" >&2; exit 1; } diff --git a/scripts/third-party-sync b/scripts/third-party-sync index 50870234..75c8b985 100755 --- a/scripts/third-party-sync +++ b/scripts/third-party-sync @@ -1,7 +1,10 @@ #!/usr/bin/env bash # # Rebuild third_party/ from its upstream commit (third_party/SOURCES) -# and patches//*.patch, applied in name order. +# and patches//*.patch, applied in name order. The result is a package +# tree of agent-vm's module: its go.mod and go.sum are dropped and the import +# paths of the four trees rewritten to github.com/sylvinus/agent-vm/third_party/, +# so that a program importing agent-vm builds them patched. # # scripts/third-party-sync |all rewrite third_party/ # scripts/third-party-sync |all --check fail when third_party/ differs @@ -12,7 +15,11 @@ # lost here and caught by --check (contents, executable bits and symbolic # links): make them a patch instead. # -# Needs git, awk, find, diff and, to rewrite third_party/, rsync. +# Rewriting a tree also raises the requirements of agent-vm's go.mod to its +# upstream go.mod's, then runs go mod tidy. +# +# Needs git, awk, find, diff, perl and, to rewrite third_party/, rsync +# and go. set -euo pipefail @@ -45,6 +52,42 @@ fi TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT +# The import paths of the four trees, upstream's to agent-vm's. Only the +# import lines (and the module path Lima's schema generator reads comments +# of) of .go files: the same string is also in comments, URLs, and in the +# protobuf descriptors of the .pb.go files, whose bytes are length-prefixed. +# A nested module (Lima's hack/tools) keeps its go.mod and stays out of +# agent-vm's. +rewrite_imports() { + find "$1" -name '*.go' -type f -exec perl -pi -e ' + next unless m{^\s*(?:import\s+)?(?:[\w.]+\s+)?"github\.com/[^"]*"\s*$} || m{AddGoComments\("github\.com/}; + s{"github\.com/lima-vm/lima/v2(?![\w-])}{"github.com/sylvinus/agent-vm/third_party/lima}g; + s{"github\.com/lima-vm/sshocker(?![\w-])}{"github.com/sylvinus/agent-vm/third_party/sshocker}g; + s{"github\.com/pkg/sftp(?![\w-])}{"github.com/sylvinus/agent-vm/third_party/sftp}g; + s{"github\.com/containers/gvisor-tap-vsock(?![\w-])}{"github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock}g; + ' {} + +} + +# AgentVMPatches in : the number of the last patch applied, which the +# engine names: a tree without the patches does not compile against it. And +# AgentVMUpstream, the upstream version the tree is made from, for the +# programs that are not stamped with it at build time (-X). +patch_level() { + local name="$1" dir="$2" upstream="$3" pkgdir last pkg + case "$name" in + lima) pkgdir=pkg/version ;; + sshocker) pkgdir=pkg/reversesshfs ;; + sftp) pkgdir=. ;; + *) pkgdir=pkg/types ;; + esac + last="$(ls "$ROOT/patches/$name" 2>/dev/null | awk -F- '/^[0-9]+-.*\.patch$/ { n = $1 + 0 } END { if (n) print n }')" + [ -n "$last" ] || return 0 + pkg="$(awk '/^package / { print $2; exit }' "$dir/$pkgdir"/*.go 2>/dev/null | head -1)" + [ -n "$pkg" ] || die "$name: no Go package in $pkgdir for AgentVMPatches" + printf '// Code generated by scripts/third-party-sync. DO NOT EDIT.\n\npackage %s\n\n// AgentVMPatches is the last of agent-vm'"'"'s patches/%s in this tree.\nconst AgentVMPatches = "%s-%d"\n\n// AgentVMUpstream is the upstream version this tree is made from.\nconst AgentVMUpstream = "%s"\n' \ + "$pkg" "$name" "$name" "$last" "$upstream" > "$dir/$pkgdir/agentvm_patches.go" +} + # : the upstream tree with the patches applied, in $TMP/. build() { local name="$1" url commit version omit p dir="$TMP/$1" @@ -66,6 +109,10 @@ build() { | while IFS= read -r g; do rm -rf "${dir:?}/$g"; done find "$dir" -type d -empty -delete rm -rf "$dir/.git" + cp "$dir/go.mod" "$TMP/$name.go.mod" + rm -f "$dir/go.mod" "$dir/go.sum" + rewrite_imports "$dir" + patch_level "$name" "$dir" "$version" if [ "$omit" != - ]; then local o for o in ${omit//,/ }; do rm -rf "${dir:?}/$o"; done @@ -79,6 +126,25 @@ shape() { ( cd "$1" && { find . -type f -perm -u+x -exec echo "x {}" \; ; find . -type l -exec echo "l {}" \; ; } | LC_ALL=C sort ) } +# The requirements of an upstream go.mod, raised in agent-vm's go.mod where +# they are higher (the minimal version selection a replace used to get). +raise_requirements() { + local path version have + while read -r path version; do + case "$path" in + github.com/lima-vm/lima/v2|github.com/lima-vm/sshocker|github.com/pkg/sftp|github.com/containers/gvisor-tap-vsock) continue ;; + esac + have="$(awk -v p="$path" '$1 == p { print $2 }' "$ROOT/go.mod" | head -1)" + if [ -z "$have" ] || [ "$(printf '%s\n%s\n' "$have" "$version" | sort -V | head -1)" != "$version" ]; then + ( cd "$ROOT" && go mod edit -require="$path@$version" ) + fi + done < <(awk ' + /^require \(/ { inreq = 1; next } + inreq && /^\)/ { inreq = 0; next } + inreq && NF >= 2 && $1 !~ /^\/\// { print $1, $2 } + /^require [^(]/ { print $2, $3 }' "$1") +} + st=0 for name in $names; do build "$name" @@ -92,12 +158,16 @@ for name in $names; do fi continue fi - rsync -a --delete "$TMP/$name/" "$ROOT/third_party/$name/" + rsync -a --no-owner --no-group --delete "$TMP/$name/" "$ROOT/third_party/$name/" read -r commit version < "$TMP/$name.commit" awk -v n="$name" -v c="$commit" -v v="$version" ' $1 == n && !/^#/ { $0 = sprintf("%-10s %-41s %-41s %-27s %s", $1, $2, c, v, $5) } { print }' \ "$SOURCES" > "$TMP/SOURCES" && cat "$TMP/SOURCES" > "$SOURCES" + raise_requirements "$TMP/$name.go.mod" printf 'third_party/%s: rebuilt (%s)\n' "$name" "$version" done +if [ "$mode" == write ]; then + ( cd "$ROOT" && go mod tidy ) +fi exit "$st" diff --git a/tests/fuzz/go.work b/tests/fuzz/go.work deleted file mode 100644 index 61016126..00000000 --- a/tests/fuzz/go.work +++ /dev/null @@ -1,10 +0,0 @@ -// `go test -fuzz` only fuzzes the main modules: this workspace makes the -// vendored sshocker and pkg/sftp ones too (make fuzz). Not Lima: as a main -// module, its own requirements (its MCP server's) would take part in the build. -go 1.26.9 - -use ( - ../.. - ../../third_party/sftp - ../../third_party/sshocker -) diff --git a/tests/fuzz/go.work.sum b/tests/fuzz/go.work.sum deleted file mode 100644 index cd9cb36f..00000000 --- a/tests/fuzz/go.work.sum +++ /dev/null @@ -1,6 +0,0 @@ -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= diff --git a/tests/release.sh b/tests/release.sh index 3841e536..6a6dd84d 100755 --- a/tests/release.sh +++ b/tests/release.sh @@ -73,9 +73,9 @@ check "no argument prints the usage (exit 2)" "$?" "2" # section set to $V, committed and pushed. RR="$SB/repo"; RO="$SB/origin.git"; RB="$SB/bin" mkdir -p "$RR/www/public" "$RB" -cp -R "$SELF_DIR/release.sh" "$SELF_DIR/agent-vm.sh" "$SELF_DIR/agent-vm.setup.sh" "$SELF_DIR/runtime.example.sh" \ +cp -R "$SELF_DIR/release.sh" "$SELF_DIR/agent-vm.sh" "$SELF_DIR/runtime.example.sh" \ "$SELF_DIR/LICENSE" "$SELF_DIR/NOTICE" "$SELF_DIR/THIRD_PARTY_LICENSES" "$SELF_DIR/README.md" \ - "$SELF_DIR/tests" "$SELF_DIR/scripts" "$RR/" + "$SELF_DIR/tests" "$SELF_DIR/scripts" "$SELF_DIR/packaging" "$SELF_DIR/provision" "$RR/" cp "$SELF_DIR/www/public/install.sh" "$RR/www/public/" printf 'NEXT_VERSION := %s\n' "$V" > "$RR/Makefile" printf '# Changelog\n\n## %s\n\n- notes\n' "$V" > "$RR/CHANGELOG.md" @@ -95,7 +95,7 @@ case "\$1 \$2" in "release create") echo "\$*" > "$SB/gh-create.log" mkdir -p "$SB/assets" - for a in "\$@"; do case "\$a" in *.tar.gz|*SHA256SUMS|*.minisig) cp "\$a" "$SB/assets/" ;; esac; done + for a in "\$@"; do case "\$a" in *.tar.gz|*.deb|*.rpm|*SHA256SUMS|*.minisig) cp "\$a" "$SB/assets/" ;; esac; done touch "$SB/draft" ;; "release download") dir="" @@ -156,7 +156,17 @@ case "\$*" in else echo "\$os-\$arch" > "\$2/_output/bin/agent-vm.exe"; fi ;; esac STUB -chmod +x "$RB/gh" "$RB/make" "$RB/curl" +# nfpm writes what it was told to (-t), holding the variables release.sh sets +# and the files it was run next to. +cat > "$RB/nfpm" <&2; exit 1; } +[ -f agent-vm ] || { echo "nfpm stub: no agent-vm in \$(pwd)" >&2; exit 1; } +echo "\$fmt \$GOARCH \$VERSION \$QEMU_DEB \$QEMU_RPM \$SOURCE_DATE_EPOCH" > "\$out" +STUB +chmod +x "$RB/gh" "$RB/make" "$RB/curl" "$RB/nfpm" +export AGENT_VM_NFPM="$RB/nfpm" ( cd "$RR" && git init -q -b main && git add -A && git commit -qm r \ && git init -q --bare "$RO" && git remote add origin "$RO" && git push -q origin main ) >/dev/null 2>&1 @@ -188,6 +198,18 @@ out="$(relrun)"; rc=$? case "$rc:$out" in 1:*"NEXT_VERSION is '9.9.8'"*) pass "a Makefile on another version is refused" ;; *) fail "other version: $out" ;; esac ( cd "$RR" && printf 'NEXT_VERSION := %s\n' "$V" > Makefile && git commit -qam v && git push -q origin main ) >/dev/null 2>&1 +# flake.nix names the checksum of go.mod and go.sum its vendorHash is for. +mods="$(cat "$SELF_DIR/go.mod" "$SELF_DIR/go.sum" | sha256_of /dev/stdin)" +check "flake.nix: its note is the checksum release.sh computes" "$(grep -c "go.mod+go.sum: $mods\$" "$SELF_DIR/flake.nix")" "1" +( cd "$RR" && cp "$SELF_DIR/go.mod" "$SELF_DIR/go.sum" . && printf '# go.mod+go.sum: %s\n' "$mods" > flake.nix \ + && git add -A && git commit -qm n && git push -q origin main ) >/dev/null 2>&1 +out="$(relrun)"; rc=$? +case "$rc:$out" in 0:*"vendorHash is for this go.mod and go.sum"*) pass "a flake.nix for these Go dependencies passes" ;; *) fail "flake.nix: $out" ;; esac +( cd "$RR" && printf '\n// changed\n' >> go.mod && git commit -qam d && git push -q origin main ) >/dev/null 2>&1 +out="$(relrun)"; rc=$? +case "$rc:$out" in 1:*"flake.nix's vendorHash is not for this go.mod and go.sum"*) pass "a flake.nix for other Go dependencies is refused" ;; *) fail "stale flake.nix: $out" ;; esac +( cd "$RR" && cp "$SELF_DIR/go.mod" go.mod && git commit -qam r && git push -q origin main ) >/dev/null 2>&1 + # A missing formula stops the run before it tags or publishes anything. out="$(AGENT_VM_TAP="$SB/no-tap" relrun)"; rc=$? case "$rc:$out" in @@ -284,8 +306,14 @@ check "real run: the tag is on origin" "$(git -C "$RO" tag)" "v$V" check "real run: the latest release (no --latest=false)" "$(grep -c -- '--latest=false' "$SB/gh-edit.log")" "0" check "real run: published as the latest release" "$(grep -c -- '--draft=false --latest$' "$SB/gh-edit.log")" "1" # Byte order (LC_ALL=C): macOS sorts these differently in its locale. -check "real run: one tarball per platform, and their sums" "$(LC_ALL=C ls "$SB/assets" | tr '\n' ' ')" \ - "SHA256SUMS agent-vm-$V-linux-amd64.tar.gz agent-vm-$V-linux-arm64.tar.gz agent-vm-$V-windows-amd64.tar.gz agent-vm-$V-windows-arm64.tar.gz " +check "real run: one tarball per platform, a .deb and .rpm per Linux one, and their sums" "$(LC_ALL=C ls "$SB/assets" | tr '\n' ' ')" \ + "SHA256SUMS agent-vm-$V-1.aarch64.rpm agent-vm-$V-1.x86_64.rpm agent-vm-$V-linux-amd64.tar.gz agent-vm-$V-linux-arm64.tar.gz agent-vm-$V-windows-amd64.tar.gz agent-vm-$V-windows-arm64.tar.gz agent-vm_${V}_amd64.deb agent-vm_${V}_arm64.deb " +check "real run: the tarballs come first in SHA256SUMS" "$(awk '{ print $2 }' "$SB/assets/SHA256SUMS" | tr '\n' ' ' | sed 's/agent-vm-[^ ]*-[a-z]*-[a-z0-9]*\.tar\.gz/T/g')" \ + "T T T T agent-vm_${V}_arm64.deb agent-vm-$V-1.aarch64.rpm agent-vm_${V}_amd64.deb agent-vm-$V-1.x86_64.rpm " +check "real run: the arm64 .deb is made from that build" "$(cat "$SB/assets/agent-vm_${V}_arm64.deb")" \ + "deb arm64 $V qemu-system-arm qemu-system-aarch64-core $(git -C "$RR" log -1 --format=%ct)" +check "real run: the x86_64 .rpm is made from that build" "$(cat "$SB/assets/agent-vm-$V-1.x86_64.rpm")" \ + "rpm amd64 $V qemu-system-x86 qemu-system-x86-core $(git -C "$RR" log -1 --format=%ct)" check "real run: Windows's is agent-vm.exe" \ "$(tar -tzf "$SB/assets/agent-vm-$V-windows-arm64.tar.gz" | grep -c "^agent-vm-$V/agent-vm.exe$")" "1" if [ -n "$SIGN" ]; then diff --git a/tests/setup-script.sh b/tests/setup-script.sh index 719eb354..be407bde 100755 --- a/tests/setup-script.sh +++ b/tests/setup-script.sh @@ -9,7 +9,7 @@ set -uo pipefail SELF_DIR="$(CDPATH= cd -- "$(dirname "${BASH_SOURCE[0]:-$0}")/.." >/dev/null && pwd)" -SETUP_SH="${SETUP_SH:-$SELF_DIR/agent-vm.setup.sh}" +SETUP_SH="${SETUP_SH:-$SELF_DIR/provision/agent-vm.setup.sh}" FAIL=0 PASSED=0 diff --git a/tests/sftp/run b/tests/sftp/run index a57818d9..ff6262bc 100755 --- a/tests/sftp/run +++ b/tests/sftp/run @@ -6,8 +6,8 @@ # tests/sftp/run [case-substring...] the server agent-vm ships # tests/sftp/run --openssh [case-substring...] OpenSSH's sftp-server, the control # -# Needs Go, sshfs, fusermount (user_allow_other in /etc/fuse.conf), python3, -# git, tar and patch. WORK (default /tmp/sftp-work) must be on this machine's +# Needs Go, sshfs, fusermount (user_allow_other in /etc/fuse.conf) and +# python3. WORK (default /tmp/sftp-work) must be on this machine's # own disk, never on a share served by the code under test. set -euo pipefail @@ -21,18 +21,16 @@ if [ "${1:-}" = --openssh ]; then harness="$HERE/openssh_harness.sh" run="$WORK/run-openssh" else - # A copy of third_party/sshocker with the harness added, built against - # third_party/sftp as agent-vm's module builds it. - src="$WORK/src" - rm -rf "$src" + # The harness is added to agent-vm's module by an overlay, never written + # into third_party/. mkdir -p "$WORK" - cp -R "$ROOT/third_party/sshocker" "$src" - cp "$HERE/testdata/harness/harness_export.go" "$src/pkg/reversesshfs/" - mkdir -p "$src/cmd/harness" - cp "$HERE/testdata/harness/main.go" "$src/cmd/harness/" - ( cd "$src" \ - && go mod edit -replace "github.com/pkg/sftp=$ROOT/third_party/sftp" \ - && GOFLAGS=-mod=mod go build -o "$WORK/harness" ./cmd/harness ) + cat > "$WORK/overlay.json" </*.patch, and its `git describe` (the Lima version the build # reports). scripts/third-party-sync reads and updates this file. # +# The trees are packages of agent-vm's module (their go.mod is dropped); the +# left out folders are the ones that would add requirements nothing here uses. +# # name upstream commit version left out -lima https://github.com/lima-vm/lima 928babab2139418b5733cf0ee39cd568ce8e065c v2.3.0-beta.0-98-g928babab website -sshocker https://github.com/lima-vm/sshocker bda6eca2979ae19ac0917f914093721c88813726 v0.3.11 - +lima https://github.com/lima-vm/lima 928babab2139418b5733cf0ee39cd568ce8e065c v2.3.0-beta.0-98-g928babab website,cmd/limactl-mcp,pkg/mcp +sshocker https://github.com/lima-vm/sshocker bda6eca2979ae19ac0917f914093721c88813726 v0.3.11 cmd sftp https://github.com/pkg/sftp fc82c354c0d87349411e30a08bef297c9f132105 v1.13.11 - -gvisor-tap-vsock https://github.com/containers/gvisor-tap-vsock 9cfc86f66679ef0feed0f20ba1df558fe2bef5c6 v0.8.9 vendor,tools +gvisor-tap-vsock https://github.com/containers/gvisor-tap-vsock 9cfc86f66679ef0feed0f20ba1df558fe2bef5c6 v0.8.9 vendor,tools,cmd,test,test-qemu,test-utils,test-vfkit,test-win-sshproxy diff --git a/third_party/gvisor-tap-vsock/cmd/gvproxy/config.go b/third_party/gvisor-tap-vsock/cmd/gvproxy/config.go deleted file mode 100644 index 710edc20..00000000 --- a/third_party/gvisor-tap-vsock/cmd/gvproxy/config.go +++ /dev/null @@ -1,446 +0,0 @@ -package main - -import ( - "errors" - "flag" - "fmt" - "net" - "net/netip" - "net/url" - "os" - "runtime" - "slices" - "strings" - - "github.com/containers/gvisor-tap-vsock/pkg/types" - log "github.com/sirupsen/logrus" - yaml "gopkg.in/yaml.v3" -) - -const ( - // gatewayIP = "192.168.127.1" - sshHostPort = "192.168.127.2:22" - hostIP = "192.168.127.254" - host = "host" - gateway = "gateway" -) - -type GvproxyArgs struct { - config string - endpoints arrayFlags - debug bool - mtu int - sshPort int - vpnkitSocket string - qemuSocket string - bessSocket string - stdioSocket string - vfkitSocket string - notificationSocket string - forwardSocket arrayFlags - forwardDest arrayFlags - forwardUser arrayFlags - forwardIdentify arrayFlags - pidFile string - pcapFile string - logFile string - servicesEndpoint string - ec2MetadataAccess bool -} - -type GvproxyConfig struct { - Listen []string `yaml:"listen,omitempty"` - LogLevel string `yaml:"log-level,omitempty"` - Stack types.Configuration `yaml:"stack,omitempty"` - Interfaces struct { - VPNKit string `yaml:"vpnkit,omitempty"` - Qemu string `yaml:"qemu,omitempty"` - Bess string `yaml:"bess,omitempty"` - Stdio string `yaml:"stdio,omitempty"` - Vfkit string `yaml:"vfkit,omitempty"` - } `yaml:"interfaces,omitempty"` - Forwards []GvproxyConfigForward `yaml:"forwards,omitempty"` - PIDFile string `yaml:"pid-file,omitempty"` - LogFile string `yaml:"log-file,omitempty"` - Services string `yaml:"services,omitempty"` - Ec2MetadataAccess bool `yaml:"ec2-metadata-access,omitempty"` - NotificationSocket string `yaml:"notification,omitempty"` -} - -type GvproxyConfigForward struct { - Socket string `yaml:"socket,omitempty"` - Dest string `yaml:"dest,omitempty"` - User string `yaml:"user,omitempty"` - Identity string `yaml:"identity,omitempty"` -} - -func GvproxyVersion() string { - return types.NewVersion("gvproxy").String() -} - -func GvproxyInit() (*GvproxyConfig, error) { - var args GvproxyArgs - var config GvproxyConfig - - version := types.NewVersion("gvproxy") - version.AddFlag() - - // Pass it to the testable function - _, err := GvproxyArgParse(flag.CommandLine, &args, os.Args[1:]) - if err != nil { - return nil, fmt.Errorf("failed to parse command line arguments: %w", err) - } - - if version.ShowVersion() { - fmt.Println(version.String()) - os.Exit(0) - } - - // Init config if provided - if args.config != "" { - content, err := os.ReadFile(args.config) - if err != nil { - return nil, fmt.Errorf("failed to read config file: %w", err) - } - - if err := yaml.Unmarshal(content, &config); err != nil { - return nil, fmt.Errorf("failed to parse configuration: %w", err) - } - } - - // Pass it to the testable function - return GvproxyConfigure(&config, &args, version.String()) -} - -func GvproxyArgParse(flagSet *flag.FlagSet, args *GvproxyArgs, argv []string) (*GvproxyArgs, error) { - flagSet.StringVar(&args.config, "config", "", "Use configuration file with command line override") - flagSet.Var(&args.endpoints, "listen", "control endpoint") - flagSet.BoolVar(&args.debug, "debug", false, "Print debug info") - flagSet.StringVar(&args.pcapFile, "pcap", "", "Capture network traffic to a pcap file") - flagSet.IntVar(&args.mtu, "mtu", 0, "Set the MTU (default: 1500)") - flagSet.IntVar(&args.sshPort, "ssh-port", 2222, "Port to access the guest virtual machine. Must be between 1024 and 65535") - flagSet.StringVar(&args.vpnkitSocket, "listen-vpnkit", "", "VPNKit socket to be used by Hyperkit") - flagSet.StringVar(&args.qemuSocket, "listen-qemu", "", "Socket to be used by Qemu") - flagSet.StringVar(&args.bessSocket, "listen-bess", "", "unixpacket socket to be used by Bess-compatible applications") - flagSet.StringVar(&args.stdioSocket, "listen-stdio", "", "accept stdio pipe") - flagSet.StringVar(&args.vfkitSocket, "listen-vfkit", "", "unixgram socket to be used by vfkit-compatible applications") - flagSet.Var(&args.forwardSocket, "forward-sock", "Forwards a unix socket to the guest virtual machine over SSH") - flagSet.Var(&args.forwardDest, "forward-dest", "Forwards a unix socket to the guest virtual machine over SSH") - flagSet.Var(&args.forwardUser, "forward-user", "SSH user to use for unix socket forward") - flagSet.Var(&args.forwardIdentify, "forward-identity", "Path to SSH identity key for forwarding") - flagSet.StringVar(&args.pidFile, "pid-file", "", "Generate a file with the PID in it") - flagSet.StringVar(&args.logFile, "log-file", "", "Output log messages (logrus) to a given file path") - flagSet.StringVar(&args.servicesEndpoint, "services", "", "Exposes the same HTTP API as the --listen flag, without the /connect endpoint") - flagSet.BoolVar(&args.ec2MetadataAccess, "ec2-metadata-access", false, "Permits access to EC2 Metadata Service and Amazon Time Sync Service") - flagSet.StringVar(&args.notificationSocket, "notification", "", "Socket to be used to send network-ready notifications") - if err := flagSet.Parse(argv); err != nil { - return nil, err - } - - return args, nil -} - -func GvproxyConfigure(config *GvproxyConfig, args *GvproxyArgs, version string) (*GvproxyConfig, error) { - if args.debug { - config.LogLevel = "debug" - } - if args.logFile != "" { - config.LogFile = args.logFile - } - - if config.LogLevel == "" { - config.LogLevel = "info" - } - - // Set log level - if logLevel, err := log.ParseLevel(strings.ToLower(config.LogLevel)); err != nil { - log.Warningf("bad log level \"%s\", falling back to \"info\"", config.LogLevel) - log.SetLevel(log.InfoLevel) - } else { - log.SetLevel(logLevel) - } - - // Set log file - if config.LogFile != "" { - lf, err := os.Create(config.LogFile) - if err != nil { - return config, fmt.Errorf("unable to open log file %s", config.LogFile) - } - log.DeferExitHandler(func() { - if err := lf.Close(); err != nil { - fmt.Printf("unable to close log-file: %q\n", err) - } - }) - log.SetOutput(lf) - - // If debug is set, lets seed the log file with some basic information - // about the environment and how it was called - log.Debugf("gvproxy version: %q", version) - log.Debugf("os: %q arch: %q", runtime.GOOS, runtime.GOARCH) - log.Debugf("command line: %q", os.Args) - } - - // Set defaults - if config.Stack.MTU == 0 { - config.Stack.MTU = 1500 - } - if config.Stack.Subnet == "" { - config.Stack.Subnet = "192.168.127.0/24" - } - - // Parse subnet address for further use - naddr, err := netip.ParsePrefix(config.Stack.Subnet) - if err != nil { - return config, fmt.Errorf("failed to parse subnet: %w", err) - } - fuaddr, err := getFirstUsableIPFromSubnet(naddr) - if err != nil { - return config, fmt.Errorf("failed to identify first usable address in subnet: %w", err) - } - luaddr, err := getLastUsableIPFromSubnet(naddr) - if err != nil { - return config, fmt.Errorf("failed to identify last usable address in subnet: %w", err) - } - - if config.Stack.GatewayIP == "" { - config.Stack.GatewayIP = fuaddr.String() - } - if config.Stack.GatewayMacAddress == "" { - config.Stack.GatewayMacAddress = "5a:94:ef:e4:0c:dd" - } - if len(config.Stack.NAT) == 0 { - config.Stack.NAT = map[string]string{ - luaddr.String(): "127.0.0.1", - } - } - if len(config.Stack.GatewayVirtualIPs) == 0 { - config.Stack.GatewayVirtualIPs = []string{ - luaddr.String(), - } - } - - // Default DNS zone enabled only for the default mode - // Default DNS search domains enabled only for the default mode - // Default forwards enabled only for the default mode - // Default static leases enabled only for the default mode - // Default vpnkit mac addresses enabled only for the default mode - - // Patch config with CLI args - if args.qemuSocket != "" { - config.Interfaces.Qemu = args.qemuSocket - } - if args.bessSocket != "" { - config.Interfaces.Bess = args.bessSocket - } - if args.vfkitSocket != "" { - config.Interfaces.Vfkit = args.vfkitSocket - } - if args.vpnkitSocket != "" { - config.Interfaces.VPNKit = args.vpnkitSocket - } - if args.stdioSocket != "" { - config.Interfaces.Stdio = args.stdioSocket - } - if args.pidFile != "" { - config.PIDFile = args.pidFile - } - if args.notificationSocket != "" { - log.Debugf("notification socket: %s", args.notificationSocket) - uri, err := url.Parse(args.notificationSocket) - if err != nil { - return config, fmt.Errorf("invalid value for notification listen address: %w", err) - } - if uri.Scheme != "unix" { - return config, errors.New("notification listen address must be unix:// address") - } - config.NotificationSocket = uri.Path - } - if len(args.endpoints) > 0 { - config.Listen = args.endpoints - } - if args.servicesEndpoint != "" { - config.Services = args.servicesEndpoint - } - if args.ec2MetadataAccess { - config.Ec2MetadataAccess = true - } - if args.mtu != 0 { - config.Stack.MTU = args.mtu - } - - // Make sure the qemu socket provided is valid syntax - if config.Interfaces.Qemu != "" { - uri, err := url.Parse(config.Interfaces.Qemu) - if err != nil || uri == nil { - return config, fmt.Errorf("invalid value for qemu listen address: %w", err) - } - if _, err := os.Stat(uri.Path); err == nil && uri.Scheme == "unix" { - return config, fmt.Errorf("%q already exists", uri.Path) - } - } - if config.Interfaces.Bess != "" { - uri, err := url.Parse(config.Interfaces.Bess) - if err != nil || uri == nil { - return config, fmt.Errorf("invalid value for bess listen address: %w", err) - } - if uri.Scheme != "unixpacket" { - return config, errors.New("bess listen address must be unixpacket:// address") - } - if _, err := os.Stat(uri.Path); err == nil { - return config, fmt.Errorf("%q already exists", uri.Path) - } - } - if config.Interfaces.Vfkit != "" { - uri, err := url.Parse(config.Interfaces.Vfkit) - if err != nil || uri == nil { - return config, fmt.Errorf("invalid value for vfkit listen address: %w", err) - } - if uri.Scheme != "unixgram" { - return config, errors.New("vfkit listen address must be unixgram:// address") - } - if _, err := os.Stat(uri.Path); err == nil { - return config, fmt.Errorf("%q already exists", uri.Path) - } - } - - if config.Interfaces.VPNKit != "" && config.Interfaces.Qemu != "" { - return config, errors.New("cannot use qemu and vpnkit protocol at the same time") - } - if config.Interfaces.VPNKit != "" && config.Interfaces.Bess != "" { - return config, errors.New("cannot use bess and vpnkit protocol at the same time") - } - if config.Interfaces.Qemu != "" && config.Interfaces.Bess != "" { - return config, errors.New("cannot use qemu and bess protocol at the same time") - } - - if args.config != "" { - if slices.Contains(os.Args, "-ssh-port") || slices.Contains(os.Args, "--ssh-port") { - log.Warningf("CLI argument \"-ssh-port\" is unavailable with config file. You need to add \"127.0.0.1:%d: 192.168.127.2:22\" entry into .stack.forwards in \"\" instead", args.sshPort) - } - } - - config.Stack.Protocol = types.HyperKitProtocol - if config.Interfaces.Qemu != "" { - config.Stack.Protocol = types.QemuProtocol - } - if config.Interfaces.Bess != "" { - config.Stack.Protocol = types.BessProtocol - } - if config.Interfaces.Vfkit != "" { - config.Stack.Protocol = types.VfkitProtocol - } - - if InDebugMode() { - config.Stack.Debug = true - } - - // Handle the default behavior without config - if args.config == "" { - if args.sshPort != -1 && args.sshPort < 1024 || args.sshPort > 65535 { - return config, errors.New("ssh-port value must be between 1024 and 65535") - } - - config.Stack.CaptureFile = args.pcapFile - - config.Stack.DNS = []types.Zone{ - { - Name: "containers.internal.", - Records: []types.Record{ - { - Name: gateway, - IP: net.ParseIP(config.Stack.GatewayIP), - }, - { - Name: host, - IP: net.ParseIP(hostIP), - }, - }, - }, - { - Name: "docker.internal.", - Records: []types.Record{ - { - Name: gateway, - IP: net.ParseIP(config.Stack.GatewayIP), - }, - { - Name: host, - IP: net.ParseIP(hostIP), - }, - }, - }, - } - config.Stack.DNSSearchDomains = searchDomains() - config.Stack.Forwards = getForwardsMap(args.sshPort, sshHostPort) - config.Stack.DHCPStaticLeases = map[string]string{ - "192.168.127.2": "5a:94:ef:e4:0c:ee", - } - config.Stack.VpnKitUUIDMacAddresses = map[string]string{ - "c3d68012-0208-11ea-9fd7-f2189899ab08": "5a:94:ef:e4:0c:ee", - } - } - - // Add SSH forwards from CLI args - if c := len(args.forwardSocket); c != len(args.forwardDest) || c != len(args.forwardUser) || c != len(args.forwardIdentify) { - return config, errors.New("--forward-sock, --forward-dest, --forward-user, and --forward-identity must all be specified together, " + - "the same number of times, or not at all") - } - - for i := range args.forwardSocket { - config.Forwards = append(config.Forwards, GvproxyConfigForward{ - Socket: args.forwardSocket[i], - Dest: args.forwardDest[i], - User: args.forwardUser[i], - Identity: args.forwardIdentify[i], - }) - } - - // Validate SSH forward rules - for _, v := range config.Forwards { - _, err := os.Stat(v.Identity) - if err != nil { - return config, fmt.Errorf("identity file %q can't be loaded: %w", v.Identity, err) - } - } - - return config, nil -} - -func getFirstUsableIPFromSubnet(network netip.Prefix) (netip.Addr, error) { - // The network must have at least 5 IP addresses: network, broadcast, gateway, guest, and preferably host - // v4/30 has only 2 devices, thus prefer at least v4/29 CIDR. This code works also for IPv6, just in case - if (network.Bits() + 3) > network.Addr().BitLen() { - return netip.Addr{}, errors.New("too small network") - } - - b := network.Masked().Addr().AsSlice() - b[len(b)-1] += 1 - - addr, ok := netip.AddrFromSlice(b) - if !ok { - return netip.Addr{}, errors.New("bad ip address") - } - - return addr, nil -} - -func getLastUsableIPFromSubnet(network netip.Prefix) (netip.Addr, error) { - // The network must have at least 5 IP addresses: network, broadcast, gateway, guest, and preferably host - // v4/30 has only 2 devices, thus prefer at least v4/29 CIDR. This code works also for IPv6, just in case - if (network.Bits() + 3) > network.Addr().BitLen() { - return netip.Addr{}, errors.New("too small network") - } - - var b = network.Masked().Addr().AsSlice() - for i, v := range net.CIDRMask(network.Bits(), network.Addr().BitLen()) { - b[i] += ^v - } - b[len(b)-1] -= 1 - - addr, ok := netip.AddrFromSlice(b) - if !ok { - return netip.Addr{}, errors.New("bad ip address") - } - - return addr, nil -} diff --git a/third_party/gvisor-tap-vsock/cmd/gvproxy/config.yaml b/third_party/gvisor-tap-vsock/cmd/gvproxy/config.yaml deleted file mode 100644 index bba3efe3..00000000 --- a/third_party/gvisor-tap-vsock/cmd/gvproxy/config.yaml +++ /dev/null @@ -1,41 +0,0 @@ -### -### This example config implements the legacy behavior, same as -### you didn't provide any -### -log-level: info -stack: - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - dns: - - name: containers.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - - name: docker.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - forwards: - 127.0.0.1:2222: 192.168.127.2:22 - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 - dhcpStaticLeases: - 192.168.127.2: 5a:94:ef:e4:0c:ee - vpnKitUUIDMacAddresses: - c3d68012-0208-11ea-9fd7-f2189899ab08: 5a:94:ef:e4:0c:ee - -### -### Using while testing -### -### go get -v . -### go test . -### go run . -config config.yaml -### diff --git a/third_party/gvisor-tap-vsock/cmd/gvproxy/config_test.go b/third_party/gvisor-tap-vsock/cmd/gvproxy/config_test.go deleted file mode 100644 index 43b54fcd..00000000 --- a/third_party/gvisor-tap-vsock/cmd/gvproxy/config_test.go +++ /dev/null @@ -1,598 +0,0 @@ -package main - -import ( - "flag" - "net/netip" - "os" - "slices" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - yaml "gopkg.in/yaml.v3" -) - -func TestIPAddressConversions(t *testing.T) { - t.Parallel() - cases := [][]string{ - {"192.168.127.1/24", "192.168.127.1", "192.168.127.254"}, - {"10.10.0.0/16", "10.10.0.1", "10.10.255.254"}, - {"172.16.16.16/12", "172.16.0.1", "172.31.255.254"}, - {"fc00::fff/64", "fc00::1", "fc00::ffff:ffff:ffff:fffe"}, - } - for _, v := range cases { - naddr, _ := netip.ParsePrefix(v[0]) - - fuaddr, err := getFirstUsableIPFromSubnet(naddr) - require.NoError(t, err, "getFirstUsableIPFromSubnet returns error for \"%s\" -> \"%s\"", v[0], fuaddr) - - luaddr, err := getLastUsableIPFromSubnet(naddr) - require.NoError(t, err, "getLastUsableIPFromSubnet returns error for \"%s\" -> \"%s\"", v[0], luaddr) - - assert.Equal(t, v[1], fuaddr.String(), "getFirstUsableIPFromSubnet returns wrong result: expects \"%s\", got \"%s\"", v[1], fuaddr) - assert.Equal(t, v[2], luaddr.String(), "getLastUsableIPFromSubnet returns wrong result: expects \"%s\", got \"%s\"", v[2], luaddr) - } -} - -func TestConfigInit(t *testing.T) { - t.Parallel() - for _, v := range getCaseDataConfig() { - var cnf GvproxyConfig - var args GvproxyArgs - - flagSet := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) - - _, errArgParse := GvproxyArgParse(flagSet, &args, v.Args) - require.NoError(t, errArgParse, "%s: failed toFparse command line arguments", v.CaseName) - - // Read config - errUnmarshal := yaml.Unmarshal([]byte(v.InputConfig), &cnf) - require.NoError(t, errUnmarshal, "%s: failed to parse config file", v.CaseName) - - _, errConfig := GvproxyConfigure(&cnf, &args, "testing") - if v.ErrorPrefix != "" { - require.ErrorContains(t, errConfig, v.ErrorPrefix, "%s: wrong config error", v.CaseName) - } else { - require.NoError(t, errConfig, "%s: unexpected config error", v.CaseName) - } - - // Ignore os-specific things while testing - if len(cnf.Listen) == 1 && slices.Contains(getCaseDataPossibleDefaultListen(), cnf.Listen[0]) { - cnf.Listen[0] = "default" - } - cnf.Stack.DNSSearchDomains = nil - - result, errMarshal := yaml.Marshal(cnf) - require.NoErrorf(t, errMarshal, "%s: unmarshallable config", v.CaseName) - - assert.YAMLEq(t, v.ResultConfig, string(result), "%s: mismatch between generated and expected config", v.CaseName) - } -} - -func getCaseDataPossibleDefaultListen() []string { - return []string{ - "unix:///var/run/gvproxy/default.sock", - "unix:\\\\\\.\\pipe\\gvproxy\\default_sock", - } -} - -// Data for test cases -type caseDataConfig struct { - CaseName string - Args []string - InputConfig string - ResultConfig string - ErrorPrefix string -} - -func getCaseDataConfig() []caseDataConfig { - return []caseDataConfig{ - { - CaseName: "Legacy with no args", - Args: []string{}, - InputConfig: "", - ResultConfig: `log-level: info -stack: - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - dns: - - name: containers.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - - name: docker.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - forwards: - 127.0.0.1:2222: 192.168.127.2:22 - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 - dhcpStaticLeases: - 192.168.127.2: 5a:94:ef:e4:0c:ee - vpnKitUUIDMacAddresses: - c3d68012-0208-11ea-9fd7-f2189899ab08: 5a:94:ef:e4:0c:ee -`, - }, - { - CaseName: "Legacy from README: qemu tcp", - Args: []string{"-listen", "unix:///tmp/network.sock", "-listen-qemu", "tcp://0.0.0.0:1234"}, - InputConfig: "", - ResultConfig: `listen: - - unix:///tmp/network.sock -log-level: info -stack: - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - dns: - - name: containers.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - - name: docker.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - forwards: - 127.0.0.1:2222: 192.168.127.2:22 - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 - dhcpStaticLeases: - 192.168.127.2: 5a:94:ef:e4:0c:ee - vpnKitUUIDMacAddresses: - c3d68012-0208-11ea-9fd7-f2189899ab08: 5a:94:ef:e4:0c:ee -interfaces: - qemu: tcp://0.0.0.0:1234 -`, - }, - { - CaseName: "Legacy from README: qemu unix", - Args: []string{"-debug", "-listen", "unix:///tmp/network.sock", "-listen-qemu", "unix:///tmp/qemu.sock"}, - InputConfig: "", - ResultConfig: `listen: - - unix:///tmp/network.sock -log-level: debug -stack: - debug: true - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - dns: - - name: containers.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - - name: docker.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - forwards: - 127.0.0.1:2222: 192.168.127.2:22 - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 - dhcpStaticLeases: - 192.168.127.2: 5a:94:ef:e4:0c:ee - vpnKitUUIDMacAddresses: - c3d68012-0208-11ea-9fd7-f2189899ab08: 5a:94:ef:e4:0c:ee -interfaces: - qemu: unix:///tmp/qemu.sock -`, - }, - { - CaseName: "Legacy from README: UML", - Args: []string{"-debug", "-listen", "unix:///tmp/network.sock", "-listen-bess", "unixpacket:///tmp/bess.sock"}, - InputConfig: "", - ResultConfig: `listen: - - unix:///tmp/network.sock -log-level: debug -stack: - debug: true - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - dns: - - name: containers.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - - name: docker.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - forwards: - 127.0.0.1:2222: 192.168.127.2:22 - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 - dhcpStaticLeases: - 192.168.127.2: 5a:94:ef:e4:0c:ee - vpnKitUUIDMacAddresses: - c3d68012-0208-11ea-9fd7-f2189899ab08: 5a:94:ef:e4:0c:ee -interfaces: - bess: unixpacket:///tmp/bess.sock -`, - }, - { - CaseName: "Legacy from README: VFKit", - Args: []string{"-debug", "-listen", "unix:///tmp/network.sock", "--listen-vfkit", "unixgram:///tmp/vfkit.sock"}, - InputConfig: "", - ResultConfig: `listen: - - unix:///tmp/network.sock -log-level: debug -stack: - debug: true - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - dns: - - name: containers.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - - name: docker.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - forwards: - 127.0.0.1:2222: 192.168.127.2:22 - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 - dhcpStaticLeases: - 192.168.127.2: 5a:94:ef:e4:0c:ee - vpnKitUUIDMacAddresses: - c3d68012-0208-11ea-9fd7-f2189899ab08: 5a:94:ef:e4:0c:ee -interfaces: - vfkit: unixgram:///tmp/vfkit.sock -`, - }, - { - CaseName: "Legacy from README: vsock", - Args: []string{"-debug", "-listen", "unix:///tmp/network.sock", "-listen-qemu", "unix:///tmp/qemu.sock"}, - InputConfig: "", - ResultConfig: `listen: - - unix:///tmp/network.sock -log-level: debug -stack: - debug: true - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - dns: - - name: containers.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - - name: docker.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - forwards: - 127.0.0.1:2222: 192.168.127.2:22 - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 - dhcpStaticLeases: - 192.168.127.2: 5a:94:ef:e4:0c:ee - vpnKitUUIDMacAddresses: - c3d68012-0208-11ea-9fd7-f2189899ab08: 5a:94:ef:e4:0c:ee -interfaces: - qemu: unix:///tmp/qemu.sock -`, - }, - { - CaseName: "config: empty config", - Args: []string{"-config", "config.yaml"}, - InputConfig: ``, - ResultConfig: `log-level: info -stack: - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 -`, - }, - { - CaseName: "config: listen, loglevel, qemu", - Args: []string{"-config", "config.yaml"}, - InputConfig: `listen: - - unix:///var/run/gvproxy/domain-2.sock -log-level: warning -interfaces: - qemu: unix:///tmp/qemu.sock`, - ResultConfig: `listen: - - unix:///var/run/gvproxy/domain-2.sock -log-level: warning -stack: - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 -interfaces: - qemu: unix:///tmp/qemu.sock -`, - }, - { - CaseName: "config: stack changes: auto addresses from subnet", - Args: []string{"-config", "config.yaml"}, - InputConfig: `listen: - - unix:///var/run/gvproxy/domain-2.sock -log-level: warning -stack: - mtu: 1480 - subnet: 10.0.0.0/16 - gatewayMacAddress: 10:11:11:11:11:00 -`, - ResultConfig: `listen: - - unix:///var/run/gvproxy/domain-2.sock -log-level: warning -stack: - mtu: 1480 - subnet: 10.0.0.0/16 - gatewayIP: 10.0.0.1 - gatewayMacAddress: "10:11:11:11:11:00" - nat: - 10.0.255.254: 127.0.0.1 - gatewayVirtualIPs: - - 10.0.255.254 -`, - }, - { - CaseName: "config: stack changes: dhcpStaticLeases", - Args: []string{"-config", "config.yaml"}, - InputConfig: `stack: - subnet: 10.0.0.0/16 - dhcpStaticLeases: - 10.0.0.2: "10:11:11:11:11:02" - 10.0.0.3: "10:11:11:11:11:03" - 10.0.0.100: "10:11:11:11:11:dd" -`, - ResultConfig: `log-level: info -stack: - mtu: 1500 - subnet: 10.0.0.0/16 - gatewayIP: 10.0.0.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - nat: - 10.0.255.254: 127.0.0.1 - gatewayVirtualIPs: - - 10.0.255.254 - dhcpStaticLeases: - 10.0.0.2: "10:11:11:11:11:02" - 10.0.0.3: "10:11:11:11:11:03" - 10.0.0.100: 10:11:11:11:11:dd -`, - }, - { - CaseName: "config: stack changes: tcp forwards", - Args: []string{"-config", "config.yaml"}, - InputConfig: `stack: - subnet: 10.0.0.0/16 - forwards: - 127.0.0.1:59022: 192.168.127.2:22 - 127.0.0.1:59080: 192.168.127.2:80 - 127.0.0.1:59443: 192.168.127.2:443 -`, - ResultConfig: `log-level: info -stack: - mtu: 1500 - subnet: 10.0.0.0/16 - gatewayIP: 10.0.0.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - forwards: - 127.0.0.1:59022: 192.168.127.2:22 - 127.0.0.1:59080: 192.168.127.2:80 - 127.0.0.1:59443: 192.168.127.2:443 - nat: - 10.0.255.254: 127.0.0.1 - gatewayVirtualIPs: - - 10.0.255.254 -`, - }, - { - CaseName: "config: ssh forwards fails on identity file missing", - Args: []string{"-config", "config.yaml"}, - InputConfig: `stack: - subnet: 10.0.0.0/16 -forwards: - - socket: ??? - dest: ??? - user: ??? - identity: ??? -`, - ResultConfig: `log-level: info -stack: - mtu: 1500 - subnet: 10.0.0.0/16 - gatewayIP: 10.0.0.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - nat: - 10.0.255.254: 127.0.0.1 - gatewayVirtualIPs: - - 10.0.255.254 -forwards: - - socket: ??? - dest: ??? - user: ??? - identity: ??? -`, - ErrorPrefix: "identity file", - }, - { - CaseName: "debug check #1", - Args: []string{"-config", "config.yaml"}, - InputConfig: `log-level: debug`, - ResultConfig: `log-level: debug -stack: - debug: true - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 -`, - }, - { - CaseName: "debug check #2", - Args: []string{"-debug", "-config", "config.yaml"}, - InputConfig: `log-level: error`, - ResultConfig: `log-level: debug -stack: - debug: true - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 -`, - }, - { - CaseName: "debug check #3", - Args: []string{"-debug", "-pcap", "capture.pcap"}, - InputConfig: "", - ResultConfig: `log-level: debug -stack: - debug: true - capture-file: capture.pcap - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - dns: - - name: containers.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - - name: docker.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - forwards: - 127.0.0.1:2222: 192.168.127.2:22 - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 - dhcpStaticLeases: - 192.168.127.2: 5a:94:ef:e4:0c:ee - vpnKitUUIDMacAddresses: - c3d68012-0208-11ea-9fd7-f2189899ab08: 5a:94:ef:e4:0c:ee -`, - }, - { - CaseName: "Legacy: stdio", - Args: []string{"-listen-stdio", "accept"}, - InputConfig: "", - ResultConfig: `log-level: info -stack: - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - dns: - - name: containers.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - - name: docker.internal. - records: - - name: gateway - ip: 192.168.127.1 - - name: host - ip: 192.168.127.254 - forwards: - 127.0.0.1:2222: 192.168.127.2:22 - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 - dhcpStaticLeases: - 192.168.127.2: 5a:94:ef:e4:0c:ee - vpnKitUUIDMacAddresses: - c3d68012-0208-11ea-9fd7-f2189899ab08: 5a:94:ef:e4:0c:ee -interfaces: - stdio: accept -`, - }, - { - CaseName: "config: stdio", - Args: []string{"-config", "config.yaml"}, - InputConfig: `interfaces: - stdio: accept`, - ResultConfig: `log-level: info -stack: - mtu: 1500 - subnet: 192.168.127.0/24 - gatewayIP: 192.168.127.1 - gatewayMacAddress: 5a:94:ef:e4:0c:dd - nat: - 192.168.127.254: 127.0.0.1 - gatewayVirtualIPs: - - 192.168.127.254 -interfaces: - stdio: accept -`, - }, - } -} diff --git a/third_party/gvisor-tap-vsock/cmd/gvproxy/main.go b/third_party/gvisor-tap-vsock/cmd/gvproxy/main.go deleted file mode 100644 index bfe41f4f..00000000 --- a/third_party/gvisor-tap-vsock/cmd/gvproxy/main.go +++ /dev/null @@ -1,437 +0,0 @@ -package main - -import ( - "bufio" - "context" - "errors" - "fmt" - "net" - "net/http" - "net/http/pprof" - "net/url" - "os" - "os/signal" - "runtime" - "strconv" - "strings" - "syscall" - "time" - - "github.com/containers/gvisor-tap-vsock/pkg/net/stdio" - "github.com/containers/gvisor-tap-vsock/pkg/notification" - "github.com/containers/gvisor-tap-vsock/pkg/sshclient" - "github.com/containers/gvisor-tap-vsock/pkg/transport" - "github.com/containers/gvisor-tap-vsock/pkg/types" - "github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork" - "github.com/containers/winquit/pkg/winquit" - humanize "github.com/dustin/go-humanize" - log "github.com/sirupsen/logrus" - "golang.org/x/sync/errgroup" -) - -var ( - exitCode int -) - -func main() { - // Use config or fallback to original behavior - config, err := GvproxyInit() - if err != nil { - log.Fatal(err.Error()) - } - - // Report version - log.Info(GvproxyVersion()) - - ctx, cancel := context.WithCancel(context.Background()) - // Make this the last defer statement in the stack - defer log.Exit(exitCode) - - // Create a PID file if requested - if config.PIDFile != "" { - f, err := os.Create(config.PIDFile) - if err != nil { - log.Errorf("failed to create pidfile: %s", err.Error()) - return - } - // Remove the pid-file when exiting - defer func() { - if err := os.Remove(config.PIDFile); err != nil { - log.Errorf("failed to remove pidfile: %s", err.Error()) - } - }() - pid := os.Getpid() - if _, err := f.WriteString(strconv.Itoa(pid)); err != nil { - log.Errorf("failed to write pidfile: %s", err.Error()) - return - } - } - - groupErrs, ctx := errgroup.WithContext(ctx) - // Setup signal channel for catching user signals - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) - - // Intercept WM_QUIT/WM_CLOSE events if on Windows as SIGTERM (noop on other OSs) - winquit.SimulateSigTermOnQuit(sigChan) - - groupErrs.Go(func() error { - return run(ctx, groupErrs, config) - }) - - // Wait for something to happen - groupErrs.Go(func() error { - select { - // Catch signals so exits are graceful and defers can run - case <-sigChan: - cancel() - return errors.New("signal caught") - case <-ctx.Done(): - return nil - } - }) - // Wait for all of the go funcs to finish up - if err := groupErrs.Wait(); err != nil { - log.Errorf("gvproxy exiting: %v", err) - exitCode = 1 - } -} - -func getForwardsMap(sshPort int, sshHostPort string) map[string]string { - if sshPort == -1 { - return map[string]string{} - } - return map[string]string{ - fmt.Sprintf("127.0.0.1:%d", sshPort): sshHostPort, - } -} - -type arrayFlags []string - -func (i *arrayFlags) String() string { - return "my string representation" -} - -func (i *arrayFlags) Set(value string) error { - *i = append(*i, value) - return nil -} - -func InDebugMode() bool { - return log.GetLevel().String() == "debug" -} - -func run(ctx context.Context, g *errgroup.Group, config *GvproxyConfig) error { - vn, err := virtualnetwork.New(&config.Stack) - if err != nil { - return err - } - log.Info("waiting for clients...") - - // Initializing notificationSender here because NewNotificationSender always returns a valid object (a no-op sender when socket is empty), - // removing the need for nil checks at every Send() call. - notificationSender := notification.NewNotificationSender(config.NotificationSocket) - if config.NotificationSocket != "" { - g.Go(func() error { - notificationSender.Start(ctx) - return nil - }) - } - vn.SetNotificationSender(notificationSender) - for _, endpoint := range config.Listen { - log.Infof("listening %s", endpoint) - ln, err := transport.Listen(endpoint) - if err != nil { - return fmt.Errorf("cannot listen: %w", err) - } - httpServe(ctx, g, ln, withProfiler(vn)) - } - notificationSender.Send(types.NotificationMessage{NotificationType: types.Ready}) - - if config.Services != "" { - log.Infof("enabling services API. Listening %s", config.Services) - ln, err := transport.Listen(config.Services) - if err != nil { - return fmt.Errorf("cannot listen: %w", err) - } - httpServe(ctx, g, ln, vn.ServicesMux()) - } - - ln, err := vn.Listen("tcp", fmt.Sprintf("%s:80", config.Stack.GatewayIP)) - if err != nil { - return err - } - mux := http.NewServeMux() - mux.Handle("/services/forwarder/all", vn.Mux()) - mux.Handle("/services/forwarder/expose", vn.Mux()) - mux.Handle("/services/forwarder/unexpose", vn.Mux()) - httpServe(ctx, g, ln, mux) - - if InDebugMode() { - g.Go(func() error { - debugLog: - for { - select { - case <-time.After(5 * time.Second): - log.Debugf("%s sent to the VM, %s received from the VM\n", humanize.Bytes(vn.BytesSent()), humanize.Bytes(vn.BytesReceived())) - case <-ctx.Done(): - break debugLog - } - } - return nil - }) - } - - if config.Interfaces.VPNKit != "" { - vpnkitListener, err := transport.Listen(config.Interfaces.VPNKit) - if err != nil { - notificationSender.Send(types.NotificationMessage{NotificationType: types.HypervisorError}) - return fmt.Errorf("vpnkit listen error: %w", err) - } - g.Go(func() error { - vpnloop: - for { - select { - case <-ctx.Done(): - break vpnloop - default: - // pass through - } - conn, err := vpnkitListener.Accept() - if err != nil { - notificationSender.Send(types.NotificationMessage{NotificationType: types.HypervisorError}) - log.Errorf("vpnkit accept error: %s", err) - continue - } - g.Go(func() error { - return vn.AcceptVpnKit(conn) - }) - } - return nil - }) - } - - if config.Interfaces.Qemu != "" { - qemuListener, err := transport.Listen(config.Interfaces.Qemu) - if err != nil { - notificationSender.Send(types.NotificationMessage{NotificationType: types.HypervisorError}) - return fmt.Errorf("qemu listen error: %w", err) - } - - g.Go(func() error { - <-ctx.Done() - if err := qemuListener.Close(); err != nil { - log.Errorf("error closing %s: %q", config.Interfaces.Qemu, err) - } - return os.Remove(config.Interfaces.Qemu) - }) - - g.Go(func() error { - conn, err := qemuListener.Accept() - if err != nil { - notificationSender.Send(types.NotificationMessage{NotificationType: types.HypervisorError}) - return fmt.Errorf("qemu accept error: %w", err) - } - return vn.AcceptQemu(ctx, conn) - }) - } - - if config.Interfaces.Bess != "" { - bessListener, err := transport.Listen(config.Interfaces.Bess) - if err != nil { - notificationSender.Send(types.NotificationMessage{NotificationType: types.HypervisorError}) - return fmt.Errorf("bess listen error: %w", err) - } - - g.Go(func() error { - <-ctx.Done() - if err := bessListener.Close(); err != nil { - log.Errorf("error closing %s: %q", config.Interfaces.Bess, err) - } - return os.Remove(config.Interfaces.Bess) - }) - - g.Go(func() error { - conn, err := bessListener.Accept() - if err != nil { - notificationSender.Send(types.NotificationMessage{NotificationType: types.HypervisorError}) - return fmt.Errorf("bess accept error: %w", err) - } - return vn.AcceptBess(ctx, conn) - }) - } - - if config.Interfaces.Vfkit != "" { - conn, err := transport.ListenUnixgram(config.Interfaces.Vfkit) - if err != nil { - notificationSender.Send(types.NotificationMessage{NotificationType: types.HypervisorError}) - return fmt.Errorf("vfkit listen error: %w", err) - } - - g.Go(func() error { - <-ctx.Done() - if err := conn.Close(); err != nil { - log.Errorf("error closing %s: %q", config.Interfaces.Vfkit, err) - } - vfkitSocketURI, _ := url.Parse(config.Interfaces.Vfkit) - return os.Remove(vfkitSocketURI.Path) - }) - - g.Go(func() error { - vfkitConn, err := transport.AcceptVfkit(conn) - if err != nil { - notificationSender.Send(types.NotificationMessage{NotificationType: types.HypervisorError}) - return fmt.Errorf("vfkit accept error: %w", err) - } - - return vn.AcceptVfkit(ctx, vfkitConn) - }) - } - - if config.Interfaces.Stdio != "" { - g.Go(func() error { - conn := stdio.GetStdioConn() - if err := vn.AcceptStdio(ctx, conn); err != nil { - notificationSender.Send(types.NotificationMessage{NotificationType: types.HypervisorError}) - return err - } - return nil - }) - } - - for i := range config.Forwards { - var ( - src *url.URL - err error - ) - if strings.Contains(config.Forwards[i].Socket, "://") { - src, err = url.Parse(config.Forwards[i].Socket) - if err != nil { - return err - } - } else { - src = &url.URL{ - Scheme: "unix", - Path: config.Forwards[i].Socket, - } - } - - dest := &url.URL{ - Scheme: "ssh", - User: url.User(config.Forwards[i].User), - Host: sshHostPort, - Path: config.Forwards[i].Dest, - } - j := i - g.Go(func() error { - defer os.Remove(config.Forwards[j].Socket) - forward, err := sshclient.CreateSSHForward(ctx, src, dest, config.Forwards[j].Identity, vn) - if err != nil { - return err - } - go func() { - <-ctx.Done() - // Abort pending accepts - forward.Close() - }() - loop: - for { - select { - case <-ctx.Done(): - break loop - default: - // proceed - } - err := forward.AcceptAndTunnel(ctx) - if err != nil { - log.Debugf("Error occurred handling ssh forwarded connection: %q", err) - } - } - return nil - }) - } - - return nil -} - -func httpServe(ctx context.Context, g *errgroup.Group, ln net.Listener, mux http.Handler) { - g.Go(func() error { - <-ctx.Done() - return ln.Close() - }) - g.Go(func() error { - s := &http.Server{ - Handler: mux, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - } - err := s.Serve(ln) - if err != nil { - if err != http.ErrServerClosed { - return err - } - return err - } - return nil - }) -} - -func withProfiler(vn *virtualnetwork.VirtualNetwork) http.Handler { - mux := vn.Mux() - if InDebugMode() { - mux.HandleFunc("/debug/pprof/", pprof.Index) - mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - mux.HandleFunc("/debug/pprof/profile", pprof.Profile) - mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - } - return mux -} - -func searchDomains() []string { - if runtime.GOOS == "darwin" || runtime.GOOS == "linux" { - f, err := os.Open("/etc/resolv.conf") - if err != nil { - log.Errorf("open file error: %v", err) - return nil - } - defer f.Close() - sc := bufio.NewScanner(f) - searchPrefix := "search " - for sc.Scan() { - if strings.HasPrefix(sc.Text(), searchPrefix) { - return parseSearchString(sc.Text(), searchPrefix) - } - } - if err := sc.Err(); err != nil { - log.Errorf("scan file error: %v", err) - return nil - } - } - return nil -} - -// Parse and sanitize search list -// macOS has limitation on number of domains (6) and general string length (256 characters) -// since glibc 2.26 Linux has no limitation on 'search' field -func parseSearchString(text, searchPrefix string) []string { - // macOS allow only 265 characters in search list - if runtime.GOOS == "darwin" && len(text) > 256 { - log.Errorf("Search domains list is too long, it should not exceed 256 chars on macOS: %d", len(text)) - text = text[:256] - lastSpace := strings.LastIndex(text, " ") - if lastSpace != -1 { - text = text[:lastSpace] - } - } - - searchDomains := strings.Split(strings.TrimPrefix(text, searchPrefix), " ") - log.Debugf("Using search domains: %v", searchDomains) - - // macOS allow only 6 domains in search list - if runtime.GOOS == "darwin" && len(searchDomains) > 6 { - log.Errorf("Search domains list is too long, it should not exceed 6 domains on macOS: %d", len(searchDomains)) - searchDomains = searchDomains[:6] - } - - return searchDomains -} diff --git a/third_party/gvisor-tap-vsock/cmd/qemu-wrapper/README.md b/third_party/gvisor-tap-vsock/cmd/qemu-wrapper/README.md deleted file mode 100644 index a6e48dcf..00000000 --- a/third_party/gvisor-tap-vsock/cmd/qemu-wrapper/README.md +++ /dev/null @@ -1,6 +0,0 @@ -Qemu doesn't accept a unix socket as netdev, only a file descriptro. -This wrapper is filling the gap. - -``` -$ ./qemu-wrapper /tmp/qemu.sock qemu-system-x86_64 [...] -netdev socket,id=vlan,fd=3 -device virtio-net-pci,netdev=vlan -``` diff --git a/third_party/gvisor-tap-vsock/cmd/qemu-wrapper/main.go b/third_party/gvisor-tap-vsock/cmd/qemu-wrapper/main.go deleted file mode 100644 index 80aaf630..00000000 --- a/third_party/gvisor-tap-vsock/cmd/qemu-wrapper/main.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "log" - "net" - "os" - "os/exec" -) - -func main() { - conn, err := net.Dial("unix", os.Args[1]) - if err != nil { - log.Fatal(err) - } - fd, err := conn.(*net.UnixConn).File() - if err != nil { - log.Fatal(err) - } - cmd := exec.Command(os.Args[2], os.Args[3:]...) // #nosec G204 - cmd.ExtraFiles = append(cmd.ExtraFiles, fd) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - log.Fatal(err) - } -} diff --git a/third_party/gvisor-tap-vsock/cmd/ssh-over-vsock/client.go b/third_party/gvisor-tap-vsock/cmd/ssh-over-vsock/client.go deleted file mode 100644 index fc982276..00000000 --- a/third_party/gvisor-tap-vsock/cmd/ssh-over-vsock/client.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "fmt" - "net" - "os" - "time" - - "golang.org/x/crypto/ssh" -) - -type client struct { - Conn net.Conn - Config *ssh.ClientConfig -} - -func newClient(conn net.Conn, user string, key string) (*client, error) { - config, err := newConfig(user, key) - if err != nil { - return nil, fmt.Errorf("error getting config for native Go SSH: %s", err) - } - - return &client{ - Conn: conn, - Config: config, - }, nil -} - -func newConfig(user string, keyFile string) (*ssh.ClientConfig, error) { - key, err := os.ReadFile(keyFile) - if err != nil { - return nil, err - } - privateKey, err := ssh.ParsePrivateKey(key) - if err != nil { - return nil, err - } - return &ssh.ClientConfig{ - User: user, - Auth: []ssh.AuthMethod{ssh.PublicKeys(privateKey)}, - // #nosec G106 - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - Timeout: time.Minute, - }, nil -} - -func (client *client) output(command string) (string, error) { - c, chans, reqs, err := ssh.NewClientConn(client.Conn, "", client.Config) - if err != nil { - return "", err - } - conn := ssh.NewClient(c, chans, reqs) - session, err := conn.NewSession() - if err != nil { - _ = conn.Close() - return "", err - } - defer conn.Close() - defer session.Close() - - output, err := session.CombinedOutput(command) - if err != nil { - return "", err - } - return string(output), nil -} diff --git a/third_party/gvisor-tap-vsock/cmd/ssh-over-vsock/main.go b/third_party/gvisor-tap-vsock/cmd/ssh-over-vsock/main.go deleted file mode 100644 index 5129e930..00000000 --- a/third_party/gvisor-tap-vsock/cmd/ssh-over-vsock/main.go +++ /dev/null @@ -1,63 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "net" - "os" - - "github.com/containers/gvisor-tap-vsock/pkg/transport" - "github.com/containers/gvisor-tap-vsock/pkg/types" - "github.com/sirupsen/logrus" -) - -var ( - ip string - port int - user string - key string - endpoint string -) - -func main() { - version := types.NewVersion("ssh-over-vsock") - version.AddFlag() - flag.StringVar(&ip, "ip", "192.168.127.2", "ip of the host") - flag.IntVar(&port, "port", 22, "port of the host") - flag.StringVar(&user, "user", "", "ssh user") - flag.StringVar(&key, "key", "", "ssh key") - flag.StringVar(&endpoint, "url", "/tmp/network.sock", "url of the daemon") - flag.Parse() - - if version.ShowVersion() { - fmt.Println(version.String()) - os.Exit(0) - } - - if err := run(); err != nil { - logrus.Fatal(err) - } -} - -func run() error { - conn, err := net.Dial("unix", endpoint) - if err != nil { - return fmt.Errorf("cannot connect to host: %w", err) - } - defer conn.Close() - - if err := transport.Tunnel(conn, ip, port); err != nil { - return err - } - - client, err := newClient(conn, user, key) - if err != nil { - return err - } - out, err := client.output("ps") - if err != nil { - return err - } - fmt.Println(out) - return nil -} diff --git a/third_party/gvisor-tap-vsock/cmd/test-companion/main.go b/third_party/gvisor-tap-vsock/cmd/test-companion/main.go deleted file mode 100644 index f1773fcd..00000000 --- a/third_party/gvisor-tap-vsock/cmd/test-companion/main.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "net" - "net/http" - "time" - - "github.com/miekg/dns" - log "github.com/sirupsen/logrus" -) - -func main() { - go func() { - mux := dns.NewServeMux() - mux.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) { - m := new(dns.Msg) - m.SetReply(r) - m.RecursionAvailable = true - for _, q := range m.Question { - if q.Qtype == dns.TypeA { - m.Answer = append(m.Answer, &dns.A{ - Hdr: dns.RR_Header{ - Name: q.Name, - Rrtype: dns.TypeA, - Class: dns.ClassINET, - Ttl: 0, - }, - A: net.ParseIP("1.2.3.4"), - }) - } - } - if err := w.WriteMsg(m); err != nil { - log.Error(err) - } - }) - if err := dns.ListenAndServe(":53", "udp", mux); err != nil { - log.Fatal(err) - } - }() - - mux := http.NewServeMux() - mux.HandleFunc("/", func(writer http.ResponseWriter, _ *http.Request) { - _, _ = writer.Write([]byte(`Hello world!`)) - }) - - s := &http.Server{ - Addr: ":8080", - Handler: mux, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - } - log.Fatal(s.ListenAndServe()) -} diff --git a/third_party/gvisor-tap-vsock/cmd/vm/main_linux.go b/third_party/gvisor-tap-vsock/cmd/vm/main_linux.go deleted file mode 100644 index 457b982c..00000000 --- a/third_party/gvisor-tap-vsock/cmd/vm/main_linux.go +++ /dev/null @@ -1,224 +0,0 @@ -package main - -import ( - "encoding/binary" - "flag" - "fmt" - "io" - "math" - "net" - "net/http" - "os" - "os/exec" - "slices" - "strings" - "time" - - "github.com/containers/gvisor-tap-vsock/pkg/transport" - "github.com/containers/gvisor-tap-vsock/pkg/types" - "github.com/google/gopacket" - "github.com/google/gopacket/layers" - log "github.com/sirupsen/logrus" - "github.com/songgao/packets/ethernet" - "github.com/songgao/water" - "github.com/vishvananda/netlink" - "gvisor.dev/gvisor/pkg/tcpip/header" -) - -var ( - endpoint string - iface string - stopIfIfaceExist string - mac string - debug bool - mtu int - tapPreexists bool -) - -func main() { - version := types.NewVersion("gvforwarder") - version.AddFlag() - flag.StringVar(&endpoint, "url", fmt.Sprintf("vsock://2:1024%s", types.ConnectPath), "url where the tap send packets") - flag.StringVar(&iface, "iface", "tap0", "tap interface name") - flag.StringVar(&stopIfIfaceExist, "stop-if-exist", "eth0,ens3,enp0s1", "stop if one of these interfaces exists at startup") - flag.StringVar(&mac, "mac", "5a:94:ef:e4:0c:ee", "mac address") - flag.BoolVar(&debug, "debug", false, "debug") - flag.IntVar(&mtu, "mtu", 4000, "mtu") - flag.BoolVar(&tapPreexists, "preexisting", false, "use preexisting/preconfigured TAP interface") - flag.Parse() - - if version.ShowVersion() { - fmt.Println(version.String()) - os.Exit(0) - } - - expected := strings.Split(stopIfIfaceExist, ",") - links, err := netlink.LinkList() - if err != nil { - log.Fatal(err) - } - for _, link := range links { - if slices.Contains(expected, link.Attrs().Name) { - log.Infof("interface %s prevented this program to run", link.Attrs().Name) - return - } - } - for { - if err := run(); err != nil { - log.Error(err) - } - time.Sleep(time.Second) - } -} - -func run() error { - log.Infof("Dialing to %s…", endpoint) - conn, path, err := transport.Dial(endpoint) - if err != nil { - return fmt.Errorf("cannot connect to host: %w", err) - } - defer conn.Close() - - if path != "" { - log.Infof("Sending post request to %s", path) - req, err := http.NewRequest("POST", path, nil) - if err != nil { - return err - } - if err := req.Write(conn); err != nil { - return err - } - } - - log.Infof("Configuring tap device %s", iface) - tap, err := water.New(water.Config{ - DeviceType: water.TAP, - PlatformSpecificParams: water.PlatformSpecificParams{ - Name: iface, - }, - }) - if err != nil { - return fmt.Errorf("cannot create tap device: %w", err) - } - defer tap.Close() - - if !tapPreexists { - log.Infof("Enabling tap device %s", iface) - if err := linkUp(); err != nil { - return fmt.Errorf("cannot set mac address: %w", err) - } - } - - log.Infof("Starting rx/tx loops") - - errCh := make(chan error, 1) - go tx(conn, tap, errCh, mtu) - go rx(conn, tap, errCh, mtu) - if !tapPreexists { - go func() { - if err := dhcp(); err != nil { - errCh <- fmt.Errorf("dhcp error: %w", err) - } - }() - } - return <-errCh -} - -func linkUp() error { - link, err := netlink.LinkByName(iface) - if err != nil { - return err - } - if mac == "" { - return netlink.LinkSetUp(link) - } - hw, err := net.ParseMAC(mac) - if err != nil { - return err - } - if err := netlink.LinkSetHardwareAddr(link, hw); err != nil { - return err - } - return netlink.LinkSetUp(link) -} - -func dhcp() error { - if _, err := exec.LookPath("udhcpc"); err == nil { // busybox dhcp client - cmd := exec.Command("udhcpc", "-f", "-q", "-i", iface, "-v") - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - return cmd.Run() - } - cmd := exec.Command("dhclient", "-4", "-d", "-v", iface) - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stdout - return cmd.Run() -} - -func rx(conn net.Conn, tap *water.Interface, errCh chan error, mtu int) { - log.Info("waiting for packets...") - size := make([]byte, 2) - var frame ethernet.Frame - for { - frame.Resize(mtu) - n, err := tap.Read([]byte(frame)) - if err != nil { - errCh <- fmt.Errorf("cannot read packet from tap: %w", err) - return - } - frame = frame[:n] - - if debug { - packet := gopacket.NewPacket(frame, layers.LayerTypeEthernet, gopacket.Default) - log.Info(packet.String()) - } - - if n < 0 || n > math.MaxUint16 { - errCh <- fmt.Errorf("invalid frame length (%d > %d)", n, math.MaxUint16) - return - } - binary.LittleEndian.PutUint16(size, uint16(n)) - if _, err := conn.Write(append(size, frame...)); err != nil { - errCh <- fmt.Errorf("cannot write size and packet to socket: %w", err) - return - } - } -} - -func tx(conn net.Conn, tap *water.Interface, errCh chan error, mtu int) { - sizeBuf := make([]byte, 2) - buf := make([]byte, mtu+header.EthernetMinimumSize) - - for { - n, err := io.ReadFull(conn, sizeBuf) - if err != nil { - errCh <- fmt.Errorf("cannot read size from socket: %w", err) - return - } - if n != 2 { - errCh <- fmt.Errorf("unexpected size %d", n) - return - } - size := int(binary.LittleEndian.Uint16(sizeBuf[0:2])) - - n, err = io.ReadFull(conn, buf[:size]) - if err != nil { - errCh <- fmt.Errorf("cannot read payload from socket: %w", err) - return - } - if n == 0 || n != size { - errCh <- fmt.Errorf("unexpected size %d != %d", n, size) - return - } - - if debug { - packet := gopacket.NewPacket(buf[:size], layers.LayerTypeEthernet, gopacket.Default) - log.Info(packet.String()) - } - - if _, err := tap.Write(buf[:size]); err != nil { - errCh <- fmt.Errorf("cannot write packet to tap: %w", err) - return - } - } -} diff --git a/third_party/gvisor-tap-vsock/cmd/win-sshproxy/event-hook.go b/third_party/gvisor-tap-vsock/cmd/win-sshproxy/event-hook.go deleted file mode 100644 index 37beb2ff..00000000 --- a/third_party/gvisor-tap-vsock/cmd/win-sshproxy/event-hook.go +++ /dev/null @@ -1,72 +0,0 @@ -//go:build windows - -package main - -import ( - "bytes" - "fmt" - - "github.com/sirupsen/logrus" - "golang.org/x/sys/windows/svc/eventlog" -) - -// Logrus hook that delegates to windows event log -type EventLogHook struct { - events *eventlog.Log -} - -type LogFormat struct { - name string -} - -func (f *LogFormat) Format(entry *logrus.Entry) ([]byte, error) { - var b *bytes.Buffer - - if entry.Buffer != nil { - b = entry.Buffer - } else { - b = &bytes.Buffer{} - } - - fmt.Fprintf(b, "[%-5s] %s: %s", entry.Level.String(), f.name, entry.Message) - - for key, value := range entry.Data { - fmt.Fprintf(b, " {%s = %s}", key, value) - } - - b.WriteByte('\n') - return b.Bytes(), nil -} - -func NewEventHook(events *eventlog.Log, name string) *EventLogHook { - logrus.SetFormatter(&LogFormat{name}) - return &EventLogHook{events} -} - -func (hook *EventLogHook) Fire(entry *logrus.Entry) error { - line, err := entry.String() - if err != nil { - return err - } - - switch entry.Level { - case logrus.PanicLevel: - return hook.events.Error(1002, line) - case logrus.FatalLevel: - return hook.events.Error(1001, line) - case logrus.ErrorLevel: - return hook.events.Error(1000, line) - case logrus.WarnLevel: - return hook.events.Warning(1000, line) - case logrus.InfoLevel: - return hook.events.Info(1000, line) - case logrus.DebugLevel, logrus.TraceLevel: - return hook.events.Info(1001, line) - default: - return nil - } -} - -func (hook *EventLogHook) Levels() []logrus.Level { - return logrus.AllLevels -} diff --git a/third_party/gvisor-tap-vsock/cmd/win-sshproxy/main.go b/third_party/gvisor-tap-vsock/cmd/win-sshproxy/main.go deleted file mode 100644 index 9b94d5d6..00000000 --- a/third_party/gvisor-tap-vsock/cmd/win-sshproxy/main.go +++ /dev/null @@ -1,235 +0,0 @@ -//go:build windows - -package main - -import ( - "context" - "errors" - "fmt" - "net/url" - "os" - "path/filepath" - "strings" - "syscall" - "time" - "unsafe" - - "github.com/containers/gvisor-tap-vsock/pkg/sshclient" - "github.com/containers/gvisor-tap-vsock/pkg/types" - "github.com/containers/gvisor-tap-vsock/pkg/utils" - "github.com/containers/winquit/pkg/winquit" - "github.com/sirupsen/logrus" - "golang.org/x/sync/errgroup" - "golang.org/x/sys/windows/svc/eventlog" -) - -const ( - ERR_BAD_ARGS = 0x000A - WM_QUIT = 0x12 - RPC_S_SERVER_UNAVAILABLE syscall.Errno = 1722 -) - -var ( - stateDir string - debug bool -) - -func main() { - args := os.Args - if len(args) > 1 { - switch args[1] { - case "-version": - version := types.NewVersion("win-sshproxy") - fmt.Println(version.String()) - os.Exit(0) - case "-debug": - debug = true - args = args[2:] - default: - args = args[1:] - } - } - - if len(args) < 5 || (len(args)-2)%3 != 0 { - alert("Usage: " + filepath.Base(os.Args[0]) + "(-debug) [name] [statedir] ([source] [dest] [identity])... \n\nThis facilty proxies windows pipes and unix sockets over ssh using the specified identity.") - os.Exit(ERR_BAD_ARGS) - } - - // Attempt to set up logging with the event log service. - // If it fails because the Windows Event Log Service is not running, show a warning - // and continue execution. There could be a reason why user disabled the Event Log. - log, err := setupLogging(args[0]) - if err != nil { - if errors.Is(err, syscall.Errno(RPC_S_SERVER_UNAVAILABLE)) { - logrus.Warn("RPC server is unavailable, continuing without event log") - } else { - logrus.Errorf("Error setting up logging: %v", err) - os.Exit(1) - } - } - - // Defer closing the log if setup was successful - if log != nil { - defer log.Close() - } - - stateDir = args[1] - - var sources, dests, identities []string - for i := 2; i < len(args)-2; i += 3 { - sources = append(sources, args[i]) - dests = append(dests, args[i+1]) - identities = append(identities, args[i+2]) - } - - ctx, cancel := context.WithCancel(context.Background()) - group, ctx := errgroup.WithContext(ctx) - - quit := make(chan bool, 1) - // Wait for a WM_QUIT message to exit - winquit.NotifyOnQuit(quit) - go func() { - <-quit - cancel() - }() - - // Save thread for legacy callers which use it to post a quit - if _, err := saveThreadId(); err != nil { - logrus.Errorf("Error saving thread id: %v", err) - } - - logrus.Debug("Setting up proxies") - err = setupProxies(ctx, group, sources, dests, identities) - if err != nil { - logrus.Errorf("Error setting up proxies: %v", err) - return - } - - // Wait for completion (cancellation) or error - if err := group.Wait(); err != nil { - logrus.Errorf("Error occurred in execution group: %v", err) - return - } -} - -func setupLogging(name string) (*eventlog.Log, error) { - // Reuse the Built-in .NET Runtime Source so that we do not - // have to provide a messaage table and modify the system - // event configuration - log, err := eventlog.Open(".NET Runtime") - if err != nil { - return nil, err - } - - logrus.AddHook(NewEventHook(log, name)) - if debug { - logrus.SetLevel(logrus.DebugLevel) - } else { - logrus.SetLevel(logrus.InfoLevel) - } - - return log, nil -} - -func setupProxies(ctx context.Context, g *errgroup.Group, sources []string, dests []string, identities []string) error { - for i := 0; i < len(sources); i++ { - var ( - src *url.URL - dest *url.URL - err error - ) - if strings.Contains(sources[i], "://") { - src, err = url.Parse(sources[i]) - if err != nil { - return err - } - } else { - src = &url.URL{ - Scheme: "unix", - Path: sources[i], - } - } - - dest, err = url.Parse(dests[i]) - if err != nil { - return err - } - j := i - g.Go(func() error { - forward, err := sshclient.CreateSSHForward(ctx, src, dest, identities[j], nil) - if err != nil { - return err - } - go func() { - <-ctx.Done() - // Abort pending accepts - forward.Close() - }() - loop: - for { - select { - case <-ctx.Done(): - break loop - default: - // proceed - } - err := forward.AcceptAndTunnel(ctx) - if err != nil { - logrus.Debugf("Error occurred handling ssh forwarded connection: %q", err) - } - } - return nil - }) - } - - return nil -} - -func saveThreadId() (uint32, error) { - path := filepath.Join(stateDir, "win-sshproxy.tid") - file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) - if err != nil { - return 0, err - } - defer file.Close() - - tid, err := getThreadId() - if err != nil { - return 0, err - } - - fmt.Fprintf(file, "%d:%d\n", os.Getpid(), tid) - return tid, nil -} - -func getThreadId() (uint32, error) { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - getTid := func() (uint32, error) { - tid := winquit.GetCurrentMessageLoopThreadId() - if tid != 0 { - return tid, nil - } - return 0, fmt.Errorf("failed to get thread ID") - } - - return utils.Retry(ctx, getTid, "Waiting for message loop thread id") -} - -// Creates an "error" style pop-up window -func alert(caption string) int { - // Error box style - format := 0x10 - - user32 := syscall.NewLazyDLL("user32.dll") - captionPtr, _ := syscall.UTF16PtrFromString(caption) - titlePtr, _ := syscall.UTF16PtrFromString("winpath") - ret, _, _ := user32.NewProc("MessageBoxW").Call( - uintptr(0), - uintptr(unsafe.Pointer(captionPtr)), - uintptr(unsafe.Pointer(titlePtr)), - uintptr(format)) - - return int(ret) -} diff --git a/third_party/gvisor-tap-vsock/go.mod b/third_party/gvisor-tap-vsock/go.mod deleted file mode 100644 index 62d4c61f..00000000 --- a/third_party/gvisor-tap-vsock/go.mod +++ /dev/null @@ -1,52 +0,0 @@ -module github.com/containers/gvisor-tap-vsock - -go 1.25.0 - -require ( - github.com/Microsoft/go-winio v0.6.2 - github.com/apparentlymart/go-cidr v1.1.1 - github.com/containers/winquit v1.1.0 - github.com/coreos/stream-metadata-go v0.4.11 - github.com/dustin/go-humanize v1.0.1 - github.com/foxcpp/go-mockdns v1.2.0 - github.com/google/gopacket v1.1.19 - github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 - github.com/insomniacslk/dhcp v0.0.0-20240710054256-ddd8a41251c9 - github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 - github.com/mdlayher/vsock v1.2.1 - github.com/miekg/dns v1.1.72 - github.com/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.39.1 - github.com/opencontainers/go-digest v1.0.0 - github.com/sirupsen/logrus v1.9.4 - github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 - github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 - github.com/stretchr/testify v1.11.1 - github.com/vishvananda/netlink v1.3.1 - golang.org/x/crypto v0.50.0 - golang.org/x/mod v0.35.0 - golang.org/x/sync v0.20.0 - golang.org/x/sys v0.43.0 - gopkg.in/yaml.v3 v3.0.1 - gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/google/btree v1.1.2 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/mdlayher/socket v0.4.1 // indirect - github.com/nxadm/tail v1.4.8 // indirect - github.com/pierrec/lz4/v4 v4.1.14 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect - github.com/vishvananda/netns v0.0.5 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.53.0 // indirect - golang.org/x/text v0.36.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.43.0 // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect -) diff --git a/third_party/gvisor-tap-vsock/go.sum b/third_party/gvisor-tap-vsock/go.sum deleted file mode 100644 index c2fc52f8..00000000 --- a/third_party/gvisor-tap-vsock/go.sum +++ /dev/null @@ -1,223 +0,0 @@ -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/apparentlymart/go-cidr v1.1.1 h1:oEEk8CE0HP0YpHxsegk/TaOtR2FLHdWv4p3eM4ceUwg= -github.com/apparentlymart/go-cidr v1.1.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= -github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= -github.com/containers/winquit v1.1.0 h1:jArun04BNDQvt2W0Y78kh9TazN2EIEMG5Im6/JY7+pE= -github.com/containers/winquit v1.1.0/go.mod h1:PsPeZlnbkmGGIToMPHF1zhWjBUkd8aHjMOr/vFcPxw8= -github.com/coreos/stream-metadata-go v0.4.11 h1:sQQOpI+v/eTyYO76r7jJeCmqZcm1oQYKdwirFJ7f0M0= -github.com/coreos/stream-metadata-go v0.4.11/go.mod h1:dTE8UEFgyUcrbdUg7vGT3uIP7S8a1IwUlmWLKlOp8G8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= -github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= -github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 h1:jaqViOFFlZtkAwqvwZN+id37fosQqR5l3Oki9Dk4hz8= -github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048/go.mod h1:Di7LXRyUcnvAcLicFhtM9/MlZl/TNgRSDHORM2c6CMI= -github.com/insomniacslk/dhcp v0.0.0-20240710054256-ddd8a41251c9 h1:LZJWucZz7ztCqY6Jsu7N9g124iJ2kt/O62j3+UchZFg= -github.com/insomniacslk/dhcp v0.0.0-20240710054256-ddd8a41251c9/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic= -github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= -github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= -github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 h1:DZMFueDbfz6PNc1GwDRA8+6lBx1TB9UnxDQliCqR73Y= -github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2/go.mod h1:SWzULI85WerrFt3u+nIm5F9l7EvxZTKQvd0InF3nmgM= -github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= -github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= -github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= -github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= -github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= -github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= -github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= -github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= -github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc= -github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= -github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= -github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 h1:1zN6ImoqhSJhN8hGXFaJlSC8msLmIbX8bFqOfWLKw0w= -github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091/go.mod h1:N20Z5Y8oye9a7HmytmZ+tr8Q2vlP0tAHP13kTHzwvQY= -github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= -github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= -github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= -github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= -github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= -github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= -github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= -golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= -google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f h1:O2w2DymsOlM/nv2pLNWCMCYOldgBBMkD7H0/prN5W2k= -gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= diff --git a/third_party/gvisor-tap-vsock/pkg/client/client.go b/third_party/gvisor-tap-vsock/pkg/client/client.go index 07c45e35..3514764b 100644 --- a/third_party/gvisor-tap-vsock/pkg/client/client.go +++ b/third_party/gvisor-tap-vsock/pkg/client/client.go @@ -11,7 +11,7 @@ import ( "net/http" "strings" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" ) type Client struct { diff --git a/third_party/gvisor-tap-vsock/pkg/notification/sender.go b/third_party/gvisor-tap-vsock/pkg/notification/sender.go index 3b3c6400..7b253d1f 100644 --- a/third_party/gvisor-tap-vsock/pkg/notification/sender.go +++ b/third_party/gvisor-tap-vsock/pkg/notification/sender.go @@ -6,7 +6,7 @@ import ( "fmt" "net" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" log "github.com/sirupsen/logrus" ) diff --git a/third_party/gvisor-tap-vsock/pkg/notification/sender_test.go b/third_party/gvisor-tap-vsock/pkg/notification/sender_test.go index 7933ce14..4ec74fab 100644 --- a/third_party/gvisor-tap-vsock/pkg/notification/sender_test.go +++ b/third_party/gvisor-tap-vsock/pkg/notification/sender_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" "github.com/stretchr/testify/assert" ) diff --git a/third_party/gvisor-tap-vsock/pkg/services/dhcp/dhcp.go b/third_party/gvisor-tap-vsock/pkg/services/dhcp/dhcp.go index aac33029..464906ac 100644 --- a/third_party/gvisor-tap-vsock/pkg/services/dhcp/dhcp.go +++ b/third_party/gvisor-tap-vsock/pkg/services/dhcp/dhcp.go @@ -8,8 +8,8 @@ import ( "net/http" "time" - "github.com/containers/gvisor-tap-vsock/pkg/tap" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/tap" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/server4" "github.com/insomniacslk/dhcp/rfc1035label" diff --git a/third_party/gvisor-tap-vsock/pkg/services/dns/dns.go b/third_party/gvisor-tap-vsock/pkg/services/dns/dns.go index 968331a6..0d90ad86 100644 --- a/third_party/gvisor-tap-vsock/pkg/services/dns/dns.go +++ b/third_party/gvisor-tap-vsock/pkg/services/dns/dns.go @@ -9,7 +9,7 @@ import ( "strings" "sync" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" "github.com/miekg/dns" log "github.com/sirupsen/logrus" ) diff --git a/third_party/gvisor-tap-vsock/pkg/services/dns/dns_test.go b/third_party/gvisor-tap-vsock/pkg/services/dns/dns_test.go index 38461e7d..00d4ea15 100644 --- a/third_party/gvisor-tap-vsock/pkg/services/dns/dns_test.go +++ b/third_party/gvisor-tap-vsock/pkg/services/dns/dns_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" "github.com/foxcpp/go-mockdns" "github.com/miekg/dns" "github.com/onsi/ginkgo" diff --git a/third_party/gvisor-tap-vsock/pkg/services/forwarder/ports.go b/third_party/gvisor-tap-vsock/pkg/services/forwarder/ports.go index f5d9aa67..3ab53da9 100644 --- a/third_party/gvisor-tap-vsock/pkg/services/forwarder/ports.go +++ b/third_party/gvisor-tap-vsock/pkg/services/forwarder/ports.go @@ -15,8 +15,8 @@ import ( "strings" "sync" - "github.com/containers/gvisor-tap-vsock/pkg/sshclient" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/sshclient" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" "github.com/inetaf/tcpproxy" log "github.com/sirupsen/logrus" "gvisor.dev/gvisor/pkg/tcpip" diff --git a/third_party/gvisor-tap-vsock/pkg/services/forwarder/tcp.go b/third_party/gvisor-tap-vsock/pkg/services/forwarder/tcp.go index f6b78baa..5af82f83 100644 --- a/third_party/gvisor-tap-vsock/pkg/services/forwarder/tcp.go +++ b/third_party/gvisor-tap-vsock/pkg/services/forwarder/tcp.go @@ -7,7 +7,7 @@ import ( "net/netip" "sync" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" "github.com/inetaf/tcpproxy" log "github.com/sirupsen/logrus" "gvisor.dev/gvisor/pkg/tcpip" diff --git a/third_party/gvisor-tap-vsock/pkg/services/forwarder/udp.go b/third_party/gvisor-tap-vsock/pkg/services/forwarder/udp.go index 0454252c..e7308a9b 100644 --- a/third_party/gvisor-tap-vsock/pkg/services/forwarder/udp.go +++ b/third_party/gvisor-tap-vsock/pkg/services/forwarder/udp.go @@ -6,7 +6,7 @@ import ( "strconv" "sync" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" log "github.com/sirupsen/logrus" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" diff --git a/third_party/gvisor-tap-vsock/pkg/sshclient/ssh_forwarder.go b/third_party/gvisor-tap-vsock/pkg/sshclient/ssh_forwarder.go index ddf2247a..37b08ec6 100644 --- a/third_party/gvisor-tap-vsock/pkg/sshclient/ssh_forwarder.go +++ b/third_party/gvisor-tap-vsock/pkg/sshclient/ssh_forwarder.go @@ -12,8 +12,8 @@ import ( "sync" "time" - "github.com/containers/gvisor-tap-vsock/pkg/fs" - "github.com/containers/gvisor-tap-vsock/pkg/utils" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/fs" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/utils" "github.com/sirupsen/logrus" ) diff --git a/third_party/gvisor-tap-vsock/pkg/tap/switch.go b/third_party/gvisor-tap-vsock/pkg/tap/switch.go index 944b618f..37798530 100644 --- a/third_party/gvisor-tap-vsock/pkg/tap/switch.go +++ b/third_party/gvisor-tap-vsock/pkg/tap/switch.go @@ -11,8 +11,8 @@ import ( "sync/atomic" "syscall" - "github.com/containers/gvisor-tap-vsock/pkg/notification" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/notification" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" "github.com/google/gopacket" "github.com/google/gopacket/layers" log "github.com/sirupsen/logrus" diff --git a/third_party/gvisor-tap-vsock/pkg/transport/dial_linux.go b/third_party/gvisor-tap-vsock/pkg/transport/dial_linux.go index 56ebcbcd..c535e085 100644 --- a/third_party/gvisor-tap-vsock/pkg/transport/dial_linux.go +++ b/third_party/gvisor-tap-vsock/pkg/transport/dial_linux.go @@ -7,7 +7,7 @@ import ( "net/url" "strconv" - "github.com/containers/gvisor-tap-vsock/pkg/net/stdio" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/net/stdio" mdlayhervsock "github.com/mdlayher/vsock" ) diff --git a/third_party/gvisor-tap-vsock/pkg/types/agentvm_patches.go b/third_party/gvisor-tap-vsock/pkg/types/agentvm_patches.go new file mode 100644 index 00000000..2d860084 --- /dev/null +++ b/third_party/gvisor-tap-vsock/pkg/types/agentvm_patches.go @@ -0,0 +1,9 @@ +// Code generated by scripts/third-party-sync. DO NOT EDIT. + +package types + +// AgentVMPatches is the last of agent-vm's patches/gvisor-tap-vsock in this tree. +const AgentVMPatches = "gvisor-tap-vsock-2" + +// AgentVMUpstream is the upstream version this tree is made from. +const AgentVMUpstream = "v0.8.9" diff --git a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/bess.go b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/bess.go index a7977314..6798c171 100644 --- a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/bess.go +++ b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/bess.go @@ -4,7 +4,7 @@ import ( "context" "net" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" ) func (n *VirtualNetwork) AcceptBess(ctx context.Context, conn net.Conn) error { diff --git a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/mux.go b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/mux.go index 71b399d5..88bd649d 100644 --- a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/mux.go +++ b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/mux.go @@ -7,7 +7,7 @@ import ( "net/http" "strconv" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" "github.com/inetaf/tcpproxy" log "github.com/sirupsen/logrus" "gvisor.dev/gvisor/pkg/tcpip" diff --git a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/qemu.go b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/qemu.go index 20fbfede..79032f6c 100644 --- a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/qemu.go +++ b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/qemu.go @@ -4,7 +4,7 @@ import ( "context" "net" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" ) func (n *VirtualNetwork) AcceptQemu(ctx context.Context, conn net.Conn) error { diff --git a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/services.go b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/services.go index 48f9e765..bab46af4 100644 --- a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/services.go +++ b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/services.go @@ -6,11 +6,11 @@ import ( "strings" "sync" - "github.com/containers/gvisor-tap-vsock/pkg/services/dhcp" - "github.com/containers/gvisor-tap-vsock/pkg/services/dns" - "github.com/containers/gvisor-tap-vsock/pkg/services/forwarder" - "github.com/containers/gvisor-tap-vsock/pkg/tap" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/services/dhcp" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/services/dns" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/services/forwarder" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/tap" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" log "github.com/sirupsen/logrus" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" diff --git a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/stdio.go b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/stdio.go index cf193d54..95da2581 100644 --- a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/stdio.go +++ b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/stdio.go @@ -4,7 +4,7 @@ import ( "context" "net" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" ) func (n *VirtualNetwork) AcceptStdio(ctx context.Context, conn net.Conn) error { diff --git a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/vfkit.go b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/vfkit.go index 905e2003..05666e05 100644 --- a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/vfkit.go +++ b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/vfkit.go @@ -4,7 +4,7 @@ import ( "context" "net" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" ) func (n *VirtualNetwork) AcceptVfkit(ctx context.Context, conn net.Conn) error { diff --git a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/virtualnetwork.go b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/virtualnetwork.go index c7d32550..0ac96f82 100644 --- a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/virtualnetwork.go +++ b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/virtualnetwork.go @@ -8,9 +8,9 @@ import ( "net/http" "os" - "github.com/containers/gvisor-tap-vsock/pkg/notification" - "github.com/containers/gvisor-tap-vsock/pkg/tap" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/notification" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/tap" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/link/sniffer" "gvisor.dev/gvisor/pkg/tcpip/network/arp" diff --git a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/vpnkit.go b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/vpnkit.go index a37ba49a..843172e3 100644 --- a/third_party/gvisor-tap-vsock/pkg/virtualnetwork/vpnkit.go +++ b/third_party/gvisor-tap-vsock/pkg/virtualnetwork/vpnkit.go @@ -9,7 +9,7 @@ import ( "math" "net" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" log "github.com/sirupsen/logrus" "gvisor.dev/gvisor/pkg/tcpip/header" ) diff --git a/third_party/gvisor-tap-vsock/test-qemu/basic_test.go b/third_party/gvisor-tap-vsock/test-qemu/basic_test.go deleted file mode 100644 index 1fdefc5b..00000000 --- a/third_party/gvisor-tap-vsock/test-qemu/basic_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package e2eqemu - -import ( - "github.com/containers/gvisor-tap-vsock/pkg/types" - e2e "github.com/containers/gvisor-tap-vsock/test" - "github.com/onsi/ginkgo" - "github.com/onsi/gomega" -) - -var _ = ginkgo.Describe("connectivity with qemu", func() { - e2e.BasicConnectivityTests(e2e.BasicTestProps{ - SSHExec: sshExec, - }) -}) - -var _ = ginkgo.Describe("dns with qemu", func() { - e2e.BasicDNSTests(e2e.BasicTestProps{ - SSHExec: sshExec, - Sock: sock, - }) -}) - -var _ = ginkgo.Describe("dhcp with qemu", func() { - e2e.BasicDHCPTests(e2e.BasicTestProps{ - SSHExec: sshExec, - Sock: sock, - }) -}) - -var _ = ginkgo.Describe("command-line format", func() { - ginkgo.It("should convert Command to command line format", func() { - command := types.NewGvproxyCommand() - command.AddEndpoint("unix:///tmp/network.sock") - command.AddServiceEndpoint("unix:///tmp/services.sock") - command.Debug = true - command.AddQemuSocket("tcp://0.0.0.0:1234") - command.PidFile = "~/gv-pidfile.txt" - command.LogFile = "~/gv.log" - command.AddForwardUser("demouser") - - cmd := command.ToCmdline() - gomega.Expect(cmd).To(gomega.Equal([]string{ - "-listen", "unix:///tmp/network.sock", - "-services", "unix:///tmp/services.sock", - "-debug", - "-mtu", "1500", - "-ssh-port", "2222", - "-listen-qemu", "tcp://0.0.0.0:1234", - "-forward-user", "demouser", - "-pid-file", "~/gv-pidfile.txt", - "-log-file", "~/gv.log", - })) - }) -}) diff --git a/third_party/gvisor-tap-vsock/test-qemu/efi_darwin_arm64_test.go b/third_party/gvisor-tap-vsock/test-qemu/efi_darwin_arm64_test.go deleted file mode 100644 index 539acae8..00000000 --- a/third_party/gvisor-tap-vsock/test-qemu/efi_darwin_arm64_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package e2eqemu - -import ( - "fmt" - "os" - "path/filepath" -) - -func efiArgs() (string, error) { - // file may not exist, that's ok - _ = os.Remove("ovmf_vars.fd") - ovmfVars, err := os.Create("ovmf_vars.fd") - if err != nil { - return "", err - } - defer ovmfVars.Close() - if err := ovmfVars.Truncate(67108864); err != nil { - return "", err - } - - edk2Path := getEdk2CodeFd("edk2-aarch64-code.fd") - return fmt.Sprintf(`-drive file=%s,if=pflash,format=raw,readonly=on -drive file=%s,if=pflash,format=raw `, edk2Path, ovmfVars.Name()), nil -} - -/* - * When QEmu is installed in a non-default location in the system - * we can use the qemu-system-* binary path to figure the install - * location for Qemu and use it to look for edk2-code-fd - */ -func getEdk2CodeFdPathFromQemuBinaryPath() string { - return filepath.Clean(filepath.Join(filepath.Dir(qemuExecutable()), "..", "share", "qemu")) -} - -/* - * QEmu can be installed in multiple locations on MacOS, especially on - * Apple Silicon systems. A build from source will likely install it in - * /usr/local/bin, whereas Homebrew package management standard is to - * install in /opt/homebrew - */ -func getEdk2CodeFd(name string) string { - dirs := []string{ - getEdk2CodeFdPathFromQemuBinaryPath(), - "/opt/homebrew/opt/podman/libexec/share/qemu", - "/usr/local/share/qemu", - "/opt/homebrew/share/qemu", - } - for _, dir := range dirs { - fullpath := filepath.Join(dir, name) - if _, err := os.Stat(fullpath); err == nil { - return fullpath - } - } - return name -} diff --git a/third_party/gvisor-tap-vsock/test-qemu/efi_generic.go b/third_party/gvisor-tap-vsock/test-qemu/efi_generic.go deleted file mode 100644 index a5899ef5..00000000 --- a/third_party/gvisor-tap-vsock/test-qemu/efi_generic.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !(darwin && arm64) - -package e2eqemu - -func efiArgs() (string, error) { - return "", nil -} diff --git a/third_party/gvisor-tap-vsock/test-qemu/port_forwarding_test.go b/third_party/gvisor-tap-vsock/test-qemu/port_forwarding_test.go deleted file mode 100644 index d18d7cb4..00000000 --- a/third_party/gvisor-tap-vsock/test-qemu/port_forwarding_test.go +++ /dev/null @@ -1,280 +0,0 @@ -package e2eqemu - -import ( - "context" - "fmt" - "io" - "net" - "net/http" - "os" - "os/exec" - "path/filepath" - "runtime" - "time" - - gvproxyclient "github.com/containers/gvisor-tap-vsock/pkg/client" - "github.com/containers/gvisor-tap-vsock/pkg/transport" - "github.com/containers/gvisor-tap-vsock/pkg/types" - "github.com/onsi/ginkgo" - "github.com/onsi/gomega" - log "github.com/sirupsen/logrus" -) - -var _ = ginkgo.Describe("port forwarding", func() { - client := gvproxyclient.New(&http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - return net.Dial("unix", sock) - }, - }, - }, "http://base") - - ginkgo.It("should reach a http server on the host", func() { - ln, err := net.Listen("tcp", "127.0.0.1:9090") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - defer ln.Close() - - mux := http.NewServeMux() - mux.HandleFunc("/", func(writer http.ResponseWriter, _ *http.Request) { - _, _ = writer.Write([]byte("Hello from the host")) - }) - go func() { - s := &http.Server{ - Handler: mux, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - } - err := s.Serve(ln) - if err != nil { - log.Error(err) - } - }() - - out, err := sshExec("curl http://host.containers.internal:9090") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("Hello from the host")) - - out, err = sshExec("curl http://host.docker.internal:9090") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("Hello from the host")) - }) - - ginkgo.It("should reach a http server in the VM using dynamic port forwarding", func() { - _, err := net.Dial("tcp", "127.0.0.1:9090") - gomega.Expect(err).Should(gomega.HaveOccurred()) - gomega.Expect(err.Error()).To(gomega.HaveSuffix("connection refused")) - - gomega.Expect(client.Expose(&types.ExposeRequest{ - Local: "127.0.0.1:9090", - Remote: "192.168.127.2:8080", - })).Should(gomega.Succeed()) - - gomega.Eventually(func(g gomega.Gomega) { - resp, err := http.Get("http://127.0.0.1:9090") - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) - }).Should(gomega.Succeed()) - - gomega.Expect(client.Unexpose(&types.UnexposeRequest{ - Local: "127.0.0.1:9090", - })).Should(gomega.Succeed()) - - gomega.Eventually(func(g gomega.Gomega) { - _, err = net.Dial("tcp", "127.0.0.1:9090") - g.Expect(err).Should(gomega.HaveOccurred()) - g.Expect(err.Error()).To(gomega.HaveSuffix("connection refused")) - }).Should(gomega.Succeed()) - }) - - ginkgo.It("should reach a dns server in the VM using dynamic port forwarding", func() { - gomega.Expect(client.Expose(&types.ExposeRequest{ - Local: ":1053", - Remote: "192.168.127.2:53", - Protocol: "udp", - })).Should(gomega.Succeed()) - - gomega.Eventually(func(g gomega.Gomega) { - cmd := exec.Command("nslookup", "-timeout=1", "-port=1053", "foobar", "127.0.0.1") - out, err := cmd.CombinedOutput() - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(string(out)).To(gomega.ContainSubstring("Address: 1.2.3.4")) - }).Should(gomega.Succeed()) - - gomega.Expect(client.Unexpose(&types.UnexposeRequest{ - Local: ":1053", - Protocol: "udp", - })).Should(gomega.Succeed()) - }) - - ginkgo.It("should reach a http server in the VM using the tunneling of the daemon", func() { - httpClient := &http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - conn, err := net.Dial("unix", sock) - if err != nil { - return nil, err - } - return conn, transport.Tunnel(conn, "192.168.127.2", 8080) - }, - }, - } - - gomega.Eventually(func(g gomega.Gomega) { - resp, err := httpClient.Get("http://placeholder/") - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) - }).Should(gomega.Succeed()) - }) - - ginkgo.It("should reach a http server in the VM using dynamic port forwarding configured within the VM", func() { - _, err := net.Dial("tcp", "127.0.0.1:9090") - gomega.Expect(err).Should(gomega.HaveOccurred()) - gomega.Expect(err.Error()).To(gomega.HaveSuffix("connection refused")) - - _, err = sshExec(`curl http://gateway.containers.internal/services/forwarder/expose -X POST -d'{"local":":9090", "remote":":8080"}'`) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - gomega.Eventually(func(g gomega.Gomega) { - resp, err := http.Get("http://127.0.0.1:9090") - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) - }).Should(gomega.Succeed()) - - _, err = sshExec(`curl http://gateway.containers.internal/services/forwarder/unexpose -X POST -d'{"local":":9090"}'`) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - gomega.Eventually(func(g gomega.Gomega) { - _, err = net.Dial("tcp", "127.0.0.1:9090") - g.Expect(err).Should(gomega.HaveOccurred()) - g.Expect(err.Error()).To(gomega.HaveSuffix("connection refused")) - }).Should(gomega.Succeed()) - }) - - ginkgo.It("should reach rootless podman API using unix socket forwarding over ssh", func() { - httpClient := &http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - return net.Dial("unix", forwardSock) - }, - }, - } - - gomega.Eventually(func(g gomega.Gomega) { - resp, err := httpClient.Get("http://host/_ping") - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) - g.Expect(resp.ContentLength).To(gomega.Equal(int64(2))) - - reply := make([]byte, resp.ContentLength) - _, err = io.ReadAtLeast(resp.Body, reply, len(reply)) - - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(string(reply)).To(gomega.Equal("OK")) - }).Should(gomega.Succeed()) - }) - - ginkgo.It("should reach rootful podman API using unix socket forwarding over ssh", func() { - httpClient := &http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - return net.Dial("unix", forwardRootSock) - }, - }, - } - - gomega.Eventually(func(g gomega.Gomega) { - resp, err := httpClient.Get("http://host/_ping") - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) - g.Expect(resp.ContentLength).To(gomega.Equal(int64(2))) - - reply := make([]byte, resp.ContentLength) - _, err = io.ReadAtLeast(resp.Body, reply, len(reply)) - - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(string(reply)).To(gomega.Equal("OK")) - }).Should(gomega.Succeed()) - }) - - ginkgo.It("should expose and reach an http service using unix to tcp forwarding", func() { - if runtime.GOOS == "windows" { - ginkgo.Skip("AF_UNIX not supported on Windows") - } - - unix2tcpfwdsock, _ := filepath.Abs(filepath.Join(tmpDir, "podman-unix-to-unix-forwarding.sock")) - - out, err := sshExec(`curl http://gateway.containers.internal/services/forwarder/expose -X POST -d'{"protocol":"unix","local":"` + unix2tcpfwdsock + `","remote":"tcp://192.168.127.2:8080"}'`) - gomega.Expect(string(out)).Should(gomega.Equal("")) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - gomega.Eventually(func(g gomega.Gomega) { - sockfile, err := os.Stat(unix2tcpfwdsock) - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(sockfile.Mode().Type().String()).To(gomega.Equal(os.ModeSocket.String())) - }).Should(gomega.Succeed()) - - httpClient := &http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - return net.Dial("unix", unix2tcpfwdsock) - }, - }, - } - - gomega.Eventually(func(g gomega.Gomega) { - resp, err := httpClient.Get("http://placeholder/") - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) - }).Should(gomega.Succeed()) - - gomega.Eventually(func(g gomega.Gomega) { - // exposing the port again should return an error - out, _ = sshExec(`curl http://gateway.containers.internal/services/forwarder/expose -X POST -d'{"protocol":"unix","local":"` + unix2tcpfwdsock + `","remote":"tcp://192.168.127.2:8080"}'`) - g.Expect(string(out)).Should(gomega.ContainSubstring("proxy already running")) - - // unexpose the port - out, err = sshExec(`curl http://gateway.containers.internal/services/forwarder/unexpose -X POST -d'{"protocol":"unix","local":"` + unix2tcpfwdsock + `","remote":"tcp://192.168.127.2:8080"}'`) - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(string(out)).Should(gomega.Equal("")) - }).Should(gomega.Succeed()) - }) - - ginkgo.It("should expose and reach rootless podman API using unix to unix forwarding over ssh", func() { - if runtime.GOOS == "windows" { - ginkgo.Skip("AF_UNIX not supported on Windows") - } - - unix2unixfwdsock, _ := filepath.Abs(filepath.Join(tmpDir, "podman-unix-to-unix-forwarding.sock")) - - remoteuri := fmt.Sprintf(`ssh-tunnel://root@%s:%d%s?key=%s`, "192.168.127.2", 22, podmanSock, privateKeyFile) - _, err := sshExec(`curl http://192.168.127.1/services/forwarder/expose -X POST -d'{"protocol":"unix","local":"` + unix2unixfwdsock + `","remote":"` + remoteuri + `"}'`) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - gomega.Eventually(func(g gomega.Gomega) { - sockfile, err := os.Stat(unix2unixfwdsock) - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(sockfile.Mode().Type().String()).To(gomega.Equal(os.ModeSocket.String())) - }).Should(gomega.Succeed()) - - httpClient := &http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - return net.Dial("unix", unix2unixfwdsock) - }, - }, - } - - gomega.Eventually(func(g gomega.Gomega) { - resp, err := httpClient.Get("http://host/_ping") - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(resp.StatusCode).To(gomega.Equal(http.StatusOK)) - g.Expect(resp.ContentLength).To(gomega.Equal(int64(2))) - - reply := make([]byte, resp.ContentLength) - _, err = io.ReadAtLeast(resp.Body, reply, len(reply)) - - g.Expect(err).ShouldNot(gomega.HaveOccurred()) - g.Expect(string(reply)).To(gomega.Equal("OK")) - }).Should(gomega.Succeed()) - }) -}) diff --git a/third_party/gvisor-tap-vsock/test-qemu/suite_test.go b/third_party/gvisor-tap-vsock/test-qemu/suite_test.go deleted file mode 100644 index 01f852f4..00000000 --- a/third_party/gvisor-tap-vsock/test-qemu/suite_test.go +++ /dev/null @@ -1,244 +0,0 @@ -package e2eqemu - -import ( - "flag" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "testing" - "time" - - e2e_utils "github.com/containers/gvisor-tap-vsock/test-utils" - - "github.com/onsi/ginkgo" - "github.com/onsi/gomega" - log "github.com/sirupsen/logrus" -) - -func TestSuite(t *testing.T) { - gomega.RegisterFailHandler(ginkgo.Fail) - ginkgo.RunSpecs(t, "gvisor-tap-vsock suite") -} - -const ( - sock = "/tmp/gvproxy-api.sock" - qemuPort = 5555 - sshPort = 2222 - ignitionUser = "test" - qconLog = "qcon.log" - podmanSock = "/run/user/1001/podman/podman.sock" - podmanRootSock = "/run/podman/podman.sock" - - // #nosec "test" (for manual usage) - ignitionPasswordHash = "$y$j9T$TqJWt3/mKJbH0sYi6B/LD1$QjVRuUgntjTHjAdAkqhkr4F73m.Be4jBXdAaKw98sPC" -) - -var ( - tmpDir string - binDir string - host *exec.Cmd - client *exec.Cmd - privateKeyFile string - publicKeyFile string - ignFile string - forwardSock string - forwardRootSock string -) - -func init() { - flag.StringVar(&tmpDir, "tmpDir", "../tmp", "temporary working directory") - flag.StringVar(&binDir, "bin", "../bin", "directory with compiled binaries") - privateKeyFile = filepath.Join(tmpDir, "id_test_qemu") - publicKeyFile = privateKeyFile + ".pub" - ignFile = filepath.Join(tmpDir, "test.ign") - forwardSock = filepath.Join(tmpDir, "podman-remote.sock") - forwardRootSock = filepath.Join(tmpDir, "podman-root-remote.sock") - -} - -var _ = ginkgo.BeforeSuite(func() { - gomega.Expect(os.MkdirAll(filepath.Join(tmpDir, "disks"), os.ModePerm)).Should(gomega.Succeed()) - - downloader, err := e2e_utils.NewFcosDownloader(filepath.Join(tmpDir, "disks")) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - qemuImage, err := downloader.DownloadImage("qemu", "qcow2.xz") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - publicKey, err := e2e_utils.CreateSSHKeys(publicKeyFile, privateKeyFile) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - err = e2e_utils.CreateIgnition(ignFile, publicKey, ignitionUser, ignitionPasswordHash) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - -outer: - for panics := 0; ; panics++ { - _ = os.Remove(sock) - - // #nosec - host = exec.Command(filepath.Join(binDir, "gvproxy"), fmt.Sprintf("--listen=unix://%s", sock), fmt.Sprintf("--listen-qemu=tcp://127.0.0.1:%d", qemuPort), - fmt.Sprintf("--forward-sock=%s", forwardSock), fmt.Sprintf("--forward-dest=%s", podmanSock), fmt.Sprintf("--forward-user=%s", ignitionUser), - fmt.Sprintf("--forward-identity=%s", privateKeyFile), - fmt.Sprintf("--forward-sock=%s", forwardRootSock), fmt.Sprintf("--forward-dest=%s", podmanRootSock), fmt.Sprintf("--forward-user=%s", "root"), - fmt.Sprintf("--forward-identity=%s", privateKeyFile)) - - host.Stderr = os.Stderr - host.Stdout = os.Stdout - gomega.Expect(host.Start()).Should(gomega.Succeed()) - go func() { - if err := host.Wait(); err != nil { - log.Error(err) - } - }() - - for { - _, err := os.Stat(sock) - if os.IsNotExist(err) { - log.Info("waiting for socket") - time.Sleep(100 * time.Millisecond) - continue - } - break - } - - efiArgs, err := efiArgs() - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - template := `%s%s-m 2048 -nographic -serial file:%s -drive if=virtio,file=%s,snapshot=on -fw_cfg name=opt/com.coreos/config,file=%s -netdev socket,id=vlan,connect=127.0.0.1:%d -device virtio-net-pci,netdev=vlan,mac=5a:94:ef:e4:0c:ee` - - // #nosec - client = exec.Command(qemuExecutable(), strings.Split(fmt.Sprintf(template, qemuArgs(), efiArgs, qconLog, qemuImage, ignFile, qemuPort), " ")...) - client.Stderr = os.Stderr - client.Stdout = os.Stdout - gomega.Expect(client.Start()).Should(gomega.Succeed()) - go func() { - if err := client.Wait(); err != nil { - log.Error(err) - } - }() - - for { - _, err := sshExec("whoami") - if err == nil { - break outer - } - - // Check for panic - didPanic, err := panicCheck(qconLog) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - if didPanic { - gomega.Expect(panics).ToNot(gomega.BeNumerically(">", 15), "No more than 15 panics allowed") - log.Info("Detected Kernel panic, retrying...") - _ = client.Process.Kill() - _ = host.Process.Kill() - _ = os.Remove(qconLog) - continue outer - } - - log.Infof("waiting for client to connect: %v", err) - time.Sleep(time.Second) - } - } - - err = scp(filepath.Join(binDir, "test-companion"), "/tmp/test-companion") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - // start an embedded DNS and http server in the VM. Wait a bit for the server to start. - cmd := sshCommand("sudo /tmp/test-companion") - gomega.Expect(cmd.Start()).ShouldNot(gomega.HaveOccurred()) - time.Sleep(5 * time.Second) -}) - -func qemuExecutable() string { - qemuBinaries := []string{"qemu-kvm", fmt.Sprintf("qemu-system-%s", e2e_utils.CoreosArch())} - for _, binary := range qemuBinaries { - path, err := exec.LookPath(binary) - if err == nil && path != "" { - return path - } - } - - return "" -} - -func qemuArgs() string { - accel := "kvm" - if runtime.GOOS == "darwin" { - accel = "hvf" - } - machine := "q35" - switch runtime.GOARCH { - case "amd64": - machine = "q35" - case "arm64": - machine = "virt" - default: - panic(fmt.Sprintf("unsupported arch: %s", runtime.GOARCH)) - } - return fmt.Sprintf("-machine %s,accel=%s:tcg -smp 4 -cpu host ", machine, accel) -} - -func scp(src, dst string) error { - sshCmd := exec.Command("scp", - "-o", "UserKnownHostsFile=/dev/null", - "-o", "StrictHostKeyChecking=no", - "-o", "IdentitiesOnly=yes", - "-i", privateKeyFile, - "-P", strconv.Itoa(sshPort), - src, - fmt.Sprintf("%s@127.0.0.1:%s", ignitionUser, dst)) // #nosec G204 - sshCmd.Stderr = os.Stderr - sshCmd.Stdout = os.Stdout - return sshCmd.Run() -} - -func sshExec(cmd ...string) ([]byte, error) { - return sshCommand(cmd...).Output() -} - -func sshCommand(cmd ...string) *exec.Cmd { - sshCmd := exec.Command("ssh", - "-o", "UserKnownHostsFile=/dev/null", - "-o", "StrictHostKeyChecking=no", - "-o", "IdentitiesOnly=yes", - "-i", privateKeyFile, - "-p", strconv.Itoa(sshPort), - fmt.Sprintf("%s@127.0.0.1", ignitionUser), "--", strings.Join(cmd, " ")) // #nosec G204 - return sshCmd -} - -func panicCheck(con string) (bool, error) { - file, err := os.Open(con) - if err != nil { - return false, err - } - - _, _ = file.Seek(-500, io.SeekEnd) - // Ignore seek errors (not enough content yet) - - contents := make([]byte, 500) - _, err = io.ReadAtLeast(file, contents, len(contents)) - if err != nil && err != io.ErrUnexpectedEOF { - return false, err - } - - return strings.Contains(string(contents), "end Kernel panic"), nil -} - -var _ = ginkgo.AfterSuite(func() { - if host != nil { - if err := host.Process.Kill(); err != nil { - log.Error(err) - } - } - if client != nil { - if err := client.Process.Kill(); err != nil { - log.Error(err) - } - } -}) diff --git a/third_party/gvisor-tap-vsock/test-utils/fcos.go b/third_party/gvisor-tap-vsock/test-utils/fcos.go deleted file mode 100644 index c8bbbd6c..00000000 --- a/third_party/gvisor-tap-vsock/test-utils/fcos.go +++ /dev/null @@ -1,160 +0,0 @@ -package e2eutils - -import ( - "os" - "path/filepath" - "strings" - - "encoding/json" - "fmt" - "io" - "net/http" - "runtime" - - "github.com/opencontainers/go-digest" - - "github.com/coreos/stream-metadata-go/fedoracoreos" - "github.com/coreos/stream-metadata-go/stream" - "github.com/sirupsen/logrus" -) - -type FcosDownload struct { - DataDir string -} - -type fcosDownloadInfo struct { - Location string - Sha256Sum string -} - -type ArtifactFormat struct { - Artifact string - Format string -} - -func NewFcosDownloader(dataDir string) (*FcosDownload, error) { - return &FcosDownload{ - DataDir: dataDir, - }, nil -} - -func imageName(info *fcosDownloadInfo) string { - urlSplit := strings.Split(info.Location, "/") - return urlSplit[len(urlSplit)-1] -} - -func (downloader *FcosDownload) DownloadImage(artifactType string, formatType string) (string, error) { - info, err := getFCOSDownload(artifactType, formatType) - if err != nil { - return "", err - } - - compressedImage := filepath.Join(downloader.DataDir, imageName(info)) - - // check if the latest image is already present - ok, err := downloader.updateAvailable(info, compressedImage) - if err != nil { - return "", err - } - if !ok { - if err := DownloadVMImage(info.Location, compressedImage); err != nil { - return "", err - } - } - - uncompressedImage := "" - if uncompressedImage, err = Decompress(compressedImage); err != nil { - return "", err - } - return uncompressedImage, nil -} - -func (downloader *FcosDownload) updateAvailable(info *fcosDownloadInfo, compressedImage string) (bool, error) { - // check the sha of the local image if it exists - // get the sha of the remote image - // == dont bother to pull - if _, err := os.Stat(compressedImage); os.IsNotExist(err) { - return false, nil - } - fd, err := os.Open(compressedImage) - if err != nil { - return false, err - } - defer fd.Close() - sum, err := digest.SHA256.FromReader(fd) - if err != nil { - return false, err - } - if sum.Encoded() == info.Sha256Sum { - return true, nil - } - return false, nil -} - -// as of 2024-05-28, these are the 4 architectures available in -// curl https://builds.coreos.fedoraproject.org/streams/next.json -func CoreosArch() string { - switch runtime.GOARCH { - case "amd64": - return "x86_64" - case "arm64": - return "aarch64" - case "ppc64le": - return "ppc64le" - case "s390x": - return "s390x" - } - panic(fmt.Sprintf("unknown arch: %s", runtime.GOOS)) -} - -// This should get Exported and stay put as it will apply to all fcos downloads -// getFCOS parses fedoraCoreOS's stream and returns the image download URL and the release version -func getFCOSDownload(artifactType string, formatType string) (*fcosDownloadInfo, error) { - streamurl := fedoracoreos.GetStreamURL(fedoracoreos.StreamNext) - resp, err := http.Get(streamurl.String()) - if err != nil { - return nil, err - } - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - defer func() { - if err := resp.Body.Close(); err != nil { - logrus.Error(err) - } - }() - - var fcosstable stream.Stream - if err := json.Unmarshal(body, &fcosstable); err != nil { - return nil, err - } - arch, ok := fcosstable.Architectures[CoreosArch()] - if !ok { - return nil, fmt.Errorf("unable to pull VM image: no targetArch in stream") - } - artifacts := arch.Artifacts - if artifacts == nil { - return nil, fmt.Errorf("unable to pull VM image: no artifact in stream") - } - artifact, ok := artifacts[artifactType] - if !ok { - return nil, fmt.Errorf("unable to pull VM image: no qemu artifact in stream") - } - formats := artifact.Formats - if formats == nil { - return nil, fmt.Errorf("unable to pull VM image: no formats in stream") - } - format, ok := formats[formatType] - if !ok { - return nil, fmt.Errorf("unable to pull VM image: no qcow2.xz format in stream") - } - disk := format.Disk - if disk == nil { - return nil, fmt.Errorf("unable to pull VM image: no disk in stream") - } - return &fcosDownloadInfo{ - Location: disk.Location, - Sha256Sum: disk.Sha256, - }, nil -} diff --git a/third_party/gvisor-tap-vsock/test-utils/ignition.go b/third_party/gvisor-tap-vsock/test-utils/ignition.go deleted file mode 100644 index 4c7aae05..00000000 --- a/third_party/gvisor-tap-vsock/test-utils/ignition.go +++ /dev/null @@ -1,162 +0,0 @@ -package e2eutils - -import ( - "encoding/json" - "net/url" - "os" -) - -var ( - mode = 0644 - dirMode = 0744 - root = "root" - test = "test" - yes = true - no = false -) - -func CreateIgnition(ignitionFile string, publicKey string, user string, password string) error { - - linger := `[Unit] -Description=Activate podman socket -Wants=podman.socket -[Service] -ExecStart=/usr/bin/sleep infinity -` - - systemd := Systemd{ - Units: []Unit{ - { - Name: "systemd-resolved.service", - Enabled: &no, - Mask: &yes, - }, - { - Name: "podman.socket", - Enabled: &yes, - }, - }, - } - - passwd := Passwd{ - Users: []PasswdUser{ - { - Name: user, - PasswordHash: &password, - SSHAuthorizedKeys: []SSHAuthorizedKey{ - SSHAuthorizedKey(publicKey), - }, - Groups: []Group{ - "wheel", - "sudo", - }, - }, - { - Name: "root", - PasswordHash: &password, - SSHAuthorizedKeys: []SSHAuthorizedKey{ - SSHAuthorizedKey(publicKey), - }, - }, - }, - } - - storage := Storage{ - // Replaces resolv.conf with an empty file that will be overwritten by NetworkManager - Files: []File{ - { - Node: Node{ - Group: NodeGroup{Name: &root}, - Path: "/etc/resolv.conf", - User: NodeUser{Name: &root}, - Overwrite: &yes, - }, - FileEmbedded1: FileEmbedded1{ - Contents: Resource{ - Source: encodeData(""), - }, - Mode: &mode, - }, - }, - { - Node: Node{ - Group: NodeGroup{Name: &test}, - Path: "/home/" + test + "/.config/systemd/user/linger-podman.service", - User: NodeUser{Name: &test}, - Overwrite: &yes, - }, - FileEmbedded1: FileEmbedded1{ - Contents: Resource{ - Source: encodeData(linger), - }, - Mode: &mode, - }, - }, - { - Node: Node{ - Group: NodeGroup{Name: &test}, - Path: "/var/lib/systemd/linger/" + test, - User: NodeUser{Name: &test}, - Overwrite: &yes, - }, - FileEmbedded1: FileEmbedded1{ - Contents: Resource{ - Source: encodeData(""), - }, - Mode: &mode, - }, - }, - }, - Directories: []Directory{ - dir("/home/" + test + "/.config"), - dir("/home/" + test + "/.config/containers"), - dir("/home/" + test + "/.config/systemd"), - dir("/home/" + test + "/.config/systemd/user"), - dir("/home/" + test + "/.config/systemd/user/default.target.wants"), - }, - Links: []Link{ - { - Node: Node{ - Group: NodeGroup{Name: &test}, - Path: "/home/" + test + "/.config/systemd/user/default.target.wants/linger-podman.service", - User: NodeUser{Name: &test}, - }, - LinkEmbedded1: LinkEmbedded1{ - Hard: &no, - Target: "/home/" + test + "/.config/systemd/user/linger-podman.service", - }, - }, - }, - } - - config := Config{ - Ignition: Ignition{Version: "3.2.0"}, - Systemd: systemd, - Passwd: passwd, - Storage: storage, - } - - contents, err := json.Marshal(config) - if err != nil { - return err - } - - // #nosec - return os.WriteFile(ignitionFile, contents, 0644) -} - -func dir(path string) Directory { - return Directory{ - Node: Node{ - Group: NodeGroup{Name: &test}, - Path: path, - User: NodeUser{Name: &test}, - }, - DirectoryEmbedded1: DirectoryEmbedded1{Mode: &dirMode}, - } -} - -func encodeData(data string) *string { - str := "data:," + url.PathEscape(data) - return &str -} diff --git a/third_party/gvisor-tap-vsock/test-utils/ignition_schema.go b/third_party/gvisor-tap-vsock/test-utils/ignition_schema.go deleted file mode 100644 index e0dfe86f..00000000 --- a/third_party/gvisor-tap-vsock/test-utils/ignition_schema.go +++ /dev/null @@ -1,248 +0,0 @@ -package e2eutils - -// Taken from https://github.com/coreos/ignition/blob/master/config/v3_2/types/schema.go - -// generated by "schematyper --package=types config/v3_2/schema/ignition.json -o config/v3_2/types/schema.go --root-type=Config" -- DO NOT EDIT - -type Clevis struct { - Custom *Custom `json:"custom,omitempty"` - Tang []Tang `json:"tang,omitempty"` - Threshold *int `json:"threshold,omitempty"` - Tpm2 *bool `json:"tpm2,omitempty"` -} - -type Config struct { - Ignition Ignition `json:"ignition"` - Passwd Passwd `json:"passwd,omitempty"` - Storage Storage `json:"storage,omitempty"` - Systemd Systemd `json:"systemd,omitempty"` -} - -type Custom struct { - Config string `json:"config"` - NeedsNetwork *bool `json:"needsNetwork,omitempty"` - Pin string `json:"pin"` -} - -type Device string - -type Directory struct { - Node - DirectoryEmbedded1 -} - -type DirectoryEmbedded1 struct { - Mode *int `json:"mode,omitempty"` -} - -type Disk struct { - Device string `json:"device"` - Partitions []Partition `json:"partitions,omitempty"` - WipeTable *bool `json:"wipeTable,omitempty"` -} - -type Dropin struct { - Contents *string `json:"contents,omitempty"` - Name string `json:"name"` -} - -type File struct { - Node - FileEmbedded1 -} - -type FileEmbedded1 struct { - Append []Resource `json:"append,omitempty"` - Contents Resource `json:"contents,omitempty"` - Mode *int `json:"mode,omitempty"` -} - -type Filesystem struct { - Device string `json:"device"` - Format *string `json:"format,omitempty"` - Label *string `json:"label,omitempty"` - MountOptions []MountOption `json:"mountOptions,omitempty"` - Options []FilesystemOption `json:"options,omitempty"` - Path *string `json:"path,omitempty"` - UUID *string `json:"uuid,omitempty"` - WipeFilesystem *bool `json:"wipeFilesystem,omitempty"` -} - -type FilesystemOption string - -type Group string - -type HTTPHeader struct { - Name string `json:"name"` - Value *string `json:"value,omitempty"` -} - -type HTTPHeaders []HTTPHeader - -type Ignition struct { - Config IgnitionConfig `json:"config,omitempty"` - Proxy Proxy `json:"proxy,omitempty"` - Security Security `json:"security,omitempty"` - Timeouts Timeouts `json:"timeouts,omitempty"` - Version string `json:"version,omitempty"` -} - -type IgnitionConfig struct { - Merge []Resource `json:"merge,omitempty"` - Replace Resource `json:"replace,omitempty"` -} - -type Link struct { - Node - LinkEmbedded1 -} - -type LinkEmbedded1 struct { - Hard *bool `json:"hard,omitempty"` - Target string `json:"target"` -} - -type Luks struct { - Clevis *Clevis `json:"clevis,omitempty"` - Device *string `json:"device,omitempty"` - KeyFile Resource `json:"keyFile,omitempty"` - Label *string `json:"label,omitempty"` - Name string `json:"name"` - Options []LuksOption `json:"options,omitempty"` - UUID *string `json:"uuid,omitempty"` - WipeVolume *bool `json:"wipeVolume,omitempty"` -} - -type LuksOption string - -type MountOption string - -type NoProxyItem string - -type Node struct { - Group NodeGroup `json:"group,omitempty"` - Overwrite *bool `json:"overwrite,omitempty"` - Path string `json:"path"` - User NodeUser `json:"user,omitempty"` -} - -type NodeGroup struct { - ID *int `json:"id,omitempty"` - Name *string `json:"name,omitempty"` -} - -type NodeUser struct { - ID *int `json:"id,omitempty"` - Name *string `json:"name,omitempty"` -} - -type Partition struct { - GUID *string `json:"guid,omitempty"` - Label *string `json:"label,omitempty"` - Number int `json:"number,omitempty"` - Resize *bool `json:"resize,omitempty"` - ShouldExist *bool `json:"shouldExist,omitempty"` - SizeMiB *int `json:"sizeMiB,omitempty"` - StartMiB *int `json:"startMiB,omitempty"` - TypeGUID *string `json:"typeGuid,omitempty"` - WipePartitionEntry *bool `json:"wipePartitionEntry,omitempty"` -} - -type Passwd struct { - Groups []PasswdGroup `json:"groups,omitempty"` - Users []PasswdUser `json:"users,omitempty"` -} - -type PasswdGroup struct { - Gid *int `json:"gid,omitempty"` - Name string `json:"name"` - PasswordHash *string `json:"passwordHash,omitempty"` - ShouldExist *bool `json:"shouldExist,omitempty"` - System *bool `json:"system,omitempty"` -} - -type PasswdUser struct { - Gecos *string `json:"gecos,omitempty"` - Groups []Group `json:"groups,omitempty"` - HomeDir *string `json:"homeDir,omitempty"` - Name string `json:"name"` - NoCreateHome *bool `json:"noCreateHome,omitempty"` - NoLogInit *bool `json:"noLogInit,omitempty"` - NoUserGroup *bool `json:"noUserGroup,omitempty"` - PasswordHash *string `json:"passwordHash,omitempty"` - PrimaryGroup *string `json:"primaryGroup,omitempty"` - SSHAuthorizedKeys []SSHAuthorizedKey `json:"sshAuthorizedKeys,omitempty"` - Shell *string `json:"shell,omitempty"` - ShouldExist *bool `json:"shouldExist,omitempty"` - System *bool `json:"system,omitempty"` - UID *int `json:"uid,omitempty"` -} - -type Proxy struct { - HTTPProxy *string `json:"httpProxy,omitempty"` - HTTPSProxy *string `json:"httpsProxy,omitempty"` - NoProxy []NoProxyItem `json:"noProxy,omitempty"` -} - -type Raid struct { - Devices []Device `json:"devices"` - Level string `json:"level"` - Name string `json:"name"` - Options []RaidOption `json:"options,omitempty"` - Spares *int `json:"spares,omitempty"` -} - -type RaidOption string - -type Resource struct { - Compression *string `json:"compression,omitempty"` - HTTPHeaders HTTPHeaders `json:"httpHeaders,omitempty"` - Source *string `json:"source,omitempty"` - Verification Verification `json:"verification,omitempty"` -} - -type SSHAuthorizedKey string - -type Security struct { - TLS TLS `json:"tls,omitempty"` -} - -type Storage struct { - Directories []Directory `json:"directories,omitempty"` - Disks []Disk `json:"disks,omitempty"` - Files []File `json:"files,omitempty"` - Filesystems []Filesystem `json:"filesystems,omitempty"` - Links []Link `json:"links,omitempty"` - Luks []Luks `json:"luks,omitempty"` - Raid []Raid `json:"raid,omitempty"` -} - -type Systemd struct { - Units []Unit `json:"units,omitempty"` -} - -type TLS struct { - CertificateAuthorities []Resource `json:"certificateAuthorities,omitempty"` -} - -type Tang struct { - Thumbprint *string `json:"thumbprint,omitempty"` - URL string `json:"url,omitempty"` -} - -type Timeouts struct { - HTTPResponseHeaders *int `json:"httpResponseHeaders,omitempty"` - HTTPTotal *int `json:"httpTotal,omitempty"` -} - -type Unit struct { - Contents *string `json:"contents,omitempty"` - Dropins []Dropin `json:"dropins,omitempty"` - Enabled *bool `json:"enabled,omitempty"` - Mask *bool `json:"mask,omitempty"` - Name string `json:"name"` -} - -type Verification struct { - Hash *string `json:"hash,omitempty"` -} \ No newline at end of file diff --git a/third_party/gvisor-tap-vsock/test-utils/port.go b/third_party/gvisor-tap-vsock/test-utils/port.go deleted file mode 100644 index 3c34fe13..00000000 --- a/third_party/gvisor-tap-vsock/test-utils/port.go +++ /dev/null @@ -1,19 +0,0 @@ -package e2eutils - -import ( - "net" - "strconv" -) - -func IsPortAvailable(port int) bool { - return IsHostPortAvailable("127.0.0.1", port) -} - -func IsHostPortAvailable(host string, port int) bool { - listener, err := net.Listen("tcp", net.JoinHostPort(host, strconv.Itoa(port))) - if err != nil { - return false - } - listener.Close() - return true -} diff --git a/third_party/gvisor-tap-vsock/test-utils/pull.go b/third_party/gvisor-tap-vsock/test-utils/pull.go deleted file mode 100644 index 819c052e..00000000 --- a/third_party/gvisor-tap-vsock/test-utils/pull.go +++ /dev/null @@ -1,128 +0,0 @@ -package e2eutils - -import ( - "compress/gzip" - "fmt" - "io" - "net/http" - "os" - "os/exec" - "strings" - - "github.com/sirupsen/logrus" -) - -// DownloadVMImage downloads a VM image from url to given path -// with download status -func DownloadVMImage(downloadURL string, localImagePath string) error { - fmt.Println("Downloading VM image: " + downloadURL) - - out, err := os.Create(localImagePath) - if err != nil { - return err - } - defer func() { - if err := out.Close(); err != nil { - logrus.Error(err) - } - }() - - // #nosec - resp, err := http.Get(downloadURL) - if err != nil { - return err - } - defer func() { - if err := resp.Body.Close(); err != nil { - logrus.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("error downloading VM image %s: %s", downloadURL, resp.Status) - } - - if _, err := io.Copy(out, resp.Body); err != nil { - return err - } - - return nil -} - -func Decompress(localPath string) (string, error) { - uncompressedPath := "" - if strings.HasSuffix(localPath, ".xz") { - uncompressedPath = strings.TrimSuffix(localPath, ".xz") - } else if strings.HasSuffix(localPath, ".gz") { - uncompressedPath = strings.TrimSuffix(localPath, ".gz") - } - - if uncompressedPath == "" { - return "", fmt.Errorf("unsupported compression for %s", localPath) - } - - // we remove the uncompressed file if already exists. Maybe it has been used earlier and can affect the tests result - os.Remove(uncompressedPath) - - uncompressedFileWriter, err := os.OpenFile(uncompressedPath, os.O_CREATE|os.O_RDWR, 0600) - if err != nil { - return "", err - } - - fmt.Printf("Extracting %s\n", localPath) - if strings.HasSuffix(localPath, ".xz") { - err = decompressXZ(localPath, uncompressedFileWriter) - } else { - err = decompressGZ(localPath, uncompressedFileWriter) - } - - if err != nil { - return "", err - } - return uncompressedPath, nil -} - -// Will error out if file without .xz already exists -// Maybe extracting then renameing is a good idea here.. -// depends on xz: not pre-installed on mac, so it becomes a brew dependency -func decompressXZ(src string, output io.Writer) error { - cmd := exec.Command("xzcat", "-T0", "-k", src) - stdOut, err := cmd.StdoutPipe() - if err != nil { - return err - } - cmd.Stderr = os.Stderr - go func() { - if _, err := io.Copy(output, stdOut); err != nil { - logrus.Error(err) - } - }() - return cmd.Run() -} - -func decompressGZ(src string, output io.Writer) error { - file, err := os.Open(src) - if err != nil { - return err - } - defer file.Close() - - // Create a gzip reader - reader, err := gzip.NewReader(file) - if err != nil { - return err - } - defer reader.Close() - - for { - _, err := io.CopyN(output, reader, 1024) - if err != nil { - if err == io.EOF { - break - } - return err - } - } - - return nil -} diff --git a/third_party/gvisor-tap-vsock/test-utils/ssh.go b/third_party/gvisor-tap-vsock/test-utils/ssh.go deleted file mode 100644 index 219b411f..00000000 --- a/third_party/gvisor-tap-vsock/test-utils/ssh.go +++ /dev/null @@ -1,28 +0,0 @@ -package e2eutils - -import ( - "fmt" - "os" - "os/exec" - "strings" -) - -func CreateSSHKeys(publicKeyFile, privateKeyFile string) (string, error) { - _ = os.Remove(publicKeyFile) - _ = os.Remove(privateKeyFile) - err := exec.Command("ssh-keygen", "-N", "", "-t", "ed25519", "-f", privateKeyFile).Run() - if err != nil { - return "", fmt.Errorf("could not generate ssh keys: %w", err) - } - - return readPublicKey(publicKeyFile) -} - -func readPublicKey(publicKeyFile string) (string, error) { - publicKey, err := os.ReadFile(publicKeyFile) - if err != nil { - return "", nil - } - - return strings.TrimSpace(string(publicKey)), nil -} diff --git a/third_party/gvisor-tap-vsock/test-vfkit/basic_test.go b/third_party/gvisor-tap-vsock/test-vfkit/basic_test.go deleted file mode 100644 index b883003c..00000000 --- a/third_party/gvisor-tap-vsock/test-vfkit/basic_test.go +++ /dev/null @@ -1,104 +0,0 @@ -//go:build darwin - -package e2evfkit - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - "math" - "os" - "path" - "path/filepath" - "strings" - - e2e "github.com/containers/gvisor-tap-vsock/test" - "github.com/onsi/ginkgo" - "github.com/onsi/gomega" -) - -var _ = ginkgo.Describe("connectivity with vfkit", func() { - e2e.BasicConnectivityTests(e2e.BasicTestProps{ - SSHExec: sshExec, - }) -}) - -var _ = ginkgo.Describe("dns with vfkit", func() { - e2e.BasicDNSTests(e2e.BasicTestProps{ - SSHExec: sshExec, - Sock: sock, - }) -}) - -var _ = ginkgo.Describe("dhcp with vfkit", func() { - e2e.BasicDHCPTests(e2e.BasicTestProps{ - SSHExec: sshExec, - Sock: sock, - }) -}) - -var _ = ginkgo.Describe("upload and download with vfkit", func() { - tmpDir, err := os.MkdirTemp("", "vfkit") - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - sumMap := make(map[string]string) - dstDir := "/tmp" - ginkgo.AfterEach(func() { - err := os.RemoveAll(tmpDir) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - }) - ginkgo.It("should upload 1MB, 10MB, and 100MB files to vfkit", func() { - for _, size := range []int{6, 7, 8} { - file, err := os.CreateTemp(tmpDir, "testfile") - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - err = file.Truncate(int64(math.Pow10(size))) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - hasher := sha256.New() - _, err = io.Copy(hasher, file) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - srcPath := file.Name() - dstPath := filepath.Join(dstDir, path.Base(srcPath)) - - err = scpToVM(srcPath, dstDir) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - out, err := sshExec(fmt.Sprintf("sha256sum %s | awk '{print $1}'", dstPath)) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - localSum := hex.EncodeToString(hasher.Sum(nil)) - vmSum := strings.TrimSpace(string(out)) - gomega.Expect(vmSum).To(gomega.Equal(localSum)) - - sumMap[dstPath] = vmSum - } - }) - ginkgo.It("should download the uploaded files from vfkit", func() { - // Download the uploaded files - dlTmpDir, err := os.MkdirTemp("", "vfkit-dl") - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - for filename := range sumMap { - err = scpFromVM(filename, dlTmpDir) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - } - - dir, err := os.ReadDir(dlTmpDir) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - for _, entry := range dir { - hasher := sha256.New() - file, err := os.Open(filepath.Join(dlTmpDir, entry.Name())) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - _, err = io.Copy(hasher, file) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - gomega.Expect(hasher.Sum(nil)).NotTo(gomega.Equal(sumMap[entry.Name()])) - - } - // Set tmpDir to dlTmpDir for cleanup in AfterEach - tmpDir = dlTmpDir - }) -}) diff --git a/third_party/gvisor-tap-vsock/test-vfkit/vfkit_suite_test.go b/third_party/gvisor-tap-vsock/test-vfkit/vfkit_suite_test.go deleted file mode 100644 index 832ee12c..00000000 --- a/third_party/gvisor-tap-vsock/test-vfkit/vfkit_suite_test.go +++ /dev/null @@ -1,253 +0,0 @@ -//go:build darwin - -package e2evfkit - -import ( - "flag" - "fmt" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - "testing" - "time" - - e2e_utils "github.com/containers/gvisor-tap-vsock/test-utils" - - "github.com/onsi/ginkgo" - "github.com/onsi/gomega" - log "github.com/sirupsen/logrus" - - "golang.org/x/mod/semver" -) - -func TestSuite(t *testing.T) { - gomega.RegisterFailHandler(ginkgo.Fail) - ginkgo.RunSpecs(t, "gvisor-tap-vsock suite") -} - -const ( - sock = "/tmp/gvproxy-api-vfkit.sock" - vfkitSock = "/tmp/vfkit.sock" - sshPort = 2223 - ignitionUser = "test" - // #nosec "test" (for manual usage) - ignitionPasswordHash = "$y$j9T$TqJWt3/mKJbH0sYi6B/LD1$QjVRuUgntjTHjAdAkqhkr4F73m.Be4jBXdAaKw98sPC" // notsecret - efiStore = "efi-variable-store" - vfkitVersionNeeded = 0.6 -) - -var ( - tmpDir string - binDir string - host *exec.Cmd - client *exec.Cmd - privateKeyFile string - publicKeyFile string - ignFile string - cmdDir string -) - -var debugEnabled = flag.Bool("debug", false, "enable debugger") - -func init() { - flag.StringVar(&tmpDir, "tmpDir", "../tmp", "temporary working directory") - flag.StringVar(&binDir, "bin", "../bin", "directory with compiled binaries") - privateKeyFile = filepath.Join(tmpDir, "id_test_vfkit") - publicKeyFile = privateKeyFile + ".pub" - ignFile = filepath.Join(tmpDir, "test.ign") - cmdDir = "../cmd" -} - -var _ = ginkgo.BeforeSuite(func() { - // clear the environment before running the tests. It may happen the tests were abruptly stopped earlier leaving a dirty env - cleanup() - - // check if vfkit version is greater than v0.5 (ignition support is available starting from v0.6) - version, err := vfkitVersion() - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(version >= vfkitVersionNeeded).Should(gomega.BeTrue()) - - // check if ssh port is free - gomega.Expect(e2e_utils.IsPortAvailable(sshPort)).Should(gomega.BeTrue()) - - gomega.Expect(os.MkdirAll(filepath.Join(tmpDir, "disks"), os.ModePerm)).Should(gomega.Succeed()) - - downloader, err := e2e_utils.NewFcosDownloader(filepath.Join(tmpDir, "disks")) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - fcosImage, err := downloader.DownloadImage("applehv", "raw.gz") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - publicKey, err := e2e_utils.CreateSSHKeys(publicKeyFile, privateKeyFile) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - err = e2e_utils.CreateIgnition(ignFile, publicKey, ignitionUser, ignitionPasswordHash) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - errors := make(chan error) - -outer: - for panics := 0; ; panics++ { - _ = os.Remove(sock) - _ = os.Remove(vfkitSock) - - gvproxyArgs := []string{fmt.Sprintf("--ssh-port=%d", sshPort), fmt.Sprintf("--listen=unix://%s", sock), fmt.Sprintf("--listen-vfkit=unixgram://%s", vfkitSock)} - if *debugEnabled { - dlvArgs := []string{"debug", "--headless", "--listen=:2345", "--api-version=2", "--accept-multiclient", filepath.Join(cmdDir, "gvproxy"), "--"} - dlvArgs = append(dlvArgs, gvproxyArgs...) - host = exec.Command("dlv", dlvArgs...) - } else { - // #nosec - host = exec.Command(filepath.Join(binDir, "gvproxy"), gvproxyArgs...) - } - - host.Stderr = os.Stderr - host.Stdout = os.Stdout - gomega.Expect(host.Start()).Should(gomega.Succeed()) - go func() { - if err := host.Wait(); err != nil { - log.Error(err) - errors <- err - } - }() - - for { - _, err := os.Stat(sock) - if os.IsNotExist(err) { - log.Info("waiting for vfkit-api socket") - time.Sleep(100 * time.Millisecond) - continue - } - _, err = os.Stat(vfkitSock) - if os.IsNotExist(err) { - log.Info("waiting for vfkit socket") - time.Sleep(100 * time.Millisecond) - continue - } - break - } - - vfkitArgs := `--cpus 2 --memory 2048 --bootloader efi,variable-store=%s,create --device virtio-blk,path=%s --ignition %s --device virtio-net,unixSocketPath=%s,mac=5a:94:ef:e4:0c:ee` - // #nosec - client = exec.Command(vfkitExecutable(), strings.Split(fmt.Sprintf(vfkitArgs, efiStore, fcosImage, ignFile, vfkitSock), " ")...) - client.Stderr = os.Stderr - client.Stdout = os.Stdout - gomega.Expect(client.Start()).Should(gomega.Succeed()) - go func() { - if err := client.Wait(); err != nil { - log.Error(err) - errors <- err - } - }() - - for { - _, err := sshExec("whoami") - if err == nil { - break outer - } - - select { - case err := <-errors: - log.Errorf("Error %v", err) - // this expect will always fail so the tests stop - gomega.Expect(err).To(gomega.Equal(nil)) - break outer - case <-time.After(1 * time.Second): - log.Infof("waiting for client to connect: %v", err) - } - } - } - - time.Sleep(5 * time.Second) -}) - -func vfkitVersion() (float64, error) { - executable := vfkitExecutable() - if executable == "" { - return 0, fmt.Errorf("vfkit executable not found") - } - out, err := exec.Command(executable, "-v").Output() - if err != nil { - return 0, err - } - version := strings.TrimPrefix(string(out), "vfkit version:") - majorMinor := strings.TrimPrefix(semver.MajorMinor(strings.TrimSpace(version)), "v") - versionF, err := strconv.ParseFloat(majorMinor, 64) - if err != nil { - return 0, err - } - return versionF, nil -} - -func vfkitExecutable() string { - vfkitBinaries := []string{"vfkit"} - for _, binary := range vfkitBinaries { - path, err := exec.LookPath(binary) - if err == nil && path != "" { - return path - } - } - - return "" -} - -func sshExec(cmd ...string) ([]byte, error) { - return sshCommand(cmd...).Output() -} - -func sshCommand(cmd ...string) *exec.Cmd { - sshCmd := exec.Command("ssh", - "-o", "UserKnownHostsFile=/dev/null", - "-o", "StrictHostKeyChecking=no", - "-o", "IdentitiesOnly=yes", - "-i", privateKeyFile, - "-p", strconv.Itoa(sshPort), - fmt.Sprintf("%s@127.0.0.1", ignitionUser), "--", strings.Join(cmd, " ")) // #nosec G204 - return sshCmd -} - -func cleanup() { - _ = os.Remove(efiStore) - _ = os.Remove(sock) - _ = os.Remove(vfkitSock) - - // this should be handled by vfkit once https://github.com/crc-org/vfkit/pull/230 gets merged - // it removes the ignition.sock file - socketPath := filepath.Join(os.TempDir(), "ignition.sock") - _ = os.Remove(socketPath) -} - -func scp(src, dst string) error { - sshCmd := exec.Command("/usr/bin/scp", - "-o", "UserKnownHostsFile=/dev/null", - "-o", "StrictHostKeyChecking=no", - "-o", "IdentitiesOnly=yes", - "-i", privateKeyFile, - "-P", strconv.Itoa(sshPort), - src, dst) // #nosec G204 - sshCmd.Stderr = os.Stderr - sshCmd.Stdout = os.Stdout - return sshCmd.Run() -} -func scpToVM(src, dst string) error { - return scp(src, fmt.Sprintf("%s@127.0.0.1:%s", ignitionUser, dst)) -} - -func scpFromVM(src, dst string) error { - return scp(fmt.Sprintf("%s@127.0.0.1:%s", ignitionUser, src), dst) -} - -var _ = ginkgo.AfterSuite(func() { - if host != nil { - if err := host.Process.Kill(); err != nil { - log.Error(err) - } - } - if client != nil { - if err := client.Process.Kill(); err != nil { - log.Error(err) - } - } - cleanup() -}) diff --git a/third_party/gvisor-tap-vsock/test-win-sshproxy/basic_test.go b/third_party/gvisor-tap-vsock/test-win-sshproxy/basic_test.go deleted file mode 100644 index 80204fee..00000000 --- a/third_party/gvisor-tap-vsock/test-win-sshproxy/basic_test.go +++ /dev/null @@ -1,90 +0,0 @@ -//go:build windows - -package e2e - -import ( - "context" - "io" - "net" - "net/http" - "os" - "os/exec" - "strings" - "time" - - winio "github.com/Microsoft/go-winio" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var timeout = 1 * time.Minute - -var _ = Describe("connectivity", func() { - It("proxy exits as requested, without a kill", func() { - err := startProxy() - Expect(err).ShouldNot(HaveOccurred()) - - var pid, tid uint32 - for i := 0; i < 20; i++ { - pid, tid, err = readTid() - if err == nil && tid != 0 { - break - } - time.Sleep(100 * time.Millisecond) - } - - Expect(tid).ShouldNot(Equal(0)) - Expect(err).ShouldNot(HaveOccurred()) - proc, err := os.FindProcess(int(pid)) - Expect(err).ShouldNot(HaveOccurred()) - Expect(proc).ShouldNot(BeNil()) - err = stopProxy(true) - Expect(err).ShouldNot(HaveOccurred()) - }) - - It("proxies over a windows pipe", func() { - err := startProxy() - Expect(err).ShouldNot(HaveOccurred()) - defer func() { - Expect(stopProxy(false)).ShouldNot(HaveOccurred()) - }() - httpClient := &http.Client{ - Transport: &http.Transport{ - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return winio.DialPipe(`\\.\pipe\fake_docker_engine`, &timeout) - }, - }, - } - - Eventually(func(g Gomega) { - resp, err := httpClient.Get("http://host/ping") - g.Expect(err).ShouldNot(HaveOccurred()) - defer resp.Body.Close() - - g.Expect(resp.StatusCode).To(Equal(http.StatusOK)) - g.Expect(resp.ContentLength).To(Equal(int64(4))) - - reply := make([]byte, resp.ContentLength) - _, err = io.ReadAtLeast(resp.Body, reply, len(reply)) - - g.Expect(err).ShouldNot(HaveOccurred()) - g.Expect(string(reply)).To(Equal("pong")) - - }).Should(Succeed()) - }) - - It("windows event logs were created", func() { - cmd := exec.Command("powershell", "-Command", "&{Get-WinEvent -ProviderName \".NET Runtime\" -MaxEvents 10 | Where-Object -Property Message -Match \"test:\"}") - reader, err := cmd.StdoutPipe() - Expect(err).ShouldNot(HaveOccurred()) - err = cmd.Start() - Expect(err).ShouldNot(HaveOccurred()) - output, err := io.ReadAll(reader) - Expect(err).ShouldNot(HaveOccurred()) - err = cmd.Wait() - Expect(err).ShouldNot(HaveOccurred()) - Expect(strings.Contains(string(output), `[info ] test: Listening on: \\.\pipe\fake_docker_engine`)).Should(BeTrue()) - Expect(strings.Contains(string(output), `[debug] test: Socket forward established`)).Should(BeTrue()) - }) -}) diff --git a/third_party/gvisor-tap-vsock/test-win-sshproxy/mock_sshserver.go b/third_party/gvisor-tap-vsock/test-win-sshproxy/mock_sshserver.go deleted file mode 100644 index 41ac0638..00000000 --- a/third_party/gvisor-tap-vsock/test-win-sshproxy/mock_sshserver.go +++ /dev/null @@ -1,142 +0,0 @@ -//go:build windows - -package e2e - -import ( - "bufio" - "context" - "fmt" - "io" - "net" - "net/http" - "strings" - - "github.com/sirupsen/logrus" - "golang.org/x/crypto/ssh" -) - -const fakeHostKey = `-----BEGIN OPENSSH PRIVATE KEY----- -b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW -QyNTUxOQAAACAkXGLzDNnY5+xdAgnt8FlBIZtoFOZEdTUkNxkdSM05PgAAAJg9WMAvPVjA -LwAAAAtzc2gtZWQyNTUxOQAAACAkXGLzDNnY5+xdAgnt8FlBIZtoFOZEdTUkNxkdSM05Pg -AAAEAFvLprhpMPdNsxSwo1Cs5VP5joCh9XLicRqKE0JJzdxCRcYvMM2djn7F0CCe3wWUEh -m2gU5kR1NSQ3GR1IzTk+AAAAEmphc29uQFRyaXBlbC5sb2NhbAECAw== ------END OPENSSH PRIVATE KEY-----` - -type streamLocalDirect struct { - SocketPath string - Reserved0 string - Reserved1 uint32 -} - -var cancel context.CancelFunc - -func startMockServer() { - sshConfig := &ssh.ServerConfig{ - NoClientAuth: true, - } - - key, err := ssh.ParsePrivateKey([]byte(fakeHostKey)) - if err != nil { - logrus.Errorf("Could not parse key: %s", err) - } - sshConfig.AddHostKey(key) - - listener, err := net.Listen("tcp", "localhost:2134") - if err != nil { - panic(err) - } - - var ctx context.Context - ctx, cancel = context.WithCancel(context.Background()) - - go func() { - loop: - for { - select { - case <-ctx.Done(): - break loop - default: - // proceed - } - conn, err := listener.Accept() - if err != nil { - panic(err) - } - - // From a standard TCP connection to an encrypted SSH connection - _, chans, reqs, err := ssh.NewServerConn(conn, sshConfig) - if err != nil { - panic(err) - } - - go handleRequests(reqs) - // Accept all channels - go handleChannels(chans) - } - listener.Close() - }() -} - -func stopMockServer() { - cancel() -} - -func handleRequests(reqs <-chan *ssh.Request) { - for range reqs { - } -} - -func handleChannels(chans <-chan ssh.NewChannel) { - directMsg := streamLocalDirect{} - for newChannel := range chans { - if t := newChannel.ChannelType(); t != "direct-streamlocal@openssh.com" { - err := newChannel.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %s", t)) - if err != nil { - logrus.Errorf("could not reject channel: %s", err) - } - continue - } - - if err := ssh.Unmarshal(newChannel.ExtraData(), &directMsg); err != nil { - logrus.Errorf("could not direct-streamlocal data: %s", err) - - err = newChannel.Reject(ssh.Prohibited, "invalid format") - if err != nil { - logrus.Errorf("could not reject channel: %s", err) - } - return - } - - channel, _, err := newChannel.Accept() - if err != nil { - logrus.Errorf("could not accept channel: %s", err) - continue - } - - req, err := http.ReadRequest(bufio.NewReader(channel)) - if err != nil { - logrus.Errorf("could not process http request: %s", err) - } - - resp := http.Response{} - resp.Close = true - switch req.RequestURI { - case "/ping": - resp.StatusCode = 200 - resp.ContentLength = 4 - resp.Body = io.NopCloser(strings.NewReader("pong")) - default: - resp.StatusCode = 404 - resp.ContentLength = 0 - } - err = resp.Write(channel) - if err != nil { - logrus.Errorf("could not write response: %s", err) - } - err = channel.CloseWrite() - if err != nil { - logrus.Errorf("could not close write: %s", err) - } - } -} diff --git a/third_party/gvisor-tap-vsock/test-win-sshproxy/suite_test.go b/third_party/gvisor-tap-vsock/test-win-sshproxy/suite_test.go deleted file mode 100644 index 5a46e0a7..00000000 --- a/third_party/gvisor-tap-vsock/test-win-sshproxy/suite_test.go +++ /dev/null @@ -1,130 +0,0 @@ -//go:build windows - -package e2e - -import ( - "flag" - "fmt" - "os" - "os/exec" - "path/filepath" - "syscall" - "testing" - "time" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/sirupsen/logrus" -) - -const ( - WM_QUIT = 0x12 -) - -var ( - tmpDir string - binDir string - keyFile string - winSshProxy string - tidFile string -) - -func TestSuite(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "win-sshproxy suite") -} - -func init() { - flag.StringVar(&tmpDir, "tmpDir", "../tmp", "temporary working directory") - flag.StringVar(&binDir, "bin", "../bin", "directory with compiled binaries") - _ = os.MkdirAll(tmpDir, 0755) - keyFile = filepath.Join(tmpDir, "id.key") - _ = os.WriteFile(keyFile, []byte(fakeHostKey), 0600) - winSshProxy = filepath.Join(binDir, "win-sshproxy.exe") - tidFile = filepath.Join(tmpDir, "win-sshproxy.tid") -} - -var _ = BeforeSuite(func() { - startMockServer() -}) - -var _ = AfterSuite(func() { - stopMockServer() -}) - -func startProxy() error { - os.Remove(tidFile) - cmd := exec.Command(winSshProxy, "-debug", "test", tmpDir, "npipe:////./pipe/fake_docker_engine", "ssh://localhost:2134/run/podman/podman.sock", keyFile) - return cmd.Start() -} - -func readTid() (uint32, uint32, error) { - contents, err := os.ReadFile(tidFile) - if err != nil { - return 0, 0, err - } - - var pid, tid uint32 - _, err = fmt.Sscanf(string(contents), "%d:%d", &pid, &tid) - if err != nil { - return 0, 0, err - } - return pid, tid, nil -} - -func sendQuit(tid uint32) { - user32 := syscall.NewLazyDLL("user32.dll") - postMessage := user32.NewProc("PostThreadMessageW") - _, _, err := postMessage.Call(uintptr(tid), WM_QUIT, 0, 0) - if err != nil { - logrus.Errorf("Error posting quit message: %v", err) - } -} - -func stopProxy(noKill bool) error { - pid, tid, err := readTid() - if err != nil { - return err - } - - proc, err := os.FindProcess(int(pid)) - if err != nil { - return err - } - sendQuit(tid) - state := waitTimeout(proc, 20*time.Second) - if state == nil || !state.Exited() { - if noKill { - return fmt.Errorf("proxy did not exit on request") - } - _ = proc.Kill() - state = waitTimeout(proc, 20*time.Second) - } - - if state == nil || !state.Exited() { - return fmt.Errorf("Stop proxy failed: %d", pid) - } - - _ = os.Remove(tidFile) - return nil -} - -func waitTimeout(proc *os.Process, timeout time.Duration) *os.ProcessState { - return doTimeout(func(complete chan *os.ProcessState) { - state, _ := proc.Wait() - complete <- state - }, timeout) -} - -func doTimeout(action func(complete chan *os.ProcessState), timeout time.Duration) *os.ProcessState { - complete := make(chan *os.ProcessState) - - go action(complete) - select { - case <-time.After(timeout): - return nil - - case state := <-complete: - return state - } -} diff --git a/third_party/gvisor-tap-vsock/test/basic_tests.go b/third_party/gvisor-tap-vsock/test/basic_tests.go deleted file mode 100644 index a6aa3050..00000000 --- a/third_party/gvisor-tap-vsock/test/basic_tests.go +++ /dev/null @@ -1,221 +0,0 @@ -package e2e - -import ( - "context" - "net" - "net/http" - - gvproxyclient "github.com/containers/gvisor-tap-vsock/pkg/client" - - "github.com/containers/gvisor-tap-vsock/pkg/types" - "github.com/onsi/ginkgo" - "github.com/onsi/gomega" -) - -type BasicTestProps struct { - SSHExec func(cmd ...string) ([]byte, error) - Sock string -} - -func BasicConnectivityTests(props BasicTestProps) { - ginkgo.It("should configure the interface", func() { - out, err := props.SSHExec("ifconfig $(route | grep '^default' | grep -o '[^ ]*$')") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("mtu 1500")) - gomega.Expect(string(out)).To(gomega.ContainSubstring("inet 192.168.127.2")) - gomega.Expect(string(out)).To(gomega.ContainSubstring("netmask 255.255.255.0")) - gomega.Expect(string(out)).To(gomega.ContainSubstring("broadcast 192.168.127.255")) - }) - - ginkgo.It("should configure the default route", func() { - out, err := props.SSHExec("ip route show") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.MatchRegexp(`default via 192\.168\.127\.1 dev (.*?) proto dhcp (src 192\.168\.127\.2 )?metric 100`)) - }) - - ginkgo.It("should configure dns settings", func() { - out, err := props.SSHExec("cat /etc/resolv.conf") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("nameserver 192.168.127.1")) - }) - - ginkgo.It("should ping the tap device", func() { - out, err := props.SSHExec("ping -c2 192.168.127.2") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("2 packets transmitted, 2 received, 0% packet loss")) - }) - - ginkgo.It("should ping the gateway", func() { - out, err := props.SSHExec("ping -c2 192.168.127.1") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("2 packets transmitted, 2 received, 0% packet loss")) - }) -} - -func BasicDHCPTests(props BasicTestProps) { - ginkgo.It("should return DHCP leases", func() { - client := gvproxyclient.New(&http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - return net.Dial("unix", props.Sock) - }, - }, - }, "http://base") - leases, err := client.ListDHCPLeases() - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(leases).Should(gomega.HaveKeyWithValue("192.168.127.1", "5a:94:ef:e4:0c:dd")) - gomega.Expect(leases).Should(gomega.HaveKeyWithValue("192.168.127.2", "5a:94:ef:e4:0c:ee")) - }) - -} - -func BasicDNSTests(props BasicTestProps) { - ginkgo.It("should resolve redhat.com", func() { - out, err := props.SSHExec("nslookup redhat.com") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 52.200.142.250")) - }) - - ginkgo.It("should resolve CNAME record for docs.crc.dev", func() { - out, err := props.SSHExec("nslookup -query=cname docs.crc.dev") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("docs.crc.dev canonical name = webredir.gandi.net.")) - }) - ginkgo.It("should resolve MX record for crc.dev", func() { - out, err := props.SSHExec("nslookup -query=mx crc.dev") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("crc.dev mail exchanger = 10 spool.mail.gandi.net.")) - }) - - ginkgo.It("should resolve NS record for wikipedia.org", func() { - out, err := props.SSHExec("nslookup -query=ns wikipedia.org") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("wikipedia.org nameserver = ns0.wikimedia.org.")) - }) - ginkgo.It("should resolve IMAPS SRV record for crc.dev", func() { - out, err := props.SSHExec("nslookup -query=srv _imaps._tcp.crc.dev") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring(`_imaps._tcp.crc.dev service = 0 1 993 mail.gandi.net.`)) - }) - ginkgo.It("should resolve TXT for crc.dev", func() { - out, err := props.SSHExec("nslookup -query=txt crc.dev") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring(`text = "v=spf1`)) - }) - - ginkgo.It("should resolve gateway.containers.internal", func() { - out, err := props.SSHExec("nslookup gateway.containers.internal") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.1")) - }) - - ginkgo.It("should resolve host.containers.internal", func() { - out, err := props.SSHExec("nslookup host.containers.internal") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.254")) - }) - - ginkgo.It("should resolve dynamically added dns entry test.dynamic.internal", func() { - client := gvproxyclient.New(&http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - return net.Dial("unix", props.Sock) - }, - }, - }, "http://base") - err := client.AddDNS(&types.Zone{ - Name: "dynamic.internal.", - Records: []types.Record{ - { - Name: "test", - IP: net.ParseIP("192.168.127.254"), - }, - }, - }) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - out, err := props.SSHExec("nslookup test.dynamic.internal") - - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.254")) - }) - - ginkgo.It("should resolve recently added dns entry test.dynamic.internal", func() { - client := gvproxyclient.New(&http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - return net.Dial("unix", props.Sock) - }, - }, - }, "http://base") - err := client.AddDNS(&types.Zone{ - Name: "dynamic.internal.", - Records: []types.Record{ - { - Name: "test", - IP: net.ParseIP("192.168.127.254"), - }, - }, - }) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - err = client.AddDNS(&types.Zone{ - Name: "dynamic.internal.", - Records: []types.Record{ - { - Name: "test", - IP: net.ParseIP("192.168.127.253"), - }, - }, - }) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - - out, err := props.SSHExec("nslookup test.dynamic.internal") - - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.253")) - }) - - ginkgo.It("should retain order of existing zone", func() { - client := gvproxyclient.New(&http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - return net.Dial("unix", props.Sock) - }, - }, - }, "http://base") - _ = client.AddDNS(&types.Zone{ - Name: "dynamic.testing.", - DefaultIP: net.ParseIP("192.168.127.2"), - }) - _ = client.AddDNS(&types.Zone{ - Name: "testing.", - Records: []types.Record{ - { - Name: "host", - IP: net.ParseIP("192.168.127.3"), - }, - }, - }) - out, err := props.SSHExec("nslookup test.dynamic.internal") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.2")) - - _ = client.AddDNS(&types.Zone{ - Name: "testing.", - Records: []types.Record{ - { - Name: "gateway", - IP: net.ParseIP("192.168.127.1"), - }, - }, - }) - out, err = props.SSHExec("nslookup *.dynamic.testing") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.2")) - - out, err = props.SSHExec("nslookup gateway.testing") - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - gomega.Expect(string(out)).To(gomega.ContainSubstring("Address: 192.168.127.1")) - }) -} diff --git a/third_party/gvisor-tap-vsock/test/performance.sh b/third_party/gvisor-tap-vsock/test/performance.sh deleted file mode 100755 index 4f4c413a..00000000 --- a/third_party/gvisor-tap-vsock/test/performance.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash - -set -x - -VM=$1 -GVPROXY_SOCKET=$2 - -echo "Testing Internet access with a server running on the host" - -nohup iperf3 -s > /dev/null 2>&1 & -serverPID=$! - -ssh $VM curl https://iperf.fr/download/ubuntu/libiperf.so.0_3.1.3 -o libiperf.so.0 -ssh $VM curl https://iperf.fr/download/ubuntu/iperf3_3.1.3 -o iperf3 -ssh $VM chmod +x iperf3 - -echo "TCP: sending data" -ssh $VM LD_LIBRARY_PATH=. ./iperf3 -c host.crc.testing -echo "TCP: receiving data" -ssh $VM LD_LIBRARY_PATH=. ./iperf3 -c host.crc.testing -R - -echo "UDP: sending data" -ssh $VM LD_LIBRARY_PATH=. ./iperf3 -c host.crc.testing -u -echo "UDP: receiving data" -ssh $VM LD_LIBRARY_PATH=. ./iperf3 -c host.crc.testing -R -u - -kill $serverPID - -echo "Testing forwarder with a server running in the VM" - -curl --unix-socket $GVPROXY_SOCKET http:/unix/services/forwarder/expose -X POST \ - -d'{"local":":5201", "protocol": "udp", "remote": "192.168.127.2:5201"}' -curl --unix-socket $GVPROXY_SOCKET http:/unix/services/forwarder/expose -X POST \ - -d'{"local":":5201", "protocol": "tcp", "remote": "192.168.127.2:5201"}' - -ssh $VM LD_LIBRARY_PATH=. ./iperf3 -s > /dev/null 2>&1 & -sleep 1 - -echo "TCP: sending data" -iperf3 -c 127.0.0.1 -echo "TCP: receiving data" -iperf3 -c 127.0.0.1 -R - -echo "UDP: sending data" -iperf3 -c 127.0.0.1 -u -l 9216 -echo "UDP: receiving data" -iperf3 -c 127.0.0.1 -R -u -l 9216 - -ssh $VM pkill iperf3 - -curl --unix-socket $GVPROXY_SOCKET http:/unix/services/forwarder/unexpose -X POST \ - -d'{"local":":5201", "protocol": "udp"}' -curl --unix-socket $GVPROXY_SOCKET http:/unix/services/forwarder/unexpose -X POST \ - -d'{"local":":5201", "protocol": "tcp"}' diff --git a/third_party/gvisor-tap-vsock/test/wsl.sh b/third_party/gvisor-tap-vsock/test/wsl.sh deleted file mode 100755 index a28daf5c..00000000 --- a/third_party/gvisor-tap-vsock/test/wsl.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -set -x - -./bin/gvforwarder \ - -url="stdio:$(pwd)/bin/gvproxy-windows.exe?listen-stdio=accept&debug=true" \ - -iface="eth1" \ - -stop-if-exist="" \ - -debug diff --git a/third_party/lima/cmd/lima-driver-krunkit/main_darwin_arm64.go b/third_party/lima/cmd/lima-driver-krunkit/main_darwin_arm64.go index 1fa75ba6..8dda4753 100644 --- a/third_party/lima/cmd/lima-driver-krunkit/main_darwin_arm64.go +++ b/third_party/lima/cmd/lima-driver-krunkit/main_darwin_arm64.go @@ -6,8 +6,8 @@ package main import ( "context" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/driver/krunkit" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/krunkit" ) // To be used as an external driver for Lima. diff --git a/third_party/lima/cmd/lima-driver-qemu/main.go b/third_party/lima/cmd/lima-driver-qemu/main.go index 17521850..3e3ba5c3 100644 --- a/third_party/lima/cmd/lima-driver-qemu/main.go +++ b/third_party/lima/cmd/lima-driver-qemu/main.go @@ -6,8 +6,8 @@ package main import ( "context" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/driver/qemu" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/qemu" ) // To be used as an external driver for Lima. diff --git a/third_party/lima/cmd/lima-driver-vz/main_darwin.go b/third_party/lima/cmd/lima-driver-vz/main_darwin.go index 02e597c2..870c40b0 100644 --- a/third_party/lima/cmd/lima-driver-vz/main_darwin.go +++ b/third_party/lima/cmd/lima-driver-vz/main_darwin.go @@ -6,8 +6,8 @@ package main import ( "context" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/driver/vz" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/vz" ) // To be used as an external driver for Lima. diff --git a/third_party/lima/cmd/lima-driver-wsl2/main_windows.go b/third_party/lima/cmd/lima-driver-wsl2/main_windows.go index 279b7b09..e613cf7d 100644 --- a/third_party/lima/cmd/lima-driver-wsl2/main_windows.go +++ b/third_party/lima/cmd/lima-driver-wsl2/main_windows.go @@ -6,8 +6,8 @@ package main import ( "context" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/driver/wsl2" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/wsl2" ) // To be used as an external driver for Lima. diff --git a/third_party/lima/cmd/lima-guestagent/daemon_linux.go b/third_party/lima/cmd/lima-guestagent/daemon_linux.go index ab7c51e5..b9730ac7 100644 --- a/third_party/lima/cmd/lima-guestagent/daemon_linux.go +++ b/third_party/lima/cmd/lima-guestagent/daemon_linux.go @@ -15,11 +15,11 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/guestagent" - "github.com/lima-vm/lima/v2/pkg/guestagent/api/server" - "github.com/lima-vm/lima/v2/pkg/guestagent/serialport" - "github.com/lima-vm/lima/v2/pkg/guestagent/ticker" - "github.com/lima-vm/lima/v2/pkg/portfwdserver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/serialport" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/ticker" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/portfwdserver" ) const hostCID = 2 diff --git a/third_party/lima/cmd/lima-guestagent/fake_cloud_init_darwin.go b/third_party/lima/cmd/lima-guestagent/fake_cloud_init_darwin.go index 0083e7a6..2dcc9ec4 100644 --- a/third_party/lima/cmd/lima-guestagent/fake_cloud_init_darwin.go +++ b/third_party/lima/cmd/lima-guestagent/fake_cloud_init_darwin.go @@ -6,7 +6,7 @@ package main import ( "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/guestagent/fakecloudinit" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/fakecloudinit" ) func newFakeCloudInitCommand() *cobra.Command { diff --git a/third_party/lima/cmd/lima-guestagent/install_systemd_linux.go b/third_party/lima/cmd/lima-guestagent/install_systemd_linux.go index ab66aa23..21e6a583 100644 --- a/third_party/lima/cmd/lima-guestagent/install_systemd_linux.go +++ b/third_party/lima/cmd/lima-guestagent/install_systemd_linux.go @@ -17,7 +17,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/textutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/textutil" ) func newInstallSystemdCommand() *cobra.Command { diff --git a/third_party/lima/cmd/lima-guestagent/main.go b/third_party/lima/cmd/lima-guestagent/main.go index 0928eca4..327d28d9 100644 --- a/third_party/lima/cmd/lima-guestagent/main.go +++ b/third_party/lima/cmd/lima-guestagent/main.go @@ -9,10 +9,10 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/cmd/yq" - "github.com/lima-vm/lima/v2/pkg/debugutil" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/version" + "github.com/sylvinus/agent-vm/third_party/lima/cmd/yq" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/debugutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" ) func main() { diff --git a/third_party/lima/cmd/lima-privileged-net/main_linux.go b/third_party/lima/cmd/lima-privileged-net/main_linux.go index 8b8e4123..ef575e48 100644 --- a/third_party/lima/cmd/lima-privileged-net/main_linux.go +++ b/third_party/lima/cmd/lima-privileged-net/main_linux.go @@ -10,9 +10,9 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/version" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" ) func main() { diff --git a/third_party/lima/cmd/lima-privileged-net/network_linux.go b/third_party/lima/cmd/lima-privileged-net/network_linux.go index b922d678..ef5c6e2c 100644 --- a/third_party/lima/cmd/lima-privileged-net/network_linux.go +++ b/third_party/lima/cmd/lima-privileged-net/network_linux.go @@ -21,7 +21,7 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" ) // netConfig is the fully resolved description of a Lima network. Every value is diff --git a/third_party/lima/cmd/lima-privileged-net/sys_linux.go b/third_party/lima/cmd/lima-privileged-net/sys_linux.go index cc08963f..80e08617 100644 --- a/third_party/lima/cmd/lima-privileged-net/sys_linux.go +++ b/third_party/lima/cmd/lima-privileged-net/sys_linux.go @@ -20,8 +20,8 @@ import ( "github.com/sirupsen/logrus" "golang.org/x/sys/unix" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) const sysClassNet = "/sys/class/net" diff --git a/third_party/lima/cmd/limactl-mcp/main.go b/third_party/lima/cmd/limactl-mcp/main.go deleted file mode 100644 index 0195646d..00000000 --- a/third_party/lima/cmd/limactl-mcp/main.go +++ /dev/null @@ -1,253 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Lima Authors -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - - "github.com/modelcontextprotocol/go-sdk/mcp" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "golang.org/x/text/cases" - "golang.org/x/text/language" - - "github.com/lima-vm/lima/v2/pkg/limactlutil" - "github.com/lima-vm/lima/v2/pkg/mcp/toolset" - "github.com/lima-vm/lima/v2/pkg/version" -) - -func main() { - if err := newApp().Execute(); err != nil { - logrus.Fatal(err) - } -} - -func newApp() *cobra.Command { - cmd := &cobra.Command{ - Use: "limactl-mcp", - Short: "Model Context Protocol plugin for Lima (EXPERIMENTAL)", - Version: strings.TrimPrefix(version.Version, "v"), - SilenceUsage: true, - SilenceErrors: true, - } - cmd.AddCommand( - newMcpInfoCommand(), - newMcpServeCommand(), - newMcpGenDocCommand(), - // TODO: `limactl-mcp configure gemini` ? - ) - return cmd -} - -func newServer() *mcp.Server { - impl := &mcp.Implementation{ - Name: "lima", - Title: "Lima VM, for sandboxing local command executions and file I/O operations", - Version: version.Version, - } - serverOpts := &mcp.ServerOptions{ - Instructions: `This MCP server provides tools for sandboxing local command executions and file I/O operations, -by wrapping them in Lima VM (https://lima-vm.io). - -Use these tools to avoid accidentally executing malicious codes directly on the host. -`, - } - if runtime.GOOS != "linux" { - serverOpts.Instructions += fmt.Sprintf(` - -NOTE: the guest OS of the VM is Linux, while the host OS is %s. -`, cases.Title(language.English).String(runtime.GOOS)) - } - return mcp.NewServer(impl, serverOpts) -} - -func newMcpInfoCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "info", - Short: "Show information about the MCP server", - Args: cobra.NoArgs, - RunE: mcpInfoAction, - } - return cmd -} - -func mcpInfoAction(cmd *cobra.Command, _ []string) error { - ctx := cmd.Context() - info, err := inspectInfo(ctx) - if err != nil { - return err - } - j, err := json.MarshalIndent(info, "", " ") - if err != nil { - return err - } - _, err = fmt.Fprint(cmd.OutOrStdout(), string(j)) - return err -} - -func inspectInfo(ctx context.Context) (*Info, error) { - ts, err := toolset.New("") - if err != nil { - return nil, err - } - server := newServer() - if err = ts.RegisterServer(server); err != nil { - return nil, err - } - serverTransport, clientTransport := mcp.NewInMemoryTransports() - serverSession, err := server.Connect(ctx, serverTransport, nil) - if err != nil { - return nil, err - } - client := mcp.NewClient(&mcp.Implementation{Name: "client"}, nil) - clientSession, err := client.Connect(ctx, clientTransport, nil) - if err != nil { - return nil, err - } - toolsResult, err := clientSession.ListTools(ctx, &mcp.ListToolsParams{}) - if err != nil { - return nil, err - } - if err = clientSession.Close(); err != nil { - return nil, err - } - if err = serverSession.Wait(); err != nil { - return nil, err - } - info := &Info{ - Tools: toolsResult.Tools, - } - return info, nil -} - -type Info struct { - Tools []*mcp.Tool `json:"tools"` -} - -func newMcpServeCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "serve INSTANCE", - Short: "Serve MCP over stdio", - Long: `Serve MCP over stdio. - -Expected to be executed via an AI agent, not by a human`, - Args: cobra.MaximumNArgs(1), - RunE: mcpServeAction, - } - return cmd -} - -func mcpServeAction(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - instName := "default" - if len(args) > 0 { - instName = args[0] - } - limactl, err := limactlutil.Path() - if err != nil { - return err - } - // FIXME: We can not use store.Inspect() here because it requires VM drivers to be compiled in. - // https://github.com/lima-vm/lima/pull/3744#issuecomment-3289274347 - inst, err := limactlutil.Inspect(ctx, limactl, instName) - if err != nil { - return err - } - if len(inst.Errors) != 0 { - return errors.Join(inst.Errors...) - } - ts, err := toolset.New(limactl) - if err != nil { - return err - } - server := newServer() - if err = ts.RegisterServer(server); err != nil { - return err - } - if err = ts.RegisterInstance(ctx, inst); err != nil { - return err - } - transport := &mcp.StdioTransport{} - return server.Run(ctx, transport) -} - -func newMcpGenDocCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "generate-doc DIR", - Short: "Generate documentation pages", - Args: cobra.MinimumNArgs(1), - RunE: mcpGenDocAction, - Hidden: true, - } - return cmd -} - -func mcpGenDocAction(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - dir := args[0] - if err := os.MkdirAll(dir, 0o755); err != nil { - return err - } - fName := filepath.Join(dir, "mcp.md") - f, err := os.Create(fName) - if err != nil { - return err - } - defer f.Close() - fmt.Fprint(f, `--- -title: MCP tools -weight: 99 ---- -Lima implements the "MCP Sandbox Interface" (tentative name): -https://pkg.go.dev/github.com/lima-vm/lima/v2/pkg/mcp/msi - -MCP Sandbox Interface defines MCP (Model Context Protocol) tools -that can be used for reading, writing, and executing local files -with an appropriate sandboxing technology, such as Lima. - -The sandboxing technology can be more secure and/or efficient than -the default tools provided by an AI agent. - -MCP Sandbox Interface was inspired by -[Google Gemini CLI's built-in tools](https://github.com/google-gemini/gemini-cli/tree/main/docs/tools). - -`) - info, err := inspectInfo(ctx) - if err != nil { - return err - } - for _, tool := range info.Tools { - fmt.Fprintf(f, "## `%s`\n\n", tool.Name) - if tool.Title != "" { - fmt.Fprintf(f, "### Title\n\n%s\n\n", tool.Title) - } - if tool.Description != "" { - fmt.Fprintf(f, "### Description\n\n%s\n\n", tool.Description) - } - if tool.InputSchema != nil { - fmt.Fprint(f, "### Input Schema\n\n") - schema, err := json.MarshalIndent(tool.InputSchema, "", " ") - if err != nil { - return err - } - fmt.Fprintf(f, "```json\n%s\n```\n\n", string(schema)) - } - if tool.OutputSchema != nil { - fmt.Fprint(f, "### Output Schema\n\n") - schema, err := json.MarshalIndent(tool.OutputSchema, "", " ") - if err != nil { - return err - } - fmt.Fprintf(f, "```json\n%s\n```\n\n", string(schema)) - } - } - return f.Close() -} diff --git a/third_party/lima/cmd/limactl/autostart_darwin.go b/third_party/lima/cmd/limactl/autostart_darwin.go index 97e9a351..9506f32b 100644 --- a/third_party/lima/cmd/limactl/autostart_darwin.go +++ b/third_party/lima/cmd/limactl/autostart_darwin.go @@ -13,11 +13,11 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/autostart" - "github.com/lima-vm/lima/v2/pkg/autostart/launchd" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/textutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart/launchd" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/textutil" ) func autostartEnableAction(cmd *cobra.Command, args []string) error { diff --git a/third_party/lima/cmd/limactl/autostart_others.go b/third_party/lima/cmd/limactl/autostart_others.go index d9bf6d9c..999910d3 100644 --- a/third_party/lima/cmd/limactl/autostart_others.go +++ b/third_party/lima/cmd/limactl/autostart_others.go @@ -13,8 +13,8 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/autostart" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func autostartEnableAction(cmd *cobra.Command, args []string) error { diff --git a/third_party/lima/cmd/limactl/clone.go b/third_party/lima/cmd/limactl/clone.go index 11f489de..80d09beb 100644 --- a/third_party/lima/cmd/limactl/clone.go +++ b/third_party/lima/cmd/limactl/clone.go @@ -11,15 +11,15 @@ import ( "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/cmd/limactl/editflags" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/instance" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/networks/reconcile" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/yqutil" + "github.com/sylvinus/agent-vm/third_party/lima/cmd/limactl/editflags" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/reconcile" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/yqutil" ) func newCloneCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/completion.go b/third_party/lima/cmd/limactl/completion.go index b9d961a2..8fa6af7d 100644 --- a/third_party/lima/cmd/limactl/completion.go +++ b/third_party/lima/cmd/limactl/completion.go @@ -11,9 +11,9 @@ import ( "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/templatestore" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/templatestore" ) func bashCompleteInstanceNames(_ *cobra.Command) ([]string, cobra.ShellCompDirective) { diff --git a/third_party/lima/cmd/limactl/copy.go b/third_party/lima/cmd/limactl/copy.go index f2590903..7aa487cc 100644 --- a/third_party/lima/cmd/limactl/copy.go +++ b/third_party/lima/cmd/limactl/copy.go @@ -7,7 +7,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/copytool" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/copytool" ) const copyHelp = `Copy files between host and guest diff --git a/third_party/lima/cmd/limactl/debug.go b/third_party/lima/cmd/limactl/debug.go index 67b86ffa..a81bdd51 100644 --- a/third_party/lima/cmd/limactl/debug.go +++ b/third_party/lima/cmd/limactl/debug.go @@ -10,7 +10,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/hostagent/dns" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/dns" ) func newDebugCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/delete.go b/third_party/lima/cmd/limactl/delete.go index c687dd36..f58cbf48 100644 --- a/third_party/lima/cmd/limactl/delete.go +++ b/third_party/lima/cmd/limactl/delete.go @@ -11,12 +11,12 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/autostart" - "github.com/lima-vm/lima/v2/pkg/instance" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/networks/reconcile" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/reconcile" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func newDeleteCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/disk.go b/third_party/lima/cmd/limactl/disk.go index 6d40ffd2..8a42898f 100644 --- a/third_party/lima/cmd/limactl/disk.go +++ b/third_party/lima/cmd/limactl/disk.go @@ -18,10 +18,10 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/proxyimgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func newDiskCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/edit.go b/third_party/lima/cmd/limactl/edit.go index 70107d0b..5743390c 100644 --- a/third_party/lima/cmd/limactl/edit.go +++ b/third_party/lima/cmd/limactl/edit.go @@ -13,20 +13,20 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/cmd/limactl/editflags" - "github.com/lima-vm/lima/v2/pkg/autostart" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/editutil" - "github.com/lima-vm/lima/v2/pkg/instance" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/networks/reconcile" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/uiutil" - "github.com/lima-vm/lima/v2/pkg/yqutil" + "github.com/sylvinus/agent-vm/third_party/lima/cmd/limactl/editflags" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/editutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/reconcile" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/uiutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/yqutil" ) func newEditCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/editflags/editflags.go b/third_party/lima/cmd/limactl/editflags/editflags.go index f1223463..e84b3663 100644 --- a/third_party/lima/cmd/limactl/editflags/editflags.go +++ b/third_party/lima/cmd/limactl/editflags/editflags.go @@ -17,8 +17,8 @@ import ( "github.com/spf13/cobra" flag "github.com/spf13/pflag" - "github.com/lima-vm/lima/v2/pkg/localpathutil" - "github.com/lima-vm/lima/v2/pkg/registry" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/localpathutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/registry" ) // RegisterEdit registers flags related to in-place YAML modification, for `limactl edit`. diff --git a/third_party/lima/cmd/limactl/editflags/editflags_test.go b/third_party/lima/cmd/limactl/editflags/editflags_test.go index 4160d231..d6645c7f 100644 --- a/third_party/lima/cmd/limactl/editflags/editflags_test.go +++ b/third_party/lima/cmd/limactl/editflags/editflags_test.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/cobra" "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/localpathutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/localpathutil" ) func TestCompleteCPUs(t *testing.T) { diff --git a/third_party/lima/cmd/limactl/factory-reset.go b/third_party/lima/cmd/limactl/factory-reset.go index 0176b922..e73bde1d 100644 --- a/third_party/lima/cmd/limactl/factory-reset.go +++ b/third_party/lima/cmd/limactl/factory-reset.go @@ -12,12 +12,12 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/cidata" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/instance" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/cidata" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func newFactoryResetCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/genschema.go b/third_party/lima/cmd/limactl/genschema.go index 993257d5..f756d741 100644 --- a/third_party/lima/cmd/limactl/genschema.go +++ b/third_party/lima/cmd/limactl/genschema.go @@ -14,8 +14,8 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/jsonschemautil" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/jsonschemautil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) func newGenSchemaCommand() *cobra.Command { @@ -53,7 +53,7 @@ func genschemaAction(cmd *cobra.Command, args []string) error { } reflector := new(jsonschema.Reflector) - err = reflector.AddGoComments("github.com/lima-vm/lima/v2", "./") + err = reflector.AddGoComments("github.com/sylvinus/agent-vm/third_party/lima", "./") if err != nil { return err } diff --git a/third_party/lima/cmd/limactl/guest-install.go b/third_party/lima/cmd/limactl/guest-install.go index 15a94bb4..9fdaee3e 100644 --- a/third_party/lima/cmd/limactl/guest-install.go +++ b/third_party/lima/cmd/limactl/guest-install.go @@ -17,11 +17,11 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/cacheutil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/usrlocal" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/cacheutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/usrlocal" ) func newGuestInstallCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/hostagent.go b/third_party/lima/cmd/limactl/hostagent.go index a8751800..27cb523f 100644 --- a/third_party/lima/cmd/limactl/hostagent.go +++ b/third_party/lima/cmd/limactl/hostagent.go @@ -19,9 +19,9 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/hostagent" - "github.com/lima-vm/lima/v2/pkg/hostagent/api/server" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/api/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func newHostagentCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/info.go b/third_party/lima/cmd/limactl/info.go index 80a9821a..1bd7a666 100644 --- a/third_party/lima/cmd/limactl/info.go +++ b/third_party/lima/cmd/limactl/info.go @@ -10,9 +10,9 @@ import ( "github.com/mikefarah/yq/v4/pkg/yqlib" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/limainfo" - "github.com/lima-vm/lima/v2/pkg/uiutil" - "github.com/lima-vm/lima/v2/pkg/yqutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limainfo" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/uiutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/yqutil" ) func newInfoCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/list.go b/third_party/lima/cmd/limactl/list.go index 784c00f1..7accc079 100644 --- a/third_party/lima/cmd/limactl/list.go +++ b/third_party/lima/cmd/limactl/list.go @@ -20,11 +20,11 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/uiutil" - "github.com/lima-vm/lima/v2/pkg/yqutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/uiutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/yqutil" ) func fieldNames() []string { diff --git a/third_party/lima/cmd/limactl/main.go b/third_party/lima/cmd/limactl/main.go index 378f5feb..f31d5553 100644 --- a/third_party/lima/cmd/limactl/main.go +++ b/third_party/lima/cmd/limactl/main.go @@ -15,15 +15,15 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/cmd/yq" - "github.com/lima-vm/lima/v2/pkg/debugutil" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/envutil" - "github.com/lima-vm/lima/v2/pkg/fsutil" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/plugins" - "github.com/lima-vm/lima/v2/pkg/version" + "github.com/sylvinus/agent-vm/third_party/lima/cmd/yq" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/debugutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/envutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/fsutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/plugins" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" ) const ( diff --git a/third_party/lima/cmd/limactl/main_darwin.go b/third_party/lima/cmd/limactl/main_darwin.go index 2f921dd6..59df7535 100644 --- a/third_party/lima/cmd/limactl/main_darwin.go +++ b/third_party/lima/cmd/limactl/main_darwin.go @@ -6,4 +6,4 @@ package main // Import vz driver to register it in the registry on darwin. -import _ "github.com/lima-vm/lima/v2/pkg/driver/vz" +import _ "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/vz" diff --git a/third_party/lima/cmd/limactl/main_hcs_windows.go b/third_party/lima/cmd/limactl/main_hcs_windows.go index 1b8b5a64..a5c64932 100644 --- a/third_party/lima/cmd/limactl/main_hcs_windows.go +++ b/third_party/lima/cmd/limactl/main_hcs_windows.go @@ -6,4 +6,4 @@ package main // Import hcs driver to register it in the registry on windows. -import _ "github.com/lima-vm/lima/v2/pkg/driver/hcs" +import _ "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/hcs" diff --git a/third_party/lima/cmd/limactl/main_qemu.go b/third_party/lima/cmd/limactl/main_qemu.go index 73e15667..f78acf9d 100644 --- a/third_party/lima/cmd/limactl/main_qemu.go +++ b/third_party/lima/cmd/limactl/main_qemu.go @@ -6,4 +6,4 @@ package main // Import qemu driver to register it in the registry on all platforms. -import _ "github.com/lima-vm/lima/v2/pkg/driver/qemu" +import _ "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/qemu" diff --git a/third_party/lima/cmd/limactl/main_wsl2_windows.go b/third_party/lima/cmd/limactl/main_wsl2_windows.go index 3438cdbc..c7e6caf7 100644 --- a/third_party/lima/cmd/limactl/main_wsl2_windows.go +++ b/third_party/lima/cmd/limactl/main_wsl2_windows.go @@ -6,4 +6,4 @@ package main // Import wsl2 driver to register it in the registry on windows. -import _ "github.com/lima-vm/lima/v2/pkg/driver/wsl2" +import _ "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/wsl2" diff --git a/third_party/lima/cmd/limactl/network.go b/third_party/lima/cmd/limactl/network.go index 0fc7b5ee..81b3684d 100644 --- a/third_party/lima/cmd/limactl/network.go +++ b/third_party/lima/cmd/limactl/network.go @@ -18,8 +18,8 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/yqutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/yqutil" ) const networkExample = ` List all networks: diff --git a/third_party/lima/cmd/limactl/protect.go b/third_party/lima/cmd/limactl/protect.go index eed1c806..f8c6bbcf 100644 --- a/third_party/lima/cmd/limactl/protect.go +++ b/third_party/lima/cmd/limactl/protect.go @@ -10,7 +10,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func newProtectCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/prune.go b/third_party/lima/cmd/limactl/prune.go index c2453aa6..8e9c1cba 100644 --- a/third_party/lima/cmd/limactl/prune.go +++ b/third_party/lima/cmd/limactl/prune.go @@ -13,13 +13,13 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/downloader" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/limatmpl" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/templatestore" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/downloader" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatmpl" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/templatestore" ) func newPruneCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/restart.go b/third_party/lima/cmd/limactl/restart.go index 3de91129..124bdd18 100644 --- a/third_party/lima/cmd/limactl/restart.go +++ b/third_party/lima/cmd/limactl/restart.go @@ -12,14 +12,14 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/cmd/limactl/editflags" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/instance" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/yqutil" + "github.com/sylvinus/agent-vm/third_party/lima/cmd/limactl/editflags" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/yqutil" ) func newRestartCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/screenshot.go b/third_party/lima/cmd/limactl/screenshot.go index 34dbe080..87627e73 100644 --- a/third_party/lima/cmd/limactl/screenshot.go +++ b/third_party/lima/cmd/limactl/screenshot.go @@ -12,10 +12,10 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/hostagent/api/client" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/api/client" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func newScreenshotCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/shell.go b/third_party/lima/cmd/limactl/shell.go index 705b585a..af7b316e 100644 --- a/third_party/lima/cmd/limactl/shell.go +++ b/third_party/lima/cmd/limactl/shell.go @@ -17,23 +17,23 @@ import ( "al.essio.dev/pkg/shellescape" "github.com/coreos/go-semver/semver" - "github.com/lima-vm/sshocker/pkg/ssh" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/ssh" "github.com/mattn/go-isatty" "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/autostart" - "github.com/lima-vm/lima/v2/pkg/copytool" - "github.com/lima-vm/lima/v2/pkg/envutil" - "github.com/lima-vm/lima/v2/pkg/fsutil" - "github.com/lima-vm/lima/v2/pkg/instance" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/localpathutil" - "github.com/lima-vm/lima/v2/pkg/networks/reconcile" - "github.com/lima-vm/lima/v2/pkg/sshutil" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/uiutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/copytool" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/envutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/fsutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/localpathutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/reconcile" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/sshutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/uiutil" ) const shellHelp = `Execute shell in Lima diff --git a/third_party/lima/cmd/limactl/show-ssh.go b/third_party/lima/cmd/limactl/show-ssh.go index d870649a..9819fb46 100644 --- a/third_party/lima/cmd/limactl/show-ssh.go +++ b/third_party/lima/cmd/limactl/show-ssh.go @@ -13,10 +13,10 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/sshutil" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/sshutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) const showSSHExample = ` diff --git a/third_party/lima/cmd/limactl/snapshot.go b/third_party/lima/cmd/limactl/snapshot.go index 826679d8..cae7eab3 100644 --- a/third_party/lima/cmd/limactl/snapshot.go +++ b/third_party/lima/cmd/limactl/snapshot.go @@ -11,8 +11,8 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/snapshot" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/snapshot" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func newSnapshotCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/start-at-login_unix.go b/third_party/lima/cmd/limactl/start-at-login_unix.go index 17ca54b1..8d4b3cfd 100644 --- a/third_party/lima/cmd/limactl/start-at-login_unix.go +++ b/third_party/lima/cmd/limactl/start-at-login_unix.go @@ -13,8 +13,8 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/autostart" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func startAtLoginAction(cmd *cobra.Command, args []string) error { diff --git a/third_party/lima/cmd/limactl/start.go b/third_party/lima/cmd/limactl/start.go index 53425c15..4abdda30 100644 --- a/third_party/lima/cmd/limactl/start.go +++ b/third_party/lima/cmd/limactl/start.go @@ -16,23 +16,23 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/lima-vm/lima/v2/cmd/limactl/editflags" - "github.com/lima-vm/lima/v2/pkg/autostart" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/editutil" - "github.com/lima-vm/lima/v2/pkg/instance" - "github.com/lima-vm/lima/v2/pkg/limatmpl" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/networks/reconcile" - "github.com/lima-vm/lima/v2/pkg/registry" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/templatestore" - "github.com/lima-vm/lima/v2/pkg/uiutil" - "github.com/lima-vm/lima/v2/pkg/yqutil" + "github.com/sylvinus/agent-vm/third_party/lima/cmd/limactl/editflags" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/editutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatmpl" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/reconcile" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/registry" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/templatestore" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/uiutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/yqutil" ) func registerCreateFlags(cmd *cobra.Command, commentPrefix string) { diff --git a/third_party/lima/cmd/limactl/start_test.go b/third_party/lima/cmd/limactl/start_test.go index 1165ef4c..ec7380de 100644 --- a/third_party/lima/cmd/limactl/start_test.go +++ b/third_party/lima/cmd/limactl/start_test.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) func TestLoadOrCreateInstance(t *testing.T) { diff --git a/third_party/lima/cmd/limactl/stop.go b/third_party/lima/cmd/limactl/stop.go index 856a1603..cae39a62 100644 --- a/third_party/lima/cmd/limactl/stop.go +++ b/third_party/lima/cmd/limactl/stop.go @@ -8,9 +8,9 @@ import ( "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/instance" - "github.com/lima-vm/lima/v2/pkg/networks/reconcile" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/reconcile" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func newStopCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/sudoers.go b/third_party/lima/cmd/limactl/sudoers.go index b4025d4f..f7d63d99 100644 --- a/third_party/lima/cmd/limactl/sudoers.go +++ b/third_party/lima/cmd/limactl/sudoers.go @@ -8,7 +8,7 @@ import ( "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" ) const socketVMNetURL = "https://lima-vm.io/docs/config/network/vmnet/#socket_vmnet" diff --git a/third_party/lima/cmd/limactl/sudoers_darwin.go b/third_party/lima/cmd/limactl/sudoers_darwin.go index 8b174153..2edb72da 100644 --- a/third_party/lima/cmd/limactl/sudoers_darwin.go +++ b/third_party/lima/cmd/limactl/sudoers_darwin.go @@ -12,7 +12,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" ) func sudoersAction(cmd *cobra.Command, args []string) error { diff --git a/third_party/lima/cmd/limactl/tab_completion.go b/third_party/lima/cmd/limactl/tab_completion.go index 160ba4cf..3e114bde 100644 --- a/third_party/lima/cmd/limactl/tab_completion.go +++ b/third_party/lima/cmd/limactl/tab_completion.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) var ( diff --git a/third_party/lima/cmd/limactl/template.go b/third_party/lima/cmd/limactl/template.go index 31b53784..5b2e4c95 100644 --- a/third_party/lima/cmd/limactl/template.go +++ b/third_party/lima/cmd/limactl/template.go @@ -13,12 +13,12 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/limatmpl" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/uiutil" - "github.com/lima-vm/lima/v2/pkg/yqutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatmpl" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/uiutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/yqutil" ) func newTemplateCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/tunnel.go b/third_party/lima/cmd/limactl/tunnel.go index 5c1f4fcd..9cf184cb 100644 --- a/third_party/lima/cmd/limactl/tunnel.go +++ b/third_party/lima/cmd/limactl/tunnel.go @@ -14,10 +14,10 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/freeport" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/sshutil" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/freeport" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/sshutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) const tunnelHelp = `Create a tunnel for Lima diff --git a/third_party/lima/cmd/limactl/unprotect.go b/third_party/lima/cmd/limactl/unprotect.go index 58d3ba65..07ec3fea 100644 --- a/third_party/lima/cmd/limactl/unprotect.go +++ b/third_party/lima/cmd/limactl/unprotect.go @@ -10,7 +10,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func newUnprotectCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/usernet.go b/third_party/lima/cmd/limactl/usernet.go index 8a507c59..1a2edd05 100644 --- a/third_party/lima/cmd/limactl/usernet.go +++ b/third_party/lima/cmd/limactl/usernet.go @@ -13,7 +13,7 @@ import ( "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/networks/usernet" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/usernet" ) func newUsernetCommand() *cobra.Command { diff --git a/third_party/lima/cmd/limactl/watch.go b/third_party/lima/cmd/limactl/watch.go index ff797826..9722bb8c 100644 --- a/third_party/lima/cmd/limactl/watch.go +++ b/third_party/lima/cmd/limactl/watch.go @@ -18,9 +18,9 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/lima-vm/lima/v2/pkg/hostagent/events" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/events" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func newWatchCommand() *cobra.Command { diff --git a/third_party/lima/go.mod b/third_party/lima/go.mod deleted file mode 100644 index c52fc93f..00000000 --- a/third_party/lima/go.mod +++ /dev/null @@ -1,157 +0,0 @@ -// gomodjail:confined -module github.com/lima-vm/lima/v2 - -go 1.26.0 - -// Our own packages and golang.org/x packages are trusted -//gosocialcheck:trusted -require ( - github.com/lima-vm/go-qcow2reader v0.8.0 // gomodjail:unconfined - github.com/lima-vm/sshocker v0.3.11 // gomodjail:unconfined - golang.org/x/image v0.46.0 - golang.org/x/net v0.59.0 // gomodjail:unconfined - golang.org/x/sync v0.23.0 - golang.org/x/sys v0.48.0 // gomodjail:unconfined - golang.org/x/text v0.42.0 -) - -require ( - al.essio.dev/pkg/shellescape v1.6.1 - github.com/AlecAivazis/survey/v2 v2.3.7 // gomodjail:unconfined - github.com/Code-Hex/vz/v3 v3.7.1 // gomodjail:unconfined - github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // gomodjail:unconfined - github.com/Microsoft/hcsshim v0.14.1 - github.com/apparentlymart/go-cidr v1.1.1 - github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e // gomodjail:unconfined - github.com/cheggaaa/pb/v3 v3.2.1 // gomodjail:unconfined - github.com/cilium/ebpf v0.22.0 // gomodjail:unconfined - github.com/containerd/continuity v0.5.0 // gomodjail:unconfined - github.com/containers/gvisor-tap-vsock v0.8.9 // gomodjail:unconfined - github.com/coreos/go-semver v0.3.1 - github.com/coreos/go-systemd/v22 v22.7.0 // gomodjail:unconfined - github.com/cpuguy83/go-md2man/v2 v2.0.7 - github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7 // gomodjail:unconfined - github.com/diskfs/go-diskfs v1.9.4 // gomodjail:unconfined - github.com/docker/go-units v0.5.0 - github.com/foxcpp/go-mockdns v1.2.0 - github.com/goccy/go-yaml v1.19.2 // gomodjail:unconfined - github.com/google/go-cmp v0.7.0 - github.com/google/yamlfmt v0.21.0 // gomodjail:unconfined - github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 // gomodjail:unconfined - github.com/invopop/jsonschema v0.14.0 // gomodjail:unconfined - github.com/mattn/go-isatty v0.0.24 // gomodjail:unconfined - github.com/mattn/go-shellwords v1.0.15 // gomodjail:unconfined - github.com/mdlayher/netlink v1.11.2 // gomodjail:unconfined - github.com/mdlayher/vsock v1.3.0 // gomodjail:unconfined - github.com/miekg/dns v1.1.73 // gomodjail:unconfined - github.com/mikefarah/yq/v4 v4.53.6 // gomodjail:unconfined - github.com/modelcontextprotocol/go-sdk v1.8.0 // gomodjail:unconfined - github.com/nxadm/tail v1.4.11 // gomodjail:unconfined - github.com/opencontainers/go-digest v1.0.0 - github.com/pb33f/ordered-map/v2 v2.3.1 - github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // gomodjail:unconfined - github.com/pkg/sftp v1.13.11 // gomodjail:unconfined - github.com/rjeczalik/notify v0.9.3 // gomodjail:unconfined - github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // gomodjail:unconfined - github.com/sethvargo/go-password v0.4.0 - github.com/sirupsen/logrus v1.10.2 // gomodjail:unconfined - github.com/spakin/netpbm v1.3.2 - github.com/spf13/cobra v1.10.2 // gomodjail:unconfined - github.com/spf13/pflag v1.0.10 - google.golang.org/grpc v1.83.2 // gomodjail:unconfined - google.golang.org/protobuf v1.36.12 // gomodjail:unconfined - gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 // gomodjail:unconfined - gotest.tools/v3 v3.5.2 -) - -//gosocialcheck:trusted -require ( - // gomodjail:unconfined - golang.org/x/crypto v0.57.0 // indirect - golang.org/x/mod v0.41.0 // indirect - // gomodjail:unconfined - golang.org/x/oauth2 v0.36.0 // indirect - // gomodjail:unconfined - golang.org/x/term v0.46.0 // indirect - golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.49.0 // indirect -) - -require ( - github.com/Code-Hex/go-infinity-channel v1.0.0 // indirect - github.com/VividCortex/ewma v1.2.0 // indirect - github.com/a8m/envsubst v1.4.3 // indirect - github.com/agext/levenshtein v1.2.1 // indirect - github.com/alecthomas/participle/v2 v2.1.4 // indirect - github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect - github.com/bahlo/generic-list-go v0.2.0 // indirect - // gomodjail:unconfined - github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect - github.com/buger/jsonparser v1.1.2 // indirect - github.com/containerd/cgroups/v3 v3.1.3 // indirect - github.com/containerd/errdefs v1.0.0 // indirect - github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/containerd/typeurl/v2 v2.3.0 // indirect - // gomodjail:unconfined - github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e // indirect - github.com/dimchansky/utfbom v1.1.1 // indirect - // gomodjail:unconfined - github.com/djherbis/times v1.6.0 // indirect - github.com/elliotchance/orderedmap v1.8.0 // indirect - // gomodjail:unconfined - github.com/fatih/color v1.19.0 // indirect - // gomodjail:unconfined - github.com/fsnotify/fsnotify v1.8.0 // indirect - // gomodjail:unconfined - github.com/go-ini/ini v1.67.0 // indirect - github.com/goccy/go-json v0.10.6 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/google/btree v1.1.3 // indirect - github.com/google/gopacket v1.1.19 // indirect - github.com/google/jsonschema-go v0.4.3 // indirect - github.com/hashicorp/hcl/v2 v2.24.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - // gomodjail:unconfined - github.com/insomniacslk/dhcp v0.0.0-20240710054256-ddd8a41251c9 // indirect - github.com/jinzhu/copier v0.4.0 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - // gomodjail:unconfined - github.com/kr/fs v0.1.0 // indirect - github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 // indirect - // gomodjail:unconfined - github.com/magiconair/properties v1.18.11 // indirect - github.com/mattn/go-colorable v0.1.15 // indirect - github.com/mattn/go-runewidth v0.0.27 // indirect - // gomodjail:unconfined - github.com/mdlayher/socket v0.6.0 // indirect - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect - github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.4.3 // indirect - github.com/pierrec/lz4/v4 v4.1.26 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - // gomodjail:unconfined - github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect - github.com/segmentio/asm v1.2.1 // indirect - github.com/segmentio/encoding v0.5.4 // indirect - // gomodjail:unconfined - github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect - github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - // gomodjail:unconfined - github.com/yuin/gopher-lua v1.1.2 // indirect - github.com/zclconf/go-cty v1.19.0 // indirect - go.opencensus.io v0.24.0 // indirect - go.yaml.in/yaml/v3 v3.0.5 // indirect - go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - // gomodjail:unconfined - gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f // indirect -) - -require ( - github.com/apparentlymart/go-textseg/v17 v17.0.1 // indirect - github.com/clipperhouse/uax29/v2 v2.2.0 // indirect -) diff --git a/third_party/lima/go.sum b/third_party/lima/go.sum deleted file mode 100644 index c67e20c9..00000000 --- a/third_party/lima/go.sum +++ /dev/null @@ -1,486 +0,0 @@ -al.essio.dev/pkg/shellescape v1.6.1 h1:ki/kUs/D3dDbZpyKbNRhwY/ajgAEIJFagVKDWKnofQ8= -al.essio.dev/pkg/shellescape v1.6.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= -github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Code-Hex/go-infinity-channel v1.0.0 h1:M8BWlfDOxq9or9yvF9+YkceoTkDI1pFAqvnP87Zh0Nw= -github.com/Code-Hex/go-infinity-channel v1.0.0/go.mod h1:5yUVg/Fqao9dAjcpzoQ33WwfdMWmISOrQloDRn3bsvY= -github.com/Code-Hex/vz/v3 v3.7.1 h1:EN1yNiyrbPq+dl388nne2NySo8I94EnPppvqypA65XM= -github.com/Code-Hex/vz/v3 v3.7.1/go.mod h1:1LsW0jqW0r0cQ+IeR4hHbjdqOtSidNCVMWhStMHGho8= -github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= -github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= -github.com/Microsoft/hcsshim v0.14.1 h1:CMuB3fqQVfPdhyXhUqYdUmPUIOhJkmghCx3dJet8Cqs= -github.com/Microsoft/hcsshim v0.14.1/go.mod h1:VnzvPLyWUhxiPVsJ31P6XadxCcTogTguBFDy/1GR/OM= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= -github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= -github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= -github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY= -github.com/a8m/envsubst v1.4.3/go.mod h1:4jjHWQlZoaXPoLQUb7H2qT4iLkZDdmEQiOUogdUmqVU= -github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= -github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= -github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U= -github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI= -github.com/alecthomas/repr v0.5.4 h1:OVP7JEcuzU9CCDsT6STCr3rg17oQfWILtPWd2EG0uN4= -github.com/alecthomas/repr v0.5.4/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs= -github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= -github.com/apparentlymart/go-cidr v1.1.1 h1:oEEk8CE0HP0YpHxsegk/TaOtR2FLHdWv4p3eM4ceUwg= -github.com/apparentlymart/go-cidr v1.1.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= -github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= -github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= -github.com/apparentlymart/go-textseg/v17 v17.0.1 h1:bpMXRgQ5cEoRNuQke1a80/Nl6w3G5eoIbWo9f3gXkAs= -github.com/apparentlymart/go-textseg/v17 v17.0.1/go.mod h1:fa8X4jgGeevslICIY6LcdjkSecWnXmYd9Lk34z/VxZs= -github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e h1:IdMhFPEfTZQU971tIHx3UhY4l+yCeynprnINrDTSrOc= -github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e/go.mod h1:aXGMJsd3XrnUFTuyf/pTGg5jG6CY8JMZ5juywvShjgQ= -github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q= -github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= -github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cheggaaa/pb/v3 v3.2.1 h1:aprZbFRG+B7+ug76S8QZ6Y1PW168UHzOmbC3wa+aU6I= -github.com/cheggaaa/pb/v3 v3.2.1/go.mod h1:U9hSVxoKqJrZIE3PkFG1xXXNaK/Ilzg+EF/scpXzEdw= -github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY= -github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= -github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ= -github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= -github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg= -github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ= -github.com/containerd/typeurl/v2 v2.3.0/go.mod h1:Qk+PAdUYArVj41TnGi6rJ+48RF0PkcTc4i/taoBcK0w= -github.com/containers/gvisor-tap-vsock v0.8.9 h1:6b7pqxFcKJ0EycBt1V4zPo3FQtgLLgs50AYkbFIb9eU= -github.com/containers/gvisor-tap-vsock v0.8.9/go.mod h1:OfqLraPkar5xMQcGbl9czDDSM6/xelt0HJpyB3es6v0= -github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= -github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= -github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= -github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e h1:SCnqm8SjSa0QqRxXbo5YY//S+OryeJioe17nK+iDZpg= -github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e/go.mod h1:o129ljs6alsIQTc8d6eweihqpmmrbxZ2g1jhgjhPykI= -github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7 h1:3OVJAbR131SnAXao7c9w8bFlAGH0oa29DCwsa88MJGk= -github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7/go.mod h1:K4+o74YGNjOb9N6yyG+LPj1NjHtk+Qz0IYQPvirbaLs= -github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= -github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= -github.com/diskfs/go-diskfs v1.9.4 h1:0j2d7eG4IjyxL6+ChWbDPocdBCF6HQ4HBWU2WDYWVnc= -github.com/diskfs/go-diskfs v1.9.4/go.mod h1:TePJORO83Adh5pb2SqsxAwaP0fofFxKLkxctiS/9OQc= -github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= -github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/elliotchance/orderedmap v1.8.0 h1:TrOREecvh3JbS+NCgwposXG5ZTFHtEsQiCGOhPElnMw= -github.com/elliotchance/orderedmap v1.8.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= -github.com/elliotwutingfeng/asciiset v0.0.0-20260129054604-cfde2086bc57 h1:x5yxNrq8XffV/OoNUeFPM6hxHVi5OTspSTBxr/9pemg= -github.com/elliotwutingfeng/asciiset v0.0.0-20260129054604-cfde2086bc57/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= -github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= -github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= -github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= -github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= -github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= -github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= -github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= -github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= -github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= -github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= -github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= -github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= -github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= -github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/yamlfmt v0.21.0 h1:9FKApQkDpMKgBjwLFytBHUCgqnQgxaQnci0uiESfbzs= -github.com/google/yamlfmt v0.21.0/go.mod h1:q6FYExB+Ueu7jZDjKECJk+EaeDXJzJ6Ne0dxx69GWfI= -github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= -github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 h1:jaqViOFFlZtkAwqvwZN+id37fosQqR5l3Oki9Dk4hz8= -github.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048/go.mod h1:Di7LXRyUcnvAcLicFhtM9/MlZl/TNgRSDHORM2c6CMI= -github.com/insomniacslk/dhcp v0.0.0-20240710054256-ddd8a41251c9 h1:LZJWucZz7ztCqY6Jsu7N9g124iJ2kt/O62j3+UchZFg= -github.com/insomniacslk/dhcp v0.0.0-20240710054256-ddd8a41251c9/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic= -github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg= -github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I= -github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= -github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= -github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= -github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= -github.com/jsimonetti/rtnetlink v1.3.5 h1:hVlNQNRlLDGZz31gBPicsG7Q53rnlsz1l1Ix/9XlpVA= -github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= -github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lima-vm/go-qcow2reader v0.8.0 h1:9XiPUbOt+mBjwwDZ1B7E6V8bHNjYs0/k6qQw21/jOtE= -github.com/lima-vm/go-qcow2reader v0.8.0/go.mod h1:Ai9fcmE2dXF6YFomrSCttveOT1x3+x5eG7AfIUUnSqw= -github.com/lima-vm/sshocker v0.3.11 h1:uM73Uduj1Djhvef8HGfhZShESz3BPGBQkSWNMRAJYiI= -github.com/lima-vm/sshocker v0.3.11/go.mod h1:yb+YBCJBy+oa3J/Sni5XID0y11o+QC5CbFbrC1dt0G0= -github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 h1:DZMFueDbfz6PNc1GwDRA8+6lBx1TB9UnxDQliCqR73Y= -github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2/go.mod h1:SWzULI85WerrFt3u+nIm5F9l7EvxZTKQvd0InF3nmgM= -github.com/magiconair/properties v1.18.11 h1:j5ozYZl0zCjG7ahMDH0GWIobOvvUzT0BdAguG0ViKy0= -github.com/magiconair/properties v1.18.11/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= -github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= -github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= -github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0= -github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= -github.com/mattn/go-shellwords v1.0.15 h1:rx0n8+ZdM9JWZMlr2BMPAjtLU0rfluLNtwMC2FJOTtY= -github.com/mattn/go-shellwords v1.0.15/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI= -github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA= -github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= -github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= -github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU= -github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18= -github.com/mdlayher/vsock v1.3.0 h1:bqQfZ1OznI03y6YiXp2sze05RVdzLn/zsfjnjd4+ivI= -github.com/mdlayher/vsock v1.3.0/go.mod h1:WsuksavOvwCnV5UqGHUkvAvCy+Dqy81y4goKQTzxxNY= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= -github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= -github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ= -github.com/mikefarah/yq/v4 v4.53.6 h1:5vB15LTZRvrF6z/metQF31bmF63SjDzCKKX8o8UemQQ= -github.com/mikefarah/yq/v4 v4.53.6/go.mod h1:3tmDcCSTOTUFa5Z1X8R4j0H1kauIwNo2wjKq9sLyAh4= -github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modelcontextprotocol/go-sdk v1.8.0 h1:KIvahhYqwtbeniWVPs3TcXEA7b8jEtwfBpOTAI+Urx4= -github.com/modelcontextprotocol/go-sdk v1.8.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= -github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= -github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY= -github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ= -github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= -github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= -github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= -github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= -github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE= -github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0= -github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM= -github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= -github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= -github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= -github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= -github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= -github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= -github.com/sethvargo/go-password v0.4.0 h1:eSidVKQw5C7CmTDAtH3RipBTSjdU1ZRxQaynD2GWLVU= -github.com/sethvargo/go-password v0.4.0/go.mod h1:PO3nYHwUpcHPR0F9woy7a4abZPvzRuqJr0GaeIYTm3k= -github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= -github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= -github.com/spakin/netpbm v1.3.2 h1:ZAb16Sw/+b4QeO9NokEvejVvbrYdF6DdcYJe0dKONL0= -github.com/spakin/netpbm v1.3.2/go.mod h1:cVep9uXARFgAu2UU+0c+OE1J+eKmDN2hW0EN+tazkTA= -github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= -github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= -github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= -github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= -github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= -github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= -github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA= -github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8= -github.com/zclconf/go-cty v1.19.0 h1:IV8WdqYZc2c5rLX9bEoLNXKojBAp0MZPBHMIrCoa/s4= -github.com/zclconf/go-cty v1.19.0/go.mod h1:12W89jGn3JCOIQi7infWr9m80rOkb5RNYJqXMZcN4c8= -github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= -github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= -go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= -go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ= -golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= -golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= -golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= -golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= -golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= -golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= -golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= -gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= -google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= -google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE= -gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= -gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f h1:O2w2DymsOlM/nv2pLNWCMCYOldgBBMkD7H0/prN5W2k= -gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/third_party/lima/pkg/autostart/autostart.go b/third_party/lima/pkg/autostart/autostart.go index bd77b77e..74d9f7da 100644 --- a/third_party/lima/pkg/autostart/autostart.go +++ b/third_party/lima/pkg/autostart/autostart.go @@ -8,7 +8,7 @@ import ( "context" "sync" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) // IsRegistered checks if the instance is registered to start at login. diff --git a/third_party/lima/pkg/autostart/autostart_test.go b/third_party/lima/pkg/autostart/autostart_test.go index 98ca2836..74c94736 100644 --- a/third_party/lima/pkg/autostart/autostart_test.go +++ b/third_party/lima/pkg/autostart/autostart_test.go @@ -9,8 +9,8 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/autostart/launchd" - "github.com/lima-vm/lima/v2/pkg/autostart/systemd" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart/launchd" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart/systemd" ) var ( diff --git a/third_party/lima/pkg/autostart/launchd/launchd.go b/third_party/lima/pkg/autostart/launchd/launchd.go index 7bb7fa10..9eb136b1 100644 --- a/third_party/lima/pkg/autostart/launchd/launchd.go +++ b/third_party/lima/pkg/autostart/launchd/launchd.go @@ -13,7 +13,7 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) //go:embed io.lima-vm.autostart.INSTANCE.plist diff --git a/third_party/lima/pkg/autostart/managers.go b/third_party/lima/pkg/autostart/managers.go index b8ea827d..ba4cbc11 100644 --- a/third_party/lima/pkg/autostart/managers.go +++ b/third_party/lima/pkg/autostart/managers.go @@ -13,9 +13,9 @@ import ( "path/filepath" "runtime" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/textutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/textutil" ) type notSupportedManager struct{} diff --git a/third_party/lima/pkg/autostart/managers_darwin.go b/third_party/lima/pkg/autostart/managers_darwin.go index 18fe5bbf..9bd0e579 100644 --- a/third_party/lima/pkg/autostart/managers_darwin.go +++ b/third_party/lima/pkg/autostart/managers_darwin.go @@ -3,7 +3,7 @@ package autostart -import "github.com/lima-vm/lima/v2/pkg/autostart/launchd" +import "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart/launchd" // Manager returns the autostart manager for Darwin. func Manager() autoStartManager { diff --git a/third_party/lima/pkg/autostart/managers_linux.go b/third_party/lima/pkg/autostart/managers_linux.go index e8309688..749684fe 100644 --- a/third_party/lima/pkg/autostart/managers_linux.go +++ b/third_party/lima/pkg/autostart/managers_linux.go @@ -3,7 +3,7 @@ package autostart -import "github.com/lima-vm/lima/v2/pkg/autostart/systemd" +import "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart/systemd" // DaemonManager is not supported on Linux; use systemd user services instead. func DaemonManager(_ string) autoStartManager { diff --git a/third_party/lima/pkg/autostart/systemd/systemd.go b/third_party/lima/pkg/autostart/systemd/systemd.go index 8c78c5ab..76871f1b 100644 --- a/third_party/lima/pkg/autostart/systemd/systemd.go +++ b/third_party/lima/pkg/autostart/systemd/systemd.go @@ -13,7 +13,7 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) //go:embed lima-vm@INSTANCE.service diff --git a/third_party/lima/pkg/cacheutil/cacheutil.go b/third_party/lima/pkg/cacheutil/cacheutil.go index 0771b526..8d343b61 100644 --- a/third_party/lima/pkg/cacheutil/cacheutil.go +++ b/third_party/lima/pkg/cacheutil/cacheutil.go @@ -8,9 +8,9 @@ import ( "fmt" "path" - "github.com/lima-vm/lima/v2/pkg/downloader" - "github.com/lima-vm/lima/v2/pkg/fileutils" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/downloader" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/fileutils" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) // NerdctlArchive returns the basename of the archive. diff --git a/third_party/lima/pkg/cidata/cidata.go b/third_party/lima/pkg/cidata/cidata.go index 1254443b..705cc31b 100644 --- a/third_party/lima/pkg/cidata/cidata.go +++ b/third_party/lima/pkg/cidata/cidata.go @@ -23,19 +23,19 @@ import ( "github.com/docker/go-units" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/debugutil" - "github.com/lima-vm/lima/v2/pkg/driver" - "github.com/lima-vm/lima/v2/pkg/instance/hostname" - "github.com/lima-vm/lima/v2/pkg/iso9660util" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/localpathutil" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/networks/usernet" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/sshutil" - "github.com/lima-vm/lima/v2/pkg/strutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/debugutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance/hostname" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/iso9660util" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/localpathutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/usernet" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/sshutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/strutil" ) var netLookupIP = func(host string) []net.IP { diff --git a/third_party/lima/pkg/cidata/cidata_test.go b/third_party/lima/pkg/cidata/cidata_test.go index 293a8494..98ee11ff 100644 --- a/third_party/lima/pkg/cidata/cidata_test.go +++ b/third_party/lima/pkg/cidata/cidata_test.go @@ -13,9 +13,9 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/iso9660util" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/iso9660util" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" ) func fakeLookupIP(_ string) []net.IP { diff --git a/third_party/lima/pkg/cidata/fuzz_test.go b/third_party/lima/pkg/cidata/fuzz_test.go index 256680c0..9addb9a4 100644 --- a/third_party/lima/pkg/cidata/fuzz_test.go +++ b/third_party/lima/pkg/cidata/fuzz_test.go @@ -6,7 +6,7 @@ package cidata import ( "testing" - "github.com/lima-vm/lima/v2/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" ) func FuzzSetupEnv(f *testing.F) { diff --git a/third_party/lima/pkg/cidata/template.go b/third_party/lima/pkg/cidata/template.go index 2cfcdb7b..e442221a 100644 --- a/third_party/lima/pkg/cidata/template.go +++ b/third_party/lima/pkg/cidata/template.go @@ -15,11 +15,11 @@ import ( "github.com/sethvargo/go-password/password" - "github.com/lima-vm/lima/v2/pkg/identifiers" - "github.com/lima-vm/lima/v2/pkg/iso9660util" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/textutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/identifiers" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/iso9660util" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/textutil" ) //go:embed cidata.TEMPLATE.d diff --git a/third_party/lima/pkg/cidata/template_test.go b/third_party/lima/pkg/cidata/template_test.go index c7d5fb94..e7c392aa 100644 --- a/third_party/lima/pkg/cidata/template_test.go +++ b/third_party/lima/pkg/cidata/template_test.go @@ -11,7 +11,7 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) var defaultRemoveDefaults = false diff --git a/third_party/lima/pkg/copytool/copytool.go b/third_party/lima/pkg/copytool/copytool.go index dba4b171..64b44e55 100644 --- a/third_party/lima/pkg/copytool/copytool.go +++ b/third_party/lima/pkg/copytool/copytool.go @@ -15,9 +15,9 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/fsutil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/fsutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) type Backend string diff --git a/third_party/lima/pkg/copytool/rsync.go b/third_party/lima/pkg/copytool/rsync.go index a73680ec..44294ce7 100644 --- a/third_party/lima/pkg/copytool/rsync.go +++ b/third_party/lima/pkg/copytool/rsync.go @@ -15,8 +15,8 @@ import ( "al.essio.dev/pkg/shellescape" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/sshutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/sshutil" ) type rsyncTool struct { diff --git a/third_party/lima/pkg/copytool/scp.go b/third_party/lima/pkg/copytool/scp.go index db5a6324..d6c14480 100644 --- a/third_party/lima/pkg/copytool/scp.go +++ b/third_party/lima/pkg/copytool/scp.go @@ -11,8 +11,8 @@ import ( "github.com/coreos/go-semver/semver" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/sshutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/sshutil" ) type scpTool struct { diff --git a/third_party/lima/pkg/downloader/downloader.go b/third_party/lima/pkg/downloader/downloader.go index 4331add9..4eb9444f 100644 --- a/third_party/lima/pkg/downloader/downloader.go +++ b/third_party/lima/pkg/downloader/downloader.go @@ -27,13 +27,13 @@ import ( "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/httpclientutil" - "github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil" - "github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil" - "github.com/lima-vm/lima/v2/pkg/iso9660util" - "github.com/lima-vm/lima/v2/pkg/localpathutil" - "github.com/lima-vm/lima/v2/pkg/lockutil" - "github.com/lima-vm/lima/v2/pkg/progressbar" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/httpclientutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/nativeimgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/proxyimgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/iso9660util" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/localpathutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/lockutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/progressbar" ) // HideProgress is used only for testing. diff --git a/third_party/lima/pkg/driver/driver.go b/third_party/lima/pkg/driver/driver.go index 7f901965..7987b787 100644 --- a/third_party/lima/pkg/driver/driver.go +++ b/third_party/lima/pkg/driver/driver.go @@ -8,8 +8,8 @@ import ( "errors" "net" - "github.com/lima-vm/lima/v2/pkg/hostagent/events" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/events" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) // ErrDriverNotScreenshotter is returned when the active driver does not implement Screenshotter. diff --git a/third_party/lima/pkg/driver/external/client/client.go b/third_party/lima/pkg/driver/external/client/client.go index 79cadd68..c88ed5b5 100644 --- a/third_party/lima/pkg/driver/external/client/client.go +++ b/third_party/lima/pkg/driver/external/client/client.go @@ -11,7 +11,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - pb "github.com/lima-vm/lima/v2/pkg/driver/external" + pb "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external" ) type DriverClient struct { diff --git a/third_party/lima/pkg/driver/external/client/methods.go b/third_party/lima/pkg/driver/external/client/methods.go index f99547df..86bb5039 100644 --- a/third_party/lima/pkg/driver/external/client/methods.go +++ b/third_party/lima/pkg/driver/external/client/methods.go @@ -12,9 +12,9 @@ import ( "google.golang.org/protobuf/types/known/emptypb" - "github.com/lima-vm/lima/v2/pkg/driver" - pb "github.com/lima-vm/lima/v2/pkg/driver/external" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + pb "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) func (d *DriverClient) Validate(ctx context.Context) error { diff --git a/third_party/lima/pkg/driver/external/server/methods.go b/third_party/lima/pkg/driver/external/server/methods.go index 093a5350..f94617ba 100644 --- a/third_party/lima/pkg/driver/external/server/methods.go +++ b/third_party/lima/pkg/driver/external/server/methods.go @@ -14,10 +14,10 @@ import ( "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" - "github.com/lima-vm/lima/v2/pkg/bicopy" - pb "github.com/lima-vm/lima/v2/pkg/driver/external" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/bicopy" + pb "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) func (s *DriverServer) Start(_ *emptypb.Empty, stream pb.Driver_StartServer) error { diff --git a/third_party/lima/pkg/driver/external/server/server.go b/third_party/lima/pkg/driver/external/server/server.go index b27a8a09..a8d10454 100644 --- a/third_party/lima/pkg/driver/external/server/server.go +++ b/third_party/lima/pkg/driver/external/server/server.go @@ -23,14 +23,14 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/keepalive" - "github.com/lima-vm/lima/v2/pkg/driver" - pb "github.com/lima-vm/lima/v2/pkg/driver/external" - "github.com/lima-vm/lima/v2/pkg/driver/external/client" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/registry" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + pb "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/client" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/registry" ) type DriverServer struct { diff --git a/third_party/lima/pkg/driver/hcs/hcs_driver_windows.go b/third_party/lima/pkg/driver/hcs/hcs_driver_windows.go index b8e3aba0..e6a8f765 100644 --- a/third_party/lima/pkg/driver/hcs/hcs_driver_windows.go +++ b/third_party/lima/pkg/driver/hcs/hcs_driver_windows.go @@ -18,10 +18,10 @@ import ( "github.com/lima-vm/go-qcow2reader/image/vhdx" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/driver" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/reflectutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/reflectutil" ) var knownYamlProperties = []string{ diff --git a/third_party/lima/pkg/driver/hcs/hcs_driver_windows_test.go b/third_party/lima/pkg/driver/hcs/hcs_driver_windows_test.go index f76f26c5..23b729e2 100644 --- a/third_party/lima/pkg/driver/hcs/hcs_driver_windows_test.go +++ b/third_party/lima/pkg/driver/hcs/hcs_driver_windows_test.go @@ -13,8 +13,8 @@ import ( "github.com/lima-vm/go-qcow2reader/image/vhdx" "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) func newTestInstance(t *testing.T, cfg *limatype.LimaYAML) *limatype.Instance { diff --git a/third_party/lima/pkg/driver/hcs/helper_windows.go b/third_party/lima/pkg/driver/hcs/helper_windows.go index 3b459ece..51ec5685 100644 --- a/third_party/lima/pkg/driver/hcs/helper_windows.go +++ b/third_party/lima/pkg/driver/hcs/helper_windows.go @@ -22,9 +22,9 @@ import ( "github.com/lima-vm/go-qcow2reader/image/vhdx" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/nativeimgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" ) const ( diff --git a/third_party/lima/pkg/driver/hcs/register.go b/third_party/lima/pkg/driver/hcs/register.go index ea9d104b..a213be21 100644 --- a/third_party/lima/pkg/driver/hcs/register.go +++ b/third_party/lima/pkg/driver/hcs/register.go @@ -8,7 +8,7 @@ package hcs import ( "context" - "github.com/lima-vm/lima/v2/pkg/registry" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/registry" ) func init() { diff --git a/third_party/lima/pkg/driver/krunkit/krunkit_darwin_arm64.go b/third_party/lima/pkg/driver/krunkit/krunkit_darwin_arm64.go index eb77f676..99cb851b 100644 --- a/third_party/lima/pkg/driver/krunkit/krunkit_darwin_arm64.go +++ b/third_party/lima/pkg/driver/krunkit/krunkit_darwin_arm64.go @@ -18,14 +18,14 @@ import ( "github.com/lima-vm/go-qcow2reader/image/raw" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/driver/vz" - "github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/networks/usernet" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/vz" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/proxyimgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/usernet" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) const logLevelInfo = "3" diff --git a/third_party/lima/pkg/driver/krunkit/krunkit_driver_darwin_arm64.go b/third_party/lima/pkg/driver/krunkit/krunkit_driver_darwin_arm64.go index 6ab86754..b2ea262f 100644 --- a/third_party/lima/pkg/driver/krunkit/krunkit_driver_darwin_arm64.go +++ b/third_party/lima/pkg/driver/krunkit/krunkit_driver_darwin_arm64.go @@ -21,15 +21,15 @@ import ( "github.com/lima-vm/go-qcow2reader/image/raw" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/driver" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/executil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/networks/usernet" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/reflectutil" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/executil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/usernet" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/reflectutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) const ( diff --git a/third_party/lima/pkg/driver/qemu/entitlementutil/entitlementutil.go b/third_party/lima/pkg/driver/qemu/entitlementutil/entitlementutil.go index 1707c6f8..6a931322 100644 --- a/third_party/lima/pkg/driver/qemu/entitlementutil/entitlementutil.go +++ b/third_party/lima/pkg/driver/qemu/entitlementutil/entitlementutil.go @@ -14,7 +14,7 @@ import ( "github.com/mattn/go-isatty" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/uiutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/uiutil" ) // IsSigned returns an error if the binary is not signed, or the sign is invalid, diff --git a/third_party/lima/pkg/driver/qemu/qemu.go b/third_party/lima/pkg/driver/qemu/qemu.go index 1bb7bccf..3289f81a 100644 --- a/third_party/lima/pkg/driver/qemu/qemu.go +++ b/third_party/lima/pkg/driver/qemu/qemu.go @@ -28,16 +28,16 @@ import ( "github.com/mattn/go-shellwords" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/fileutils" - "github.com/lima-vm/lima/v2/pkg/iso9660util" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/networks/usernet" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/qemuimgutil" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/fileutils" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/iso9660util" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/usernet" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/qemuimgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) type Config struct { diff --git a/third_party/lima/pkg/driver/qemu/qemu_darwin_arm64.go b/third_party/lima/pkg/driver/qemu/qemu_darwin_arm64.go index 3a7bb3d8..6437acc1 100644 --- a/third_party/lima/pkg/driver/qemu/qemu_darwin_arm64.go +++ b/third_party/lima/pkg/driver/qemu/qemu_darwin_arm64.go @@ -10,7 +10,7 @@ import ( "github.com/coreos/go-semver/semver" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) // nestedVirtualizationEnabled reports whether the host can provide nested virtualization diff --git a/third_party/lima/pkg/driver/qemu/qemu_driver.go b/third_party/lima/pkg/driver/qemu/qemu_driver.go index 5a68db80..a352f294 100644 --- a/third_party/lima/pkg/driver/qemu/qemu_driver.go +++ b/third_party/lima/pkg/driver/qemu/qemu_driver.go @@ -35,17 +35,17 @@ import ( "github.com/spakin/netpbm" "golang.org/x/image/bmp" - "github.com/lima-vm/lima/v2/pkg/driver" - "github.com/lima-vm/lima/v2/pkg/driver/qemu/entitlementutil" - "github.com/lima-vm/lima/v2/pkg/executil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/networks/usernet" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/reflectutil" - "github.com/lima-vm/lima/v2/pkg/version/versionutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/qemu/entitlementutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/executil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/usernet" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/reflectutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version/versionutil" ) type LimaQemuDriver struct { diff --git a/third_party/lima/pkg/driver/qemu/qemu_linux_amd64.go b/third_party/lima/pkg/driver/qemu/qemu_linux_amd64.go index daed5abb..f16d33b7 100644 --- a/third_party/lima/pkg/driver/qemu/qemu_linux_amd64.go +++ b/third_party/lima/pkg/driver/qemu/qemu_linux_amd64.go @@ -13,7 +13,7 @@ import ( "github.com/coreos/go-semver/semver" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) // nestedVirtualizationEnabled reports whether the host can provide nested virtualization diff --git a/third_party/lima/pkg/driver/qemu/qemu_linux_arm64.go b/third_party/lima/pkg/driver/qemu/qemu_linux_arm64.go index 9f51b75a..02577f05 100644 --- a/third_party/lima/pkg/driver/qemu/qemu_linux_arm64.go +++ b/third_party/lima/pkg/driver/qemu/qemu_linux_arm64.go @@ -10,7 +10,7 @@ import ( "github.com/coreos/go-semver/semver" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) // nestedVirtualizationEnabled reports whether the host can provide nested virtualization diff --git a/third_party/lima/pkg/driver/qemu/qemu_test.go b/third_party/lima/pkg/driver/qemu/qemu_test.go index 6b21a7a0..aa187f05 100644 --- a/third_party/lima/pkg/driver/qemu/qemu_test.go +++ b/third_party/lima/pkg/driver/qemu/qemu_test.go @@ -12,9 +12,9 @@ import ( "github.com/coreos/go-semver/semver" "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) func TestArgValue(t *testing.T) { diff --git a/third_party/lima/pkg/driver/qemu/register.go b/third_party/lima/pkg/driver/qemu/register.go index ccd64fe0..ac404595 100644 --- a/third_party/lima/pkg/driver/qemu/register.go +++ b/third_party/lima/pkg/driver/qemu/register.go @@ -8,7 +8,7 @@ package qemu import ( "context" - "github.com/lima-vm/lima/v2/pkg/registry" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/registry" ) func init() { diff --git a/third_party/lima/pkg/driver/vz/register.go b/third_party/lima/pkg/driver/vz/register.go index 651830c7..abc3a7f9 100644 --- a/third_party/lima/pkg/driver/vz/register.go +++ b/third_party/lima/pkg/driver/vz/register.go @@ -8,7 +8,7 @@ package vz import ( "context" - "github.com/lima-vm/lima/v2/pkg/registry" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/registry" ) func init() { diff --git a/third_party/lima/pkg/driver/vz/rosetta_directory_share_arm64.go b/third_party/lima/pkg/driver/vz/rosetta_directory_share_arm64.go index 3b71dec7..1fa37d14 100644 --- a/third_party/lima/pkg/driver/vz/rosetta_directory_share_arm64.go +++ b/third_party/lima/pkg/driver/vz/rosetta_directory_share_arm64.go @@ -12,7 +12,7 @@ import ( "github.com/coreos/go-semver/semver" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) func createRosettaDirectoryShareConfiguration() (*vz.VirtioFileSystemDeviceConfiguration, error) { diff --git a/third_party/lima/pkg/driver/vz/screenshot_darwin.go b/third_party/lima/pkg/driver/vz/screenshot_darwin.go index 3b1c7ea6..86f2e93d 100644 --- a/third_party/lima/pkg/driver/vz/screenshot_darwin.go +++ b/third_party/lima/pkg/driver/vz/screenshot_darwin.go @@ -86,7 +86,7 @@ import ( "errors" "fmt" - "github.com/lima-vm/lima/v2/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" ) // CaptureScreenshot captures the VM display window. diff --git a/third_party/lima/pkg/driver/vz/vm_darwin.go b/third_party/lima/pkg/driver/vz/vm_darwin.go index b737325c..87ab6716 100644 --- a/third_party/lima/pkg/driver/vz/vm_darwin.go +++ b/third_party/lima/pkg/driver/vz/vm_darwin.go @@ -28,15 +28,15 @@ import ( "github.com/lima-vm/go-qcow2reader/image/raw" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/hostagent/events" - "github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/networks/usernet" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/events" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/proxyimgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/usernet" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) // diskImageCachingMode is set to DiskImageCachingModeCached so as to avoid disk corruption on ARM: diff --git a/third_party/lima/pkg/driver/vz/vm_darwin_amd64.go b/third_party/lima/pkg/driver/vz/vm_darwin_amd64.go index fdd18137..941004aa 100644 --- a/third_party/lima/pkg/driver/vz/vm_darwin_amd64.go +++ b/third_party/lima/pkg/driver/vz/vm_darwin_amd64.go @@ -10,7 +10,7 @@ import ( "github.com/Code-Hex/vz/v3" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) func getMacMachineIdentifier(_ string) (machineIdentifier, error) { diff --git a/third_party/lima/pkg/driver/vz/vm_darwin_arm64.go b/third_party/lima/pkg/driver/vz/vm_darwin_arm64.go index 0a3fc0b6..fd92559b 100644 --- a/third_party/lima/pkg/driver/vz/vm_darwin_arm64.go +++ b/third_party/lima/pkg/driver/vz/vm_darwin_arm64.go @@ -17,10 +17,10 @@ import ( "github.com/Code-Hex/vz/v3" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/progressbar" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/progressbar" ) func getMacMachineIdentifier(identifier string) (machineIdentifier, error) { diff --git a/third_party/lima/pkg/driver/vz/vz_driver_darwin.go b/third_party/lima/pkg/driver/vz/vz_driver_darwin.go index 0430da1a..b204e14f 100644 --- a/third_party/lima/pkg/driver/vz/vz_driver_darwin.go +++ b/third_party/lima/pkg/driver/vz/vz_driver_darwin.go @@ -25,18 +25,18 @@ import ( "github.com/lima-vm/go-qcow2reader/image/raw" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/driver" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/guestpatch/macos" - "github.com/lima-vm/lima/v2/pkg/hostagent/events" - "github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil" - "github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil/asifutil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/reflectutil" - "github.com/lima-vm/lima/v2/pkg/sshutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestpatch/macos" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/events" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/nativeimgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/nativeimgutil/asifutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/reflectutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/sshutil" ) var knownYamlProperties = []string{ diff --git a/third_party/lima/pkg/driver/vz/vz_driver_darwin_test.go b/third_party/lima/pkg/driver/vz/vz_driver_darwin_test.go index e7519196..e34ecdea 100644 --- a/third_party/lima/pkg/driver/vz/vz_driver_darwin_test.go +++ b/third_party/lima/pkg/driver/vz/vz_driver_darwin_test.go @@ -14,8 +14,8 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) func TestWaitForStopped(t *testing.T) { diff --git a/third_party/lima/pkg/driver/wsl2/fs.go b/third_party/lima/pkg/driver/wsl2/fs.go index 4ded42d8..2b271804 100644 --- a/third_party/lima/pkg/driver/wsl2/fs.go +++ b/third_party/lima/pkg/driver/wsl2/fs.go @@ -11,9 +11,9 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/fileutils" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/fileutils" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) // EnsureFs downloads the root fs. diff --git a/third_party/lima/pkg/driver/wsl2/register.go b/third_party/lima/pkg/driver/wsl2/register.go index c75d6c28..45b76cc8 100644 --- a/third_party/lima/pkg/driver/wsl2/register.go +++ b/third_party/lima/pkg/driver/wsl2/register.go @@ -8,7 +8,7 @@ package wsl2 import ( "context" - "github.com/lima-vm/lima/v2/pkg/registry" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/registry" ) func init() { diff --git a/third_party/lima/pkg/driver/wsl2/vm_windows.go b/third_party/lima/pkg/driver/wsl2/vm_windows.go index a9ca48c6..6fd47611 100644 --- a/third_party/lima/pkg/driver/wsl2/vm_windows.go +++ b/third_party/lima/pkg/driver/wsl2/vm_windows.go @@ -17,10 +17,10 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/executil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/textutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/executil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/textutil" ) // startVM calls WSL to start a VM. diff --git a/third_party/lima/pkg/driver/wsl2/wsl_driver_windows.go b/third_party/lima/pkg/driver/wsl2/wsl_driver_windows.go index dcfce84b..f5fd5ad3 100644 --- a/third_party/lima/pkg/driver/wsl2/wsl_driver_windows.go +++ b/third_party/lima/pkg/driver/wsl2/wsl_driver_windows.go @@ -15,11 +15,11 @@ import ( "github.com/Microsoft/go-winio/pkg/guid" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/driver" - "github.com/lima-vm/lima/v2/pkg/freeport" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/reflectutil" - "github.com/lima-vm/lima/v2/pkg/windows" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/freeport" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/reflectutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/windows" ) var knownYamlProperties = []string{ diff --git a/third_party/lima/pkg/driver/wsl2/wsl_driver_windows_test.go b/third_party/lima/pkg/driver/wsl2/wsl_driver_windows_test.go index dcd376ca..131d898f 100644 --- a/third_party/lima/pkg/driver/wsl2/wsl_driver_windows_test.go +++ b/third_party/lima/pkg/driver/wsl2/wsl_driver_windows_test.go @@ -9,7 +9,7 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) func TestValidateConfigImages(t *testing.T) { diff --git a/third_party/lima/pkg/driverutil/disk.go b/third_party/lima/pkg/driverutil/disk.go index 58151146..5a6f9644 100644 --- a/third_party/lima/pkg/driverutil/disk.go +++ b/third_party/lima/pkg/driverutil/disk.go @@ -14,11 +14,11 @@ import ( "github.com/lima-vm/go-qcow2reader/image" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil" - "github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil" - "github.com/lima-vm/lima/v2/pkg/iso9660util" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/nativeimgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/proxyimgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/iso9660util" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) // MigrateDiskLayout creates symlinks from the current filenames (disk, iso) to diff --git a/third_party/lima/pkg/driverutil/disk_test.go b/third_party/lima/pkg/driverutil/disk_test.go index 6e4c5696..f855cc93 100644 --- a/third_party/lima/pkg/driverutil/disk_test.go +++ b/third_party/lima/pkg/driverutil/disk_test.go @@ -16,9 +16,9 @@ import ( "github.com/lima-vm/go-qcow2reader/image" "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/iso9660util" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/iso9660util" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) const ( diff --git a/third_party/lima/pkg/driverutil/instance.go b/third_party/lima/pkg/driverutil/instance.go index 8823fbcb..ec50437c 100644 --- a/third_party/lima/pkg/driverutil/instance.go +++ b/third_party/lima/pkg/driverutil/instance.go @@ -9,10 +9,10 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/driver" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/registry" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/registry" ) // CreateConfiguredDriver creates a driver.ConfiguredDriver for the given instance. diff --git a/third_party/lima/pkg/driverutil/vm.go b/third_party/lima/pkg/driverutil/vm.go index 8c3c28ec..bea28360 100644 --- a/third_party/lima/pkg/driverutil/vm.go +++ b/third_party/lima/pkg/driverutil/vm.go @@ -14,8 +14,8 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/registry" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/registry" ) // ResolveVMType sets y.VMType to the appropriate default if it is nil. diff --git a/third_party/lima/pkg/editutil/editutil.go b/third_party/lima/pkg/editutil/editutil.go index a0711ce9..0690dc8b 100644 --- a/third_party/lima/pkg/editutil/editutil.go +++ b/third_party/lima/pkg/editutil/editutil.go @@ -14,9 +14,9 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/editutil/editorcmd" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/editutil/editorcmd" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) func fileWarning(filename string) string { diff --git a/third_party/lima/pkg/executil/command.go b/third_party/lima/pkg/executil/command.go index 0ee93a65..54aa6aee 100644 --- a/third_party/lima/pkg/executil/command.go +++ b/third_party/lima/pkg/executil/command.go @@ -9,7 +9,7 @@ import ( "fmt" "os/exec" - "github.com/lima-vm/lima/v2/pkg/strutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/strutil" ) type options struct { diff --git a/third_party/lima/pkg/fileutils/download.go b/third_party/lima/pkg/fileutils/download.go index d9a0f435..7f355c3c 100644 --- a/third_party/lima/pkg/fileutils/download.go +++ b/third_party/lima/pkg/fileutils/download.go @@ -11,8 +11,8 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/downloader" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/downloader" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) // ErrSkipped is returned when the downloader did not attempt to download the specified file. diff --git a/third_party/lima/pkg/freeport/freeport_windows.go b/third_party/lima/pkg/freeport/freeport_windows.go index f00d2957..95bdbc88 100644 --- a/third_party/lima/pkg/freeport/freeport_windows.go +++ b/third_party/lima/pkg/freeport/freeport_windows.go @@ -3,7 +3,7 @@ package freeport -import "github.com/lima-vm/lima/v2/pkg/windows" +import "github.com/sylvinus/agent-vm/third_party/lima/pkg/windows" func VSock() (int, error) { return windows.GetRandomFreeVSockPort(0, 2147483647) diff --git a/third_party/lima/pkg/guestagent/api/client/client.go b/third_party/lima/pkg/guestagent/api/client/client.go index a7ea79a5..b418527d 100644 --- a/third_party/lima/pkg/guestagent/api/client/client.go +++ b/third_party/lima/pkg/guestagent/api/client/client.go @@ -14,7 +14,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" ) type GuestAgentClient struct { diff --git a/third_party/lima/pkg/guestagent/api/server/server.go b/third_party/lima/pkg/guestagent/api/server/server.go index 9b4aed4d..13fefea1 100644 --- a/third_party/lima/pkg/guestagent/api/server/server.go +++ b/third_party/lima/pkg/guestagent/api/server/server.go @@ -13,10 +13,10 @@ import ( "google.golang.org/grpc/keepalive" "google.golang.org/protobuf/types/known/emptypb" - "github.com/lima-vm/lima/v2/pkg/guestagent" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" - "github.com/lima-vm/lima/v2/pkg/guestagent/timesync" - "github.com/lima-vm/lima/v2/pkg/portfwdserver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/timesync" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/portfwdserver" ) func StartServer(ctx context.Context, lis net.Listener, guest *GuestServer) error { diff --git a/third_party/lima/pkg/guestagent/fakecloudinit/fakecloudinit_darwin.go b/third_party/lima/pkg/guestagent/fakecloudinit/fakecloudinit_darwin.go index 5a0dd403..73f06a70 100644 --- a/third_party/lima/pkg/guestagent/fakecloudinit/fakecloudinit_darwin.go +++ b/third_party/lima/pkg/guestagent/fakecloudinit/fakecloudinit_darwin.go @@ -22,8 +22,8 @@ import ( "github.com/sethvargo/go-password/password" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/cidata/cloudinittypes" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/cidata/cloudinittypes" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) func Run(ctx context.Context) error { diff --git a/third_party/lima/pkg/guestagent/guestagent.go b/third_party/lima/pkg/guestagent/guestagent.go index 6b2b21b0..4cc4a908 100644 --- a/third_party/lima/pkg/guestagent/guestagent.go +++ b/third_party/lima/pkg/guestagent/guestagent.go @@ -7,7 +7,7 @@ import ( "context" "io" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" ) type Agent interface { diff --git a/third_party/lima/pkg/guestagent/guestagent_linux.go b/third_party/lima/pkg/guestagent/guestagent_linux.go index 36c372dc..6a765f4e 100644 --- a/third_party/lima/pkg/guestagent/guestagent_linux.go +++ b/third_party/lima/pkg/guestagent/guestagent_linux.go @@ -14,10 +14,10 @@ import ( "github.com/sirupsen/logrus" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" - "github.com/lima-vm/lima/v2/pkg/guestagent/kubernetesservice" - "github.com/lima-vm/lima/v2/pkg/guestagent/sockets" - "github.com/lima-vm/lima/v2/pkg/guestagent/ticker" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/kubernetesservice" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/sockets" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/ticker" ) func New(ctx context.Context, ticker ticker.Ticker, runtimeDir string) (Agent, error) { diff --git a/third_party/lima/pkg/guestpatch/macos/macos_darwin.go b/third_party/lima/pkg/guestpatch/macos/macos_darwin.go index e6a3974d..133c77c2 100644 --- a/third_party/lima/pkg/guestpatch/macos/macos_darwin.go +++ b/third_party/lima/pkg/guestpatch/macos/macos_darwin.go @@ -13,10 +13,10 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/apfs" - "github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil/asifutil" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/apfs" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/nativeimgutil/asifutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) // attachImageWithRetry retries `diskutil image attach` because the diff --git a/third_party/lima/pkg/hostagent/api/client/client.go b/third_party/lima/pkg/hostagent/api/client/client.go index e9cb93fe..3ef9ac6f 100644 --- a/third_party/lima/pkg/hostagent/api/client/client.go +++ b/third_party/lima/pkg/hostagent/api/client/client.go @@ -15,8 +15,8 @@ import ( "net/http" "net/url" - "github.com/lima-vm/lima/v2/pkg/hostagent/api" - "github.com/lima-vm/lima/v2/pkg/httpclientutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/httpclientutil" ) type HostAgentClient interface { diff --git a/third_party/lima/pkg/hostagent/api/server/server.go b/third_party/lima/pkg/hostagent/api/server/server.go index 9eece8ec..3d4fdda6 100644 --- a/third_party/lima/pkg/hostagent/api/server/server.go +++ b/third_party/lima/pkg/hostagent/api/server/server.go @@ -11,9 +11,9 @@ import ( "net/http" "strings" - "github.com/lima-vm/lima/v2/pkg/driver" - "github.com/lima-vm/lima/v2/pkg/hostagent" - "github.com/lima-vm/lima/v2/pkg/httputil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/httputil" ) type Backend struct { diff --git a/third_party/lima/pkg/hostagent/cloudinit_test.go b/third_party/lima/pkg/hostagent/cloudinit_test.go index a012584e..c9aa555f 100644 --- a/third_party/lima/pkg/hostagent/cloudinit_test.go +++ b/third_party/lima/pkg/hostagent/cloudinit_test.go @@ -12,7 +12,7 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/hostagent/events" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/events" ) func TestEmitCloudInitProgressEventStripsControlChars(t *testing.T) { diff --git a/third_party/lima/pkg/hostagent/events/watcher.go b/third_party/lima/pkg/hostagent/events/watcher.go index 695a408a..3e6787f4 100644 --- a/third_party/lima/pkg/hostagent/events/watcher.go +++ b/third_party/lima/pkg/hostagent/events/watcher.go @@ -12,7 +12,7 @@ import ( "github.com/nxadm/tail" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/logrusutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/logrusutil" ) func Watch(ctx context.Context, haStdoutPath, haStderrPath string, begin time.Time, propagateStderr bool, onEvent func(Event) bool) error { diff --git a/third_party/lima/pkg/hostagent/ga_sock_forward_test.go b/third_party/lima/pkg/hostagent/ga_sock_forward_test.go index fbd4d3c9..f2068c73 100644 --- a/third_party/lima/pkg/hostagent/ga_sock_forward_test.go +++ b/third_party/lima/pkg/hostagent/ga_sock_forward_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/lima-vm/sshocker/pkg/ssh" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/ssh" "gotest.tools/v3/assert" ) diff --git a/third_party/lima/pkg/hostagent/hostagent.go b/third_party/lima/pkg/hostagent/hostagent.go index d1e8fa70..24859be7 100644 --- a/third_party/lima/pkg/hostagent/hostagent.go +++ b/third_party/lima/pkg/hostagent/hostagent.go @@ -22,35 +22,35 @@ import ( "sync" "time" - "github.com/lima-vm/sshocker/pkg/ssh" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/ssh" "github.com/sethvargo/go-password/password" "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/lima-vm/lima/v2/pkg/autostart" - "github.com/lima-vm/lima/v2/pkg/cidata" - "github.com/lima-vm/lima/v2/pkg/driver" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/freeport" - guestagentapi "github.com/lima-vm/lima/v2/pkg/guestagent/api" - guestagentclient "github.com/lima-vm/lima/v2/pkg/guestagent/api/client" - hostagentapi "github.com/lima-vm/lima/v2/pkg/hostagent/api" - "github.com/lima-vm/lima/v2/pkg/hostagent/dns" - "github.com/lima-vm/lima/v2/pkg/hostagent/events" - "github.com/lima-vm/lima/v2/pkg/instance/hostname" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/networks/usernet" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/portfwd" - "github.com/lima-vm/lima/v2/pkg/sshutil" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/strutil" - "github.com/lima-vm/lima/v2/pkg/version/versionutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/cidata" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/freeport" + guestagentapi "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" + guestagentclient "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api/client" + hostagentapi "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/dns" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/events" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance/hostname" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/usernet" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/portfwd" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/sshutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/strutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version/versionutil" ) type HostAgent struct { diff --git a/third_party/lima/pkg/hostagent/inotify.go b/third_party/lima/pkg/hostagent/inotify.go index a80acb9a..e6a2e089 100644 --- a/third_party/lima/pkg/hostagent/inotify.go +++ b/third_party/lima/pkg/hostagent/inotify.go @@ -16,7 +16,7 @@ import ( "github.com/sirupsen/logrus" "google.golang.org/protobuf/types/known/timestamppb" - guestagentapi "github.com/lima-vm/lima/v2/pkg/guestagent/api" + guestagentapi "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" ) const CacheSize = 10000 diff --git a/third_party/lima/pkg/hostagent/mount.go b/third_party/lima/pkg/hostagent/mount.go index cb1946d4..8d868e59 100644 --- a/third_party/lima/pkg/hostagent/mount.go +++ b/third_party/lima/pkg/hostagent/mount.go @@ -11,11 +11,11 @@ import ( "path/filepath" "runtime" - "github.com/lima-vm/sshocker/pkg/reversesshfs" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/reversesshfs" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/sshutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/sshutil" ) type mount struct { diff --git a/third_party/lima/pkg/hostagent/mount_windows_test.go b/third_party/lima/pkg/hostagent/mount_windows_test.go index 56a3a231..dcc23d88 100644 --- a/third_party/lima/pkg/hostagent/mount_windows_test.go +++ b/third_party/lima/pkg/hostagent/mount_windows_test.go @@ -11,8 +11,8 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/sshutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/sshutil" ) // TestMountPathAndSftpServer: the mount path stays native whatever the diff --git a/third_party/lima/pkg/hostagent/port.go b/third_party/lima/pkg/hostagent/port.go index 1b199ff9..5320abfd 100644 --- a/third_party/lima/pkg/hostagent/port.go +++ b/third_party/lima/pkg/hostagent/port.go @@ -7,13 +7,13 @@ import ( "context" "net" - "github.com/lima-vm/sshocker/pkg/ssh" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/ssh" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" - "github.com/lima-vm/lima/v2/pkg/hostagent/events" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/events" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" ) type portForwarder struct { diff --git a/third_party/lima/pkg/hostagent/port_darwin.go b/third_party/lima/pkg/hostagent/port_darwin.go index e2a87f94..645d62a5 100644 --- a/third_party/lima/pkg/hostagent/port_darwin.go +++ b/third_party/lima/pkg/hostagent/port_darwin.go @@ -12,11 +12,11 @@ import ( "strconv" "strings" - "github.com/lima-vm/sshocker/pkg/ssh" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/ssh" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/bicopy" - "github.com/lima-vm/lima/v2/pkg/portfwd" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/bicopy" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/portfwd" ) // forwardTCP is not thread-safe. diff --git a/third_party/lima/pkg/hostagent/port_others.go b/third_party/lima/pkg/hostagent/port_others.go index ad5ced84..fbd44500 100644 --- a/third_party/lima/pkg/hostagent/port_others.go +++ b/third_party/lima/pkg/hostagent/port_others.go @@ -8,7 +8,7 @@ package hostagent import ( "context" - "github.com/lima-vm/sshocker/pkg/ssh" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/ssh" ) func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, sshAddress string, sshPort int, local, remote, verb string) error { diff --git a/third_party/lima/pkg/hostagent/port_test.go b/third_party/lima/pkg/hostagent/port_test.go index a09a4db8..a8832147 100644 --- a/third_party/lima/pkg/hostagent/port_test.go +++ b/third_party/lima/pkg/hostagent/port_test.go @@ -8,8 +8,8 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) func testPortForwarder(sshLocalPort int) *portForwarder { diff --git a/third_party/lima/pkg/hostagent/port_windows.go b/third_party/lima/pkg/hostagent/port_windows.go index 7c0d235f..4bede541 100644 --- a/third_party/lima/pkg/hostagent/port_windows.go +++ b/third_party/lima/pkg/hostagent/port_windows.go @@ -6,7 +6,7 @@ package hostagent import ( "context" - "github.com/lima-vm/sshocker/pkg/ssh" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/ssh" ) func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, sshAddress string, sshPort int, local, remote, verb string) error { diff --git a/third_party/lima/pkg/hostagent/portforward_test.go b/third_party/lima/pkg/hostagent/portforward_test.go index de699417..596f40c7 100644 --- a/third_party/lima/pkg/hostagent/portforward_test.go +++ b/third_party/lima/pkg/hostagent/portforward_test.go @@ -12,8 +12,8 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" - guestagentclient "github.com/lima-vm/lima/v2/pkg/guestagent/api/client" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" + guestagentclient "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api/client" ) // invalidIPGuestService streams one port event whose IP is not an IP address. diff --git a/third_party/lima/pkg/hostagent/requirements.go b/third_party/lima/pkg/hostagent/requirements.go index 6137f958..66da2fc6 100644 --- a/third_party/lima/pkg/hostagent/requirements.go +++ b/third_party/lima/pkg/hostagent/requirements.go @@ -14,11 +14,11 @@ import ( "strings" "time" - "github.com/lima-vm/sshocker/pkg/ssh" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/ssh" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/sshutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/sshutil" ) func (a *HostAgent) waitForRequirements(label string, requirements []requirement) error { diff --git a/third_party/lima/pkg/httpclientutil/httpclientutil.go b/third_party/lima/pkg/httpclientutil/httpclientutil.go index c7e64ee6..9cbc6ecf 100644 --- a/third_party/lima/pkg/httpclientutil/httpclientutil.go +++ b/third_party/lima/pkg/httpclientutil/httpclientutil.go @@ -15,7 +15,7 @@ import ( "net" "net/http" - "github.com/lima-vm/lima/v2/pkg/httputil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/httputil" ) // Get calls HTTP GET and verifies that the status code is 2XX . diff --git a/third_party/lima/pkg/imgutil/nativeimgutil/asifutil/asif_darwin.go b/third_party/lima/pkg/imgutil/nativeimgutil/asifutil/asif_darwin.go index af3cc7fb..0a59325b 100644 --- a/third_party/lima/pkg/imgutil/nativeimgutil/asifutil/asif_darwin.go +++ b/third_party/lima/pkg/imgutil/nativeimgutil/asifutil/asif_darwin.go @@ -14,7 +14,7 @@ import ( "strconv" "strings" - "github.com/lima-vm/lima/v2/pkg/plist" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/plist" ) // NewASIF creates a new ASIF image file at the specified path with the given size. diff --git a/third_party/lima/pkg/imgutil/nativeimgutil/nativeimgutil.go b/third_party/lima/pkg/imgutil/nativeimgutil/nativeimgutil.go index 61616d32..209e5d04 100644 --- a/third_party/lima/pkg/imgutil/nativeimgutil/nativeimgutil.go +++ b/third_party/lima/pkg/imgutil/nativeimgutil/nativeimgutil.go @@ -25,8 +25,8 @@ import ( "github.com/lima-vm/go-qcow2reader/image/raw" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil/asifutil" - "github.com/lima-vm/lima/v2/pkg/progressbar" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/nativeimgutil/asifutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/progressbar" ) // Disk image size must be aligned to sector size. Qemu block layer is rounding diff --git a/third_party/lima/pkg/imgutil/proxyimgutil/proxyimgutil.go b/third_party/lima/pkg/imgutil/proxyimgutil/proxyimgutil.go index 09ad8150..071b7ca1 100644 --- a/third_party/lima/pkg/imgutil/proxyimgutil/proxyimgutil.go +++ b/third_party/lima/pkg/imgutil/proxyimgutil/proxyimgutil.go @@ -12,9 +12,9 @@ import ( "github.com/lima-vm/go-qcow2reader/image" "github.com/lima-vm/go-qcow2reader/image/raw" - "github.com/lima-vm/lima/v2/pkg/imgutil" - "github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil" - "github.com/lima-vm/lima/v2/pkg/qemuimgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/nativeimgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/qemuimgutil" ) // ImageDiskManager is a proxy implementation of imgutil.ImageDiskManager that uses both QEMU and native image utilities. diff --git a/third_party/lima/pkg/instance/ansible.go b/third_party/lima/pkg/instance/ansible.go index f050dee1..7b2d0b67 100644 --- a/third_party/lima/pkg/instance/ansible.go +++ b/third_party/lima/pkg/instance/ansible.go @@ -13,8 +13,8 @@ import ( "github.com/goccy/go-yaml" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) func runAnsibleProvision(ctx context.Context, inst *limatype.Instance) error { diff --git a/third_party/lima/pkg/instance/clone.go b/third_party/lima/pkg/instance/clone.go index 50ec485c..e748344e 100644 --- a/third_party/lima/pkg/instance/clone.go +++ b/third_party/lima/pkg/instance/clone.go @@ -15,11 +15,11 @@ import ( continuityfs "github.com/containerd/continuity/fs" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func CloneOrRename(ctx context.Context, oldInst *limatype.Instance, newInstName string, rename bool) (*limatype.Instance, error) { @@ -53,6 +53,10 @@ func CloneOrRename(ctx context.Context, oldInst *limatype.Instance, newInstName newInstName, maxSockName, osutil.UnixPathMax, len(maxSockName)) } + // Not LimaDir's own subfolder when $LIMA_INSTANCES_DIR is set. + if err = os.MkdirAll(filepath.Dir(newInstDir), 0o700); err != nil { + return nil, err + } if err = os.Mkdir(newInstDir, 0o700); err != nil { return nil, err } diff --git a/third_party/lima/pkg/instance/create.go b/third_party/lima/pkg/instance/create.go index f66650b3..ab4a3bb8 100644 --- a/third_party/lima/pkg/instance/create.go +++ b/third_party/lima/pkg/instance/create.go @@ -10,16 +10,16 @@ import ( "os" "path/filepath" - "github.com/lima-vm/lima/v2/pkg/cidata" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/version" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/cidata" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" ) func Create(ctx context.Context, instName string, instConfig []byte, saveBrokenYAML bool) (*limatype.Instance, error) { diff --git a/third_party/lima/pkg/instance/delete.go b/third_party/lima/pkg/instance/delete.go index 2a0eb54d..abb8cccf 100644 --- a/third_party/lima/pkg/instance/delete.go +++ b/third_party/lima/pkg/instance/delete.go @@ -9,9 +9,9 @@ import ( "fmt" "os" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) func Delete(ctx context.Context, inst *limatype.Instance, force bool) error { diff --git a/third_party/lima/pkg/instance/hostname/hostname_test.go b/third_party/lima/pkg/instance/hostname/hostname_test.go index 54729e8b..64766ff5 100644 --- a/third_party/lima/pkg/instance/hostname/hostname_test.go +++ b/third_party/lima/pkg/instance/hostname/hostname_test.go @@ -8,7 +8,7 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/instance/hostname" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance/hostname" ) func TestFromInstName(t *testing.T) { diff --git a/third_party/lima/pkg/instance/restart.go b/third_party/lima/pkg/instance/restart.go index d1ff23d7..c0797107 100644 --- a/third_party/lima/pkg/instance/restart.go +++ b/third_party/lima/pkg/instance/restart.go @@ -10,9 +10,9 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/autostart" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/networks/reconcile" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/reconcile" ) const ( diff --git a/third_party/lima/pkg/instance/start.go b/third_party/lima/pkg/instance/start.go index 41caa5be..a01cb373 100644 --- a/third_party/lima/pkg/instance/start.go +++ b/third_party/lima/pkg/instance/start.go @@ -20,21 +20,21 @@ import ( "github.com/lima-vm/go-qcow2reader/image/vhdx" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/autostart" - "github.com/lima-vm/lima/v2/pkg/cacheutil" - "github.com/lima-vm/lima/v2/pkg/driver" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/executil" - "github.com/lima-vm/lima/v2/pkg/fileutils" - hostagentevents "github.com/lima-vm/lima/v2/pkg/hostagent/events" - "github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/registry" - "github.com/lima-vm/lima/v2/pkg/store" - "github.com/lima-vm/lima/v2/pkg/usrlocal" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/cacheutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/executil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/fileutils" + hostagentevents "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/events" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/imgutil/proxyimgutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/registry" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/usrlocal" ) // DefaultWatchHostAgentEventsTimeout is the duration to wait for the instance @@ -291,7 +291,9 @@ func StartWithPaths(ctx context.Context, inst *limatype.Instance, launchHostAgen args = append(args, "--progress") } args = append(args, inst.Name) - haCmd = exec.CommandContext(ctx, limactl, args...) + // Not exec.CommandContext: the hostagent outlives this call, and os/exec + // would kill it whenever ctx ends, even after Start returned. + haCmd = exec.Command(limactl, args...) haCmd.SysProcAttr = executil.BackgroundSysProcAttr diff --git a/third_party/lima/pkg/instance/start_unix.go b/third_party/lima/pkg/instance/start_unix.go index 93bf2ad2..ec2fe13c 100644 --- a/third_party/lima/pkg/instance/start_unix.go +++ b/third_party/lima/pkg/instance/start_unix.go @@ -14,7 +14,7 @@ import ( "github.com/mattn/go-isatty" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) func execHostAgentForeground(limactl string, haCmd *exec.Cmd) error { diff --git a/third_party/lima/pkg/instance/stop.go b/third_party/lima/pkg/instance/stop.go index f232b18c..0a5dc802 100644 --- a/third_party/lima/pkg/instance/stop.go +++ b/third_party/lima/pkg/instance/stop.go @@ -14,13 +14,13 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/autostart" - "github.com/lima-vm/lima/v2/pkg/driver/external/server" - hostagentevents "github.com/lima-vm/lima/v2/pkg/hostagent/events" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/autostart" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/server" + hostagentevents "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/events" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func StopGracefully(ctx context.Context, inst *limatype.Instance, isRestart bool) error { diff --git a/third_party/lima/pkg/limactlutil/limactlutil.go b/third_party/lima/pkg/limactlutil/limactlutil.go index 9c306ec8..3f303b4d 100644 --- a/third_party/lima/pkg/limactlutil/limactlutil.go +++ b/third_party/lima/pkg/limactlutil/limactlutil.go @@ -12,7 +12,7 @@ import ( "os" "os/exec" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) // Path returns the path to the `limactl` executable. diff --git a/third_party/lima/pkg/limainfo/limainfo.go b/third_party/lima/pkg/limainfo/limainfo.go index 8b135c15..0d4204e5 100644 --- a/third_party/lima/pkg/limainfo/limainfo.go +++ b/third_party/lima/pkg/limainfo/limainfo.go @@ -12,16 +12,16 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/envutil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/plugins" - "github.com/lima-vm/lima/v2/pkg/registry" - "github.com/lima-vm/lima/v2/pkg/templatestore" - "github.com/lima-vm/lima/v2/pkg/usrlocal" - "github.com/lima-vm/lima/v2/pkg/version" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/envutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/plugins" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/registry" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/templatestore" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/usrlocal" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" ) type LimaInfo struct { diff --git a/third_party/lima/pkg/limatmpl/abs.go b/third_party/lima/pkg/limatmpl/abs.go index 66f15e6d..538f2064 100644 --- a/third_party/lima/pkg/limatmpl/abs.go +++ b/third_party/lima/pkg/limatmpl/abs.go @@ -13,7 +13,7 @@ import ( "runtime" "strings" - "github.com/lima-vm/lima/v2/pkg/localpathutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/localpathutil" ) // UseAbsLocators will replace all relative template locators with absolute ones, so this template diff --git a/third_party/lima/pkg/limatmpl/embed.go b/third_party/lima/pkg/limatmpl/embed.go index 7ef2e999..7f76ea39 100644 --- a/third_party/lima/pkg/limatmpl/embed.go +++ b/third_party/lima/pkg/limatmpl/embed.go @@ -18,12 +18,12 @@ import ( "github.com/coreos/go-semver/semver" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/version/versionutil" - "github.com/lima-vm/lima/v2/pkg/yqutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version/versionutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/yqutil" ) // Embed will recursively resolve all "base" dependencies and update the diff --git a/third_party/lima/pkg/limatmpl/embed_test.go b/third_party/lima/pkg/limatmpl/embed_test.go index c9776854..e11f28db 100644 --- a/third_party/lima/pkg/limatmpl/embed_test.go +++ b/third_party/lima/pkg/limatmpl/embed_test.go @@ -14,8 +14,8 @@ import ( "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" ) type embedTestCase struct { diff --git a/third_party/lima/pkg/limatmpl/locator.go b/third_party/lima/pkg/limatmpl/locator.go index a95219c2..32dce956 100644 --- a/third_party/lima/pkg/limatmpl/locator.go +++ b/third_party/lima/pkg/limatmpl/locator.go @@ -20,11 +20,11 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/identifiers" - "github.com/lima-vm/lima/v2/pkg/ioutilx" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/plugins" - "github.com/lima-vm/lima/v2/pkg/templatestore" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/identifiers" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/ioutilx" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/plugins" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/templatestore" ) const yBytesLimit = 4 * 1024 * 1024 // 4MiB diff --git a/third_party/lima/pkg/limatmpl/locator_test.go b/third_party/lima/pkg/limatmpl/locator_test.go index ade9b084..1f5c7023 100644 --- a/third_party/lima/pkg/limatmpl/locator_test.go +++ b/third_party/lima/pkg/limatmpl/locator_test.go @@ -10,8 +10,8 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatmpl" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatmpl" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) func TestInstNameFromImageURL(t *testing.T) { diff --git a/third_party/lima/pkg/limatmpl/template.go b/third_party/lima/pkg/limatmpl/template.go index 5ac277f9..0ce278fd 100644 --- a/third_party/lima/pkg/limatmpl/template.go +++ b/third_party/lima/pkg/limatmpl/template.go @@ -6,8 +6,8 @@ package limatmpl import ( "strings" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" ) type Template struct { diff --git a/third_party/lima/pkg/limatype/dirnames/dirnames.go b/third_party/lima/pkg/limatype/dirnames/dirnames.go index 136c7661..27526923 100644 --- a/third_party/lima/pkg/limatype/dirnames/dirnames.go +++ b/third_party/lima/pkg/limatype/dirnames/dirnames.go @@ -10,8 +10,8 @@ import ( "path/filepath" "strings" - "github.com/lima-vm/lima/v2/pkg/identifiers" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/identifiers" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) // DotLima is a directory that appears under the home directory. @@ -77,12 +77,49 @@ func LimaTemplatesDir() (string, error) { return filepath.Join(limaDir, filenames.TemplatesDir), nil } +// InstancesEnv is the environment variable that moves the instances out of +// $LIMA_HOME. When it is set, the instance NAME is in +// $LIMA_INSTANCES_DIR/NAME/InstanceSubdir, and a program embedding Lima can +// keep its own files for NAME in $LIMA_INSTANCES_DIR/NAME. $LIMA_HOME keeps +// the rest (_config, _networks, _disks, _templates). +const InstancesEnv = "LIMA_INSTANCES_DIR" + +// InstanceSubdir is the instance's folder in $LIMA_INSTANCES_DIR/NAME. +const InstanceSubdir = "lima" + +// InstancesDir returns the absolute path of $LIMA_INSTANCES_DIR, or "" when +// it is not set and the instances are in LimaDir. +func InstancesDir() (string, error) { + dir := os.Getenv(InstancesEnv) + if dir == "" { + return "", nil + } + if !filepath.IsAbs(dir) { + return "", fmt.Errorf("$%s must be an absolute path, got %#q", InstancesEnv, dir) + } + if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) { + return dir, nil + } + realdir, err := filepath.EvalSymlinks(dir) + if err != nil { + return "", fmt.Errorf("cannot evaluate symlinks in %#q: %w", dir, err) + } + return realdir, nil +} + // InstanceDir returns the instance dir. // InstanceDir does not check whether the instance exists. func InstanceDir(name string) (string, error) { if err := ValidateInstName(name); err != nil { return "", err } + instances, err := InstancesDir() + if err != nil { + return "", err + } + if instances != "" { + return filepath.Join(instances, name, InstanceSubdir), nil + } limaDir, err := LimaDir() if err != nil { return "", err @@ -91,6 +128,14 @@ func InstanceDir(name string) (string, error) { return dir, nil } +// InstanceName returns the name of the instance whose dir is instDir. +func InstanceName(instDir string) string { + if os.Getenv(InstancesEnv) != "" { + return filepath.Base(filepath.Dir(instDir)) + } + return filepath.Base(instDir) +} + // ValidateInstName checks if the name is a valid instance name. For this it needs to // be a valid identifier, and not end in .yml or .yaml (case insensitively). func ValidateInstName(name string) error { diff --git a/third_party/lima/pkg/limatype/lima_instance.go b/third_party/lima/pkg/limatype/lima_instance.go index fada2f34..4474e0bf 100644 --- a/third_party/lima/pkg/limatype/lima_instance.go +++ b/third_party/lima/pkg/limatype/lima_instance.go @@ -9,7 +9,7 @@ import ( "os" "path/filepath" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) type Status = string diff --git a/third_party/lima/pkg/limayaml/defaults.go b/third_party/lima/pkg/limayaml/defaults.go index 1984575b..65c82fba 100644 --- a/third_party/lima/pkg/limayaml/defaults.go +++ b/third_party/lima/pkg/limayaml/defaults.go @@ -26,16 +26,16 @@ import ( "github.com/pbnjay/memory" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/fsutil" - "github.com/lima-vm/lima/v2/pkg/instance/hostname" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/localpathutil" - . "github.com/lima-vm/lima/v2/pkg/must" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/version" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/fsutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance/hostname" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/localpathutil" + . "github.com/sylvinus/agent-vm/third_party/lima/pkg/must" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" ) const ( @@ -955,7 +955,7 @@ func deleteNonStaticPortForwards(portForwards *[]limatype.PortForward) { func executeGuestTemplate(format, instDir string, user limatype.User, param map[string]string) (bytes.Buffer, error) { tmpl, err := template.New("").Parse(format) if err == nil { - name := filepath.Base(instDir) + name := dirnames.InstanceName(instDir) data := map[string]any{ "Name": name, "Hostname": hostname.FromInstName(name), // TODO: support customization @@ -985,7 +985,7 @@ func executeHostTemplate(format, instDir string, param map[string]string) (bytes } data := map[string]any{ "Dir": instDir, - "Name": filepath.Base(instDir), + "Name": dirnames.InstanceName(instDir), // TODO: add hostname fields for the host and the guest "UID": currentUser.Uid, "User": currentUser.Username, @@ -994,8 +994,8 @@ func executeHostTemplate(format, instDir string, param map[string]string) (bytes "GlobalTempDir": globalTempDir, "TempDir": os.TempDir(), - "Instance": filepath.Base(instDir), // DEPRECATED, use `{{.Name}}` - "LimaHome": limaHome, // DEPRECATED, use `{{.Dir}}` instead of `{{.LimaHome}}/{{.Instance}}` + "Instance": dirnames.InstanceName(instDir), // DEPRECATED, use `{{.Name}}` + "LimaHome": limaHome, // DEPRECATED, use `{{.Dir}}` instead of `{{.LimaHome}}/{{.Instance}}` } var out bytes.Buffer err = tmpl.Execute(&out, data) diff --git a/third_party/lima/pkg/limayaml/defaults_test.go b/third_party/lima/pkg/limayaml/defaults_test.go index e6666be8..9c2d4366 100644 --- a/third_party/lima/pkg/limayaml/defaults_test.go +++ b/third_party/lima/pkg/limayaml/defaults_test.go @@ -19,12 +19,12 @@ import ( "github.com/sirupsen/logrus" "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/fsutil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/version" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/fsutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" ) func TestExistingLimaVersion(t *testing.T) { diff --git a/third_party/lima/pkg/limayaml/limayaml_test.go b/third_party/lima/pkg/limayaml/limayaml_test.go index 64e70b6f..dbee6ee8 100644 --- a/third_party/lima/pkg/limayaml/limayaml_test.go +++ b/third_party/lima/pkg/limayaml/limayaml_test.go @@ -13,7 +13,7 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) func dumpJSON(t *testing.T, d any) string { diff --git a/third_party/lima/pkg/limayaml/load.go b/third_party/lima/pkg/limayaml/load.go index f10855ba..1f54d902 100644 --- a/third_party/lima/pkg/limayaml/load.go +++ b/third_party/lima/pkg/limayaml/load.go @@ -12,9 +12,9 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) // Load loads the yaml and fulfills unspecified fields with the default values. diff --git a/third_party/lima/pkg/limayaml/marshal.go b/third_party/lima/pkg/limayaml/marshal.go index 71d6d4d7..3508fc18 100644 --- a/third_party/lima/pkg/limayaml/marshal.go +++ b/third_party/lima/pkg/limayaml/marshal.go @@ -9,8 +9,8 @@ import ( "github.com/goccy/go-yaml" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/yqutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/yqutil" ) const ( diff --git a/third_party/lima/pkg/limayaml/marshal_test.go b/third_party/lima/pkg/limayaml/marshal_test.go index c92c53b6..e235b98d 100644 --- a/third_party/lima/pkg/limayaml/marshal_test.go +++ b/third_party/lima/pkg/limayaml/marshal_test.go @@ -11,7 +11,7 @@ import ( "github.com/goccy/go-yaml" "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) func dumpYAML(t *testing.T, d any) string { diff --git a/third_party/lima/pkg/limayaml/validate.go b/third_party/lima/pkg/limayaml/validate.go index 33cefd5d..8b3a0e69 100644 --- a/third_party/lima/pkg/limayaml/validate.go +++ b/third_party/lima/pkg/limayaml/validate.go @@ -21,13 +21,13 @@ import ( "github.com/docker/go-units" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/identifiers" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/localpathutil" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/version" - "github.com/lima-vm/lima/v2/pkg/version/versionutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/identifiers" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/localpathutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version/versionutil" ) func Validate(y *limatype.LimaYAML, warn bool) error { diff --git a/third_party/lima/pkg/limayaml/validate_test.go b/third_party/lima/pkg/limayaml/validate_test.go index 8040c296..efde80c0 100644 --- a/third_party/lima/pkg/limayaml/validate_test.go +++ b/third_party/lima/pkg/limayaml/validate_test.go @@ -12,8 +12,8 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/version" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version" ) func TestValidateEmpty(t *testing.T) { diff --git a/third_party/lima/pkg/mcp/msi/filesystem.go b/third_party/lima/pkg/mcp/msi/filesystem.go deleted file mode 100644 index ba4e6dfb..00000000 --- a/third_party/lima/pkg/mcp/msi/filesystem.go +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Lima Authors -// SPDX-License-Identifier: Apache-2.0 - -// Portion of AI prompt texts from: -// - https://github.com/google-gemini/gemini-cli/blob/v0.1.12/docs/tools/file-system.md -// -// SPDX-FileCopyrightText: Copyright 2025 Google LLC - -package msi - -import ( - "io/fs" - "time" - - "github.com/modelcontextprotocol/go-sdk/mcp" -) - -var ListDirectory = &mcp.Tool{ - Name: "list_directory", - Description: `Lists the names of files and subdirectories directly within a specified directory path.`, -} - -type ListDirectoryParams struct { - Path string `json:"path" jsonschema:"The absolute path to the directory to list."` -} - -// ListDirectoryResultEntry is similar to [io/fs.FileInfo]. -type ListDirectoryResultEntry struct { - Name string `json:"name" jsonschema:"base name of the file"` - Size *int64 `json:"size,omitempty" jsonschema:"length in bytes for regular files; system-dependent for others"` - Mode *fs.FileMode `json:"mode,omitempty" jsonschema:"file mode bits"` - ModTime *time.Time `json:"time,omitempty" jsonschema:"modification time"` - IsDir *bool `json:"is_dir,omitempty" jsonschema:"true for a directory"` -} - -type ListDirectoryResult struct { - Entries []ListDirectoryResultEntry `json:"entries" jsonschema:"The directory content entries."` -} - -var ReadFile = &mcp.Tool{ - Name: "read_file", - Description: `Reads and returns the content of a specified file.`, -} - -type ReadFileResult struct { - Content string `json:"content" jsonschema:"The content of the file."` -} - -type ReadFileParams struct { - Path string `json:"path" jsonschema:"The absolute path to the file to read."` - // TODO: Offset *int `json:"offset,omitempty" jsonschema:"For text files, the 0-based line number to start reading from. Requires limit to be set."` - // TODO: Limit *int `json:"limit,omitempty" jsonschema:"For text files, the maximum number of lines to read. If omitted, reads a default maximum (e.g., 2000 lines) or the entire file if feasible."` -} - -var WriteFile = &mcp.Tool{ - Name: "write_file", - Description: `Writes content to a specified file. If the file exists, it will be overwritten. If the file doesn't exist, it (and any necessary parent directories) will be created.`, -} - -type WriteFileResult struct { - // Empty for now -} - -type WriteFileParams struct { - Path string `json:"path" jsonschema:"The absolute path to the file to write to."` - Content string `json:"content" jsonschema:"The content to write into the file."` -} - -var Glob = &mcp.Tool{ - Name: "glob", - Description: `Finds files matching specific glob patterns (e.g., src/**/*.ts, *.md)`, // Not sorted by mod time, unlike Gemini -} - -type GlobParams struct { - Pattern string `json:"pattern" jsonschema:"The glob pattern to match against (e.g., '*.py', 'src/**/*.js')."` - Path *string `json:"path,omitempty" jsonschema:"The absolute path to the directory to search within. If omitted, searches the tool's root directory."` - // TODO: CaseSensitive bool `json:"case_sensitive,omitempty" jsonschema:": Whether the search should be case-sensitive. Defaults to false."` -} - -type GlobResult struct { - Matches []string `json:"matches" jsonschema:"A list of absolute file paths that match the provided glob pattern."` -} - -var SearchFileContent = &mcp.Tool{ - Name: "search_file_content", - Description: `Searches for a regular expression pattern within the content of files in a specified directory. Internally calls 'git grep -n --no-index'.`, -} - -type SearchFileContentParams struct { - Pattern string `json:"pattern" jsonschema:"The regular expression (regex) to search for (e.g., 'function\\s+myFunction')."` - Path *string `json:"path,omitempty" jsonschema:"The absolute path to the directory to search within. Defaults to the current working directory."` - Include *string `json:"include,omitempty" jsonschema:"A glob pattern to filter which files are searched (e.g., '*.js', 'src/**/*.{ts,tsx}'). If omitted, searches most files (respecting common ignores)."` -} - -type SearchFileContentResult struct { - GitGrepOutput string `json:"git_grep_output" jsonschema:"The raw output from the 'git grep -n --no-index' command, containing matching lines with filenames and line numbers."` -} - -// TODO: implement Replace diff --git a/third_party/lima/pkg/mcp/msi/msi.go b/third_party/lima/pkg/mcp/msi/msi.go deleted file mode 100644 index fb59ac42..00000000 --- a/third_party/lima/pkg/mcp/msi/msi.go +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Lima Authors -// SPDX-License-Identifier: Apache-2.0 - -// Package msi provides the "MCP Sandbox Interface" (tentative) -// that should be reusable for other projects too. -// -// MCP Sandbox Interface defines MCP (Model Context Protocol) tools -// that can be used for reading, writing, and executing local files -// with an appropriate sandboxing technology. The sandboxing technology -// can be more secure and/or efficient than the default tools provided -// by an AI agent. -// -// MCP Sandbox Interface was inspired by Gemini CLI's built-in tools. -// https://github.com/google-gemini/gemini-cli/tree/v0.1.12/docs/tools -// -// Notable differences from Gemini CLI's built-in tools: -// - the output format is JSON, not a plain text -// - the output of [SearchFileContent] always corresponds to `git grep -n --no-index` -// - [RunShellCommandParams].Command is a string slice, not a string -// - [RunShellCommandParams].Directory is an absolute path, not a relative path -// - [RunShellCommandParams].Directory must not be empty -// -// Eventually, this package may be split to a separate repository. -package msi diff --git a/third_party/lima/pkg/mcp/msi/shell.go b/third_party/lima/pkg/mcp/msi/shell.go deleted file mode 100644 index e53ed1c8..00000000 --- a/third_party/lima/pkg/mcp/msi/shell.go +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Lima Authors -// SPDX-License-Identifier: Apache-2.0 - -// Portion of AI prompt texts from: -// - https://github.com/google-gemini/gemini-cli/blob/v0.1.12/docs/tools/shell.md -// -// SPDX-FileCopyrightText: Copyright 2025 Google LLC - -package msi - -import "github.com/modelcontextprotocol/go-sdk/mcp" - -var RunShellCommand = &mcp.Tool{ - Name: "run_shell_command", - Description: `Executes a given shell command.`, -} - -type RunShellCommandParams struct { - Command []string `json:"command" jsonschema:"The exact shell command to execute. Defined as a string slice, unlike Gemini's run_shell_command that defines it as a single string."` - Description string `json:"description,omitempty" jsonschema:"A brief description of the command's purpose, which will be potentially shown to the user."` - Directory string `json:"directory" jsonschema:"The absolute directory in which to execute the command. Unlike Gemini's run_shell_command, this must not be a relative path, and must not be empty."` -} - -type RunShellCommandResult struct { - Stdout string `json:"stdout" jsonschema:"Output from the standard output stream."` - Stderr string `json:"stderr" jsonschema:"Output from the standard error stream."` - Error string `json:"error,omitempty" jsonschema:"Any error message reported by the subprocess."` - ExitCode *int `json:"exit_code,omitempty" jsonschema:"Exit code of the command."` -} diff --git a/third_party/lima/pkg/mcp/toolset/filesystem.go b/third_party/lima/pkg/mcp/toolset/filesystem.go deleted file mode 100644 index bf86d1b2..00000000 --- a/third_party/lima/pkg/mcp/toolset/filesystem.go +++ /dev/null @@ -1,183 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Lima Authors -// SPDX-License-Identifier: Apache-2.0 - -package toolset - -import ( - "context" - "errors" - "io" - "os" - "path" - "path/filepath" - - "github.com/modelcontextprotocol/go-sdk/mcp" - - "github.com/lima-vm/lima/v2/pkg/mcp/msi" -) - -func (ts *ToolSet) ListDirectory(ctx context.Context, - _ *mcp.CallToolRequest, args msi.ListDirectoryParams, -) (*mcp.CallToolResult, *msi.ListDirectoryResult, error) { - if ts.inst == nil { - return nil, nil, errors.New("instance not registered") - } - guestPath, err := ts.TranslateHostPath(args.Path) - if err != nil { - return nil, nil, err - } - guestEnts, err := ts.sftp.ReadDirContext(ctx, guestPath) - if err != nil { - return nil, nil, err - } - res := &msi.ListDirectoryResult{ - Entries: make([]msi.ListDirectoryResultEntry, len(guestEnts)), - } - for i, f := range guestEnts { - res.Entries[i].Name = f.Name() - res.Entries[i].Size = new(f.Size()) - res.Entries[i].Mode = new(f.Mode()) - res.Entries[i].ModTime = new(f.ModTime()) - res.Entries[i].IsDir = new(f.IsDir()) - } - return &mcp.CallToolResult{ - StructuredContent: res, - }, res, nil -} - -func (ts *ToolSet) ReadFile(_ context.Context, - _ *mcp.CallToolRequest, args msi.ReadFileParams, -) (*mcp.CallToolResult, *msi.ReadFileResult, error) { - if ts.inst == nil { - return nil, nil, errors.New("instance not registered") - } - guestPath, err := ts.TranslateHostPath(args.Path) - if err != nil { - return nil, nil, err - } - f, err := ts.sftp.Open(guestPath) - if err != nil { - return nil, nil, err - } - defer f.Close() - const limitBytes = 32 * 1024 * 1024 - lr := io.LimitReader(f, limitBytes) - b, err := io.ReadAll(lr) - if err != nil { - return nil, nil, err - } - res := &msi.ReadFileResult{ - Content: string(b), - } - return &mcp.CallToolResult{ - // Gemini: - // For text files: The file content, potentially prefixed with a truncation message - // (e.g., [File content truncated: showing lines 1-100 of 500 total lines...]\nActual file content...). - StructuredContent: res, - }, res, nil -} - -func (ts *ToolSet) WriteFile(_ context.Context, - _ *mcp.CallToolRequest, args msi.WriteFileParams, -) (*mcp.CallToolResult, *msi.WriteFileResult, error) { - if ts.inst == nil { - return nil, nil, errors.New("instance not registered") - } - guestPath, err := ts.TranslateHostPath(args.Path) - if err != nil { - return nil, nil, err - } - dir := filepath.Dir(guestPath) - err = ts.sftp.MkdirAll(dir) - if err != nil { - return nil, nil, err - } - f, err := ts.sftp.Create(guestPath) - if err != nil { - return nil, nil, err - } - defer f.Close() - _, err = f.Write([]byte(args.Content)) - if err != nil { - return nil, nil, err - } - res := &msi.WriteFileResult{} - return &mcp.CallToolResult{ - // Gemini: - // A success message, e.g., `Successfully overwrote file: /path/to/your/file.txt` - // or `Successfully created and wrote to new file: /path/to/new/file.txt.` - StructuredContent: res, - }, res, nil -} - -func (ts *ToolSet) Glob(_ context.Context, - _ *mcp.CallToolRequest, args msi.GlobParams, -) (*mcp.CallToolResult, *msi.GlobResult, error) { - if ts.inst == nil { - return nil, nil, errors.New("instance not registered") - } - pathStr, err := os.Getwd() - if err != nil { - return nil, nil, err - } - if args.Path != nil && *args.Path != "" { - pathStr = *args.Path - } - guestPath, err := ts.TranslateHostPath(pathStr) - if err != nil { - return nil, nil, err - } - pattern := path.Join(guestPath, args.Pattern) - matches, err := ts.sftp.Glob(pattern) - if matches == nil { - matches = []string{} - } - if err != nil { - return nil, nil, err - } - res := &msi.GlobResult{ - Matches: matches, - } - return &mcp.CallToolResult{ - // Gemini: - // A message like: Found 5 file(s) matching "*.ts" within src, sorted by modification time (newest first):\nsrc/file1.ts\nsrc/subdir/file2.ts... - StructuredContent: res, - }, res, nil -} - -func (ts *ToolSet) SearchFileContent(ctx context.Context, - req *mcp.CallToolRequest, args msi.SearchFileContentParams, -) (*mcp.CallToolResult, *msi.SearchFileContentResult, error) { - if ts.inst == nil { - return nil, nil, errors.New("instance not registered") - } - pathStr, err := os.Getwd() - if err != nil { - return nil, nil, err - } - if args.Path != nil && *args.Path != "" { - pathStr = *args.Path - } - guestPath, err := ts.TranslateHostPath(pathStr) - if err != nil { - return nil, nil, err - } - if args.Include != nil && *args.Include != "" { - guestPath = path.Join(guestPath, *args.Include) - } - cmdToolRes, cmdRes, err := ts.RunShellCommand(ctx, req, msi.RunShellCommandParams{ - Command: []string{"git", "grep", "-n", "--no-index", args.Pattern, guestPath}, - Directory: pathStr, // Directory must be always set - }) - if err != nil { - return cmdToolRes, nil, err - } - res := &msi.SearchFileContentResult{ - GitGrepOutput: cmdRes.Stdout, - } - return &mcp.CallToolResult{ - // Gemini: - // A message like: Found 10 matching lines for regex "function\\s+myFunction" in directory src:\nsrc/file1.js:10:function myFunction() {...}\nsrc/subdir/file2.ts:45: function myFunction(param) {...}... - StructuredContent: res, - }, res, nil -} diff --git a/third_party/lima/pkg/mcp/toolset/shell.go b/third_party/lima/pkg/mcp/toolset/shell.go deleted file mode 100644 index 2f90f1d0..00000000 --- a/third_party/lima/pkg/mcp/toolset/shell.go +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Lima Authors -// SPDX-License-Identifier: Apache-2.0 - -package toolset - -import ( - "bytes" - "context" - "errors" - "os/exec" - - "github.com/modelcontextprotocol/go-sdk/mcp" - - "github.com/lima-vm/lima/v2/pkg/mcp/msi" -) - -func (ts *ToolSet) RunShellCommand(ctx context.Context, - _ *mcp.CallToolRequest, args msi.RunShellCommandParams, -) (*mcp.CallToolResult, *msi.RunShellCommandResult, error) { - if ts.inst == nil { - return nil, nil, errors.New("instance not registered") - } - guestPath, err := ts.TranslateHostPath(args.Directory) - if err != nil { - return nil, nil, err - } - cmd := exec.CommandContext(ctx, ts.limactl, - append([]string{"shell", "--workdir=" + guestPath, ts.inst.Name}, - args.Command...)...) - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmdErr := cmd.Run() - res := &msi.RunShellCommandResult{ - Stdout: stdout.String(), - Stderr: stderr.String(), - } - if cmdErr == nil { - res.ExitCode = new(0) - } else { - res.Error = cmdErr.Error() - if st := cmd.ProcessState; st != nil { - res.ExitCode = new(st.ExitCode()) - } - } - return &mcp.CallToolResult{ - StructuredContent: res, - }, res, nil -} diff --git a/third_party/lima/pkg/mcp/toolset/toolset.go b/third_party/lima/pkg/mcp/toolset/toolset.go deleted file mode 100644 index ca85efb8..00000000 --- a/third_party/lima/pkg/mcp/toolset/toolset.go +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Lima Authors -// SPDX-License-Identifier: Apache-2.0 - -package toolset - -import ( - "context" - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "slices" - - "github.com/modelcontextprotocol/go-sdk/mcp" - "github.com/pkg/sftp" - "github.com/sirupsen/logrus" - - "github.com/lima-vm/lima/v2/pkg/instance/hostname" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/mcp/msi" - "github.com/lima-vm/lima/v2/pkg/sshutil" -) - -func New(limactl string) (*ToolSet, error) { - ts := &ToolSet{ - limactl: limactl, - } - return ts, nil -} - -type ToolSet struct { - limactl string // needed for `limactl shell --workdir` - - // Set on RegisterInstance() - inst *limatype.Instance - sftp *sftp.Client - sftpCmd *exec.Cmd -} - -func newSFTPClient(ctx context.Context, inst *limatype.Instance) (*sftp.Client, *exec.Cmd, error) { - sshExe, err := sshutil.NewSSHExe() - if err != nil { - return nil, nil, err - } - args := slices.Concat(sshExe.Args, []string{"-F", filepath.Join(inst.Dir, filenames.SSHConfig), hostname.FromInstName(inst.Name), "-s", "sftp"}) - cmd := exec.CommandContext(ctx, sshExe.Exe, args...) - cmd.Stderr = os.Stderr - w, err := cmd.StdinPipe() - if err != nil { - return nil, nil, err - } - r, err := cmd.StdoutPipe() - if err != nil { - return nil, nil, err - } - if err = cmd.Start(); err != nil { - return nil, nil, err - } - client, err := sftp.NewClientPipe(r, w) - if err != nil { - if cmd != nil && cmd.Process != nil { - _ = cmd.Process.Kill() - } - } - return client, cmd, err -} - -func (ts *ToolSet) RegisterInstance(ctx context.Context, inst *limatype.Instance) error { - if inst.Status != limatype.StatusRunning { - return fmt.Errorf("expected status of instance %#q to be %#q, got %#q", - inst.Name, limatype.StatusRunning, inst.Status) - } - if len(inst.Config.Mounts) == 0 { - logrus.Warnf("instance %#q has no mount", inst.Name) - } - ts.inst = inst - var err error - ts.sftp, ts.sftpCmd, err = newSFTPClient(ctx, inst) - return err -} - -func (ts *ToolSet) RegisterServer(server *mcp.Server) error { - mcp.AddTool(server, msi.ListDirectory, ts.ListDirectory) - mcp.AddTool(server, msi.ReadFile, ts.ReadFile) - mcp.AddTool(server, msi.WriteFile, ts.WriteFile) - mcp.AddTool(server, msi.Glob, ts.Glob) - mcp.AddTool(server, msi.SearchFileContent, ts.SearchFileContent) - mcp.AddTool(server, msi.RunShellCommand, ts.RunShellCommand) - return nil -} - -func (ts *ToolSet) Close() error { - var err error - if ts.sftp != nil { - err = errors.Join(err, ts.sftp.Close()) - } - if ts.sftpCmd != nil && ts.sftpCmd.Process != nil { - err = errors.Join(err, ts.sftpCmd.Process.Kill()) - } - return err -} - -func (ts *ToolSet) TranslateHostPath(hostPath string) (string, error) { - if hostPath == "" { - return "", errors.New("path is empty") - } - if !filepath.IsAbs(hostPath) { - return "", fmt.Errorf("expected an absolute path, got a relative path: %#q", hostPath) - } - // TODO: make sure that hostPath is mounted - return hostPath, nil -} diff --git a/third_party/lima/pkg/networks/commands.go b/third_party/lima/pkg/networks/commands.go index 8754f155..10ffc82f 100644 --- a/third_party/lima/pkg/networks/commands.go +++ b/third_party/lima/pkg/networks/commands.go @@ -17,8 +17,8 @@ import ( "github.com/opencontainers/go-digest" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) const ( diff --git a/third_party/lima/pkg/networks/commands_test.go b/third_party/lima/pkg/networks/commands_test.go index 7622bc16..64f5da9c 100644 --- a/third_party/lima/pkg/networks/commands_test.go +++ b/third_party/lima/pkg/networks/commands_test.go @@ -10,7 +10,7 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" ) func TestCheck(t *testing.T) { diff --git a/third_party/lima/pkg/networks/config.go b/third_party/lima/pkg/networks/config.go index 6e720c00..4ba5760e 100644 --- a/third_party/lima/pkg/networks/config.go +++ b/third_party/lima/pkg/networks/config.go @@ -15,10 +15,10 @@ import ( "github.com/goccy/go-yaml" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/textutil" - "github.com/lima-vm/lima/v2/pkg/usrlocal" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/textutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/usrlocal" ) //go:embed networks.TEMPLATE.yaml diff --git a/third_party/lima/pkg/networks/reconcile/reconcile.go b/third_party/lima/pkg/networks/reconcile/reconcile.go index 989c7150..3fb1c5da 100644 --- a/third_party/lima/pkg/networks/reconcile/reconcile.go +++ b/third_party/lima/pkg/networks/reconcile/reconcile.go @@ -17,12 +17,12 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/networks/usernet" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/usernet" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) func Reconcile(ctx context.Context, newInst string) error { diff --git a/third_party/lima/pkg/networks/usernet/client.go b/third_party/lima/pkg/networks/usernet/client.go index 6a2437c3..a327ab8c 100644 --- a/third_party/lima/pkg/networks/usernet/client.go +++ b/third_party/lima/pkg/networks/usernet/client.go @@ -14,13 +14,13 @@ import ( "strconv" "time" - gvproxyclient "github.com/containers/gvisor-tap-vsock/pkg/client" - "github.com/containers/gvisor-tap-vsock/pkg/types" + gvproxyclient "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/client" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" - "github.com/lima-vm/lima/v2/pkg/httpclientutil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limayaml" - "github.com/lima-vm/lima/v2/pkg/networks/usernet/dnshosts" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/httpclientutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks/usernet/dnshosts" ) type Client struct { diff --git a/third_party/lima/pkg/networks/usernet/config.go b/third_party/lima/pkg/networks/usernet/config.go index 94896e3c..56f40039 100644 --- a/third_party/lima/pkg/networks/usernet/config.go +++ b/third_party/lima/pkg/networks/usernet/config.go @@ -10,9 +10,9 @@ import ( "github.com/apparentlymart/go-cidr/cidr" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/networks" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) type SockType = string diff --git a/third_party/lima/pkg/networks/usernet/config_test.go b/third_party/lima/pkg/networks/usernet/config_test.go index da1359cf..0269b879 100644 --- a/third_party/lima/pkg/networks/usernet/config_test.go +++ b/third_party/lima/pkg/networks/usernet/config_test.go @@ -9,7 +9,7 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/networks" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/networks" ) func TestUsernetConfig(t *testing.T) { diff --git a/third_party/lima/pkg/networks/usernet/dnshosts/dnshosts.go b/third_party/lima/pkg/networks/usernet/dnshosts/dnshosts.go index 00272f3b..ac2e845b 100644 --- a/third_party/lima/pkg/networks/usernet/dnshosts/dnshosts.go +++ b/third_party/lima/pkg/networks/usernet/dnshosts/dnshosts.go @@ -32,7 +32,7 @@ import ( "net" "strings" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" ) func ExtractZones(hosts hostMap) []types.Zone { diff --git a/third_party/lima/pkg/networks/usernet/dnshosts/dnshosts_test.go b/third_party/lima/pkg/networks/usernet/dnshosts/dnshosts_test.go index 9e6f3261..331598a0 100644 --- a/third_party/lima/pkg/networks/usernet/dnshosts/dnshosts_test.go +++ b/third_party/lima/pkg/networks/usernet/dnshosts/dnshosts_test.go @@ -35,7 +35,7 @@ import ( "strings" "testing" - "github.com/containers/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" ) func Test_hostsMapIP(t *testing.T) { diff --git a/third_party/lima/pkg/networks/usernet/gvproxy.go b/third_party/lima/pkg/networks/usernet/gvproxy.go index 05c30cad..856a3f16 100644 --- a/third_party/lima/pkg/networks/usernet/gvproxy.go +++ b/third_party/lima/pkg/networks/usernet/gvproxy.go @@ -19,9 +19,9 @@ import ( "time" "github.com/balajiv113/fd" - "github.com/containers/gvisor-tap-vsock/pkg/transport" - "github.com/containers/gvisor-tap-vsock/pkg/types" - "github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/transport" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/types" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/virtualnetwork" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" ) diff --git a/third_party/lima/pkg/networks/usernet/recoincile.go b/third_party/lima/pkg/networks/usernet/recoincile.go index ebc477ce..48a04e6c 100644 --- a/third_party/lima/pkg/networks/usernet/recoincile.go +++ b/third_party/lima/pkg/networks/usernet/recoincile.go @@ -17,11 +17,11 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/executil" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/lockutil" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/store" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/executil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/lockutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/store" ) // Start starts a instance a usernet network with the given name. diff --git a/third_party/lima/pkg/networks/validate.go b/third_party/lima/pkg/networks/validate.go index 5dd35047..c327baea 100644 --- a/third_party/lima/pkg/networks/validate.go +++ b/third_party/lima/pkg/networks/validate.go @@ -13,8 +13,8 @@ import ( "runtime" "strings" - "github.com/lima-vm/lima/v2/pkg/identifiers" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/identifiers" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) func (c *Config) Validate() error { diff --git a/third_party/lima/pkg/osutil/dns_darwin.go b/third_party/lima/pkg/osutil/dns_darwin.go index 20d2f943..d52ca35a 100644 --- a/third_party/lima/pkg/osutil/dns_darwin.go +++ b/third_party/lima/pkg/osutil/dns_darwin.go @@ -8,7 +8,7 @@ import ( "net" "strings" - "github.com/lima-vm/lima/v2/pkg/sysprof" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/sysprof" ) func DNSAddresses() ([]string, error) { diff --git a/third_party/lima/pkg/osutil/machineid.go b/third_party/lima/pkg/osutil/machineid.go index 4e6deaf7..8ecc6418 100644 --- a/third_party/lima/pkg/osutil/machineid.go +++ b/third_party/lima/pkg/osutil/machineid.go @@ -18,7 +18,7 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/plist" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/plist" ) var MachineID = sync.OnceValue(func() string { diff --git a/third_party/lima/pkg/osutil/user.go b/third_party/lima/pkg/osutil/user.go index 401fab87..25b63575 100644 --- a/third_party/lima/pkg/osutil/user.go +++ b/third_party/lima/pkg/osutil/user.go @@ -16,9 +16,9 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype" - . "github.com/lima-vm/lima/v2/pkg/must" - "github.com/lima-vm/lima/v2/pkg/version/versionutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + . "github.com/sylvinus/agent-vm/third_party/lima/pkg/must" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version/versionutil" ) type User struct { diff --git a/third_party/lima/pkg/plugins/plugins.go b/third_party/lima/pkg/plugins/plugins.go index bc490bd6..9658c8d9 100644 --- a/third_party/lima/pkg/plugins/plugins.go +++ b/third_party/lima/pkg/plugins/plugins.go @@ -17,8 +17,8 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/osutil" - "github.com/lima-vm/lima/v2/pkg/usrlocal" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/usrlocal" ) const defaultPathExt = ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL" diff --git a/third_party/lima/pkg/portfwd/client.go b/third_party/lima/pkg/portfwd/client.go index dcb40ad2..49452487 100644 --- a/third_party/lima/pkg/portfwd/client.go +++ b/third_party/lima/pkg/portfwd/client.go @@ -11,12 +11,12 @@ import ( "sync/atomic" "time" - "github.com/containers/gvisor-tap-vsock/pkg/services/forwarder" + "github.com/sylvinus/agent-vm/third_party/gvisor-tap-vsock/pkg/services/forwarder" "github.com/inetaf/tcpproxy" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" - guestagentclient "github.com/lima-vm/lima/v2/pkg/guestagent/api/client" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" + guestagentclient "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api/client" ) func HandleTCPConnection(_ context.Context, dialContext func(ctx context.Context, network string, addr string) (net.Conn, error), conn net.Conn, guestAddr string) { diff --git a/third_party/lima/pkg/portfwd/client_test.go b/third_party/lima/pkg/portfwd/client_test.go index f465856c..649c4015 100644 --- a/third_party/lima/pkg/portfwd/client_test.go +++ b/third_party/lima/pkg/portfwd/client_test.go @@ -15,8 +15,8 @@ import ( "google.golang.org/grpc/test/bufconn" "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" - guestagentclient "github.com/lima-vm/lima/v2/pkg/guestagent/api/client" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" + guestagentclient "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api/client" ) func TestDialContextToGRPCTunnelClosesStreamFromTCPProxy(t *testing.T) { diff --git a/third_party/lima/pkg/portfwd/forward.go b/third_party/lima/pkg/portfwd/forward.go index cee8a000..6d3908a5 100644 --- a/third_party/lima/pkg/portfwd/forward.go +++ b/third_party/lima/pkg/portfwd/forward.go @@ -10,10 +10,10 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" - "github.com/lima-vm/lima/v2/pkg/hostagent/events" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/events" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" ) var IPv4loopback1 = limayaml.IPv4loopback1 diff --git a/third_party/lima/pkg/portfwd/forward_test.go b/third_party/lima/pkg/portfwd/forward_test.go index bf986c0a..09267bb7 100644 --- a/third_party/lima/pkg/portfwd/forward_test.go +++ b/third_party/lima/pkg/portfwd/forward_test.go @@ -8,9 +8,9 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" ) // testForwarder returns a forwarder configured to disable TCP forwarding, followed diff --git a/third_party/lima/pkg/portfwd/listener.go b/third_party/lima/pkg/portfwd/listener.go index 255a206f..8aed0af9 100644 --- a/third_party/lima/pkg/portfwd/listener.go +++ b/third_party/lima/pkg/portfwd/listener.go @@ -15,7 +15,7 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) type ClosableListeners struct { diff --git a/third_party/lima/pkg/portfwd/listener_test.go b/third_party/lima/pkg/portfwd/listener_test.go index c449eead..bb103616 100644 --- a/third_party/lima/pkg/portfwd/listener_test.go +++ b/third_party/lima/pkg/portfwd/listener_test.go @@ -13,7 +13,7 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/freeport" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/freeport" ) func hasUDPListener(p *ClosableListeners, k string) bool { diff --git a/third_party/lima/pkg/portfwd/probe_test.go b/third_party/lima/pkg/portfwd/probe_test.go index 421e61a9..760db2c7 100644 --- a/third_party/lima/pkg/portfwd/probe_test.go +++ b/third_party/lima/pkg/portfwd/probe_test.go @@ -11,10 +11,10 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" - "github.com/lima-vm/lima/v2/pkg/hostagent/events" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/events" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" ) // A port a program of the host serves, on any address its loopback clients diff --git a/third_party/lima/pkg/portfwdserver/server.go b/third_party/lima/pkg/portfwdserver/server.go index ddd16d6f..b96c6083 100644 --- a/third_party/lima/pkg/portfwdserver/server.go +++ b/third_party/lima/pkg/portfwdserver/server.go @@ -14,7 +14,7 @@ import ( "github.com/inetaf/tcpproxy" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" ) type TunnelServer struct{} diff --git a/third_party/lima/pkg/portfwdserver/server_test.go b/third_party/lima/pkg/portfwdserver/server_test.go index 38918ce0..0c07c968 100644 --- a/third_party/lima/pkg/portfwdserver/server_test.go +++ b/third_party/lima/pkg/portfwdserver/server_test.go @@ -13,7 +13,7 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/guestagent/api" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/guestagent/api" ) // recvOnlyTunnelServer implements only Recv of the bidi stream; the other diff --git a/third_party/lima/pkg/registry/registry.go b/third_party/lima/pkg/registry/registry.go index 59de40d9..e049f979 100644 --- a/third_party/lima/pkg/registry/registry.go +++ b/third_party/lima/pkg/registry/registry.go @@ -13,9 +13,9 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/driver" - "github.com/lima-vm/lima/v2/pkg/driver/external/client" - "github.com/lima-vm/lima/v2/pkg/usrlocal" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver/external/client" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/usrlocal" ) const ( diff --git a/third_party/lima/pkg/registry/registry_test.go b/third_party/lima/pkg/registry/registry_test.go index 1ffd9a53..ba95376a 100644 --- a/third_party/lima/pkg/registry/registry_test.go +++ b/third_party/lima/pkg/registry/registry_test.go @@ -13,8 +13,8 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/driver" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) type mockDriver struct { diff --git a/third_party/lima/pkg/snapshot/snapshot.go b/third_party/lima/pkg/snapshot/snapshot.go index d47055a0..fb9ba3a8 100644 --- a/third_party/lima/pkg/snapshot/snapshot.go +++ b/third_party/lima/pkg/snapshot/snapshot.go @@ -7,10 +7,10 @@ import ( "context" "fmt" - "github.com/lima-vm/lima/v2/pkg/driver" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driver" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" ) func Del(ctx context.Context, inst *limatype.Instance, tag string) error { diff --git a/third_party/lima/pkg/sshutil/format.go b/third_party/lima/pkg/sshutil/format.go index d1d17f0d..ef6d7dd7 100644 --- a/third_party/lima/pkg/sshutil/format.go +++ b/third_party/lima/pkg/sshutil/format.go @@ -8,7 +8,7 @@ import ( "io" "strings" - "github.com/lima-vm/lima/v2/pkg/instance/hostname" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance/hostname" ) // FormatT specifies the format type. diff --git a/third_party/lima/pkg/sshutil/sshutil.go b/third_party/lima/pkg/sshutil/sshutil.go index 154d32c1..b0db13b1 100644 --- a/third_party/lima/pkg/sshutil/sshutil.go +++ b/third_party/lima/pkg/sshutil/sshutil.go @@ -27,11 +27,11 @@ import ( "github.com/sirupsen/logrus" "golang.org/x/sys/cpu" - "github.com/lima-vm/lima/v2/pkg/fsutil" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/lockutil" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/fsutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/lockutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) // Environment variable that allows configuring the command (alias) to execute diff --git a/third_party/lima/pkg/sshutil/sshutil_test.go b/third_party/lima/pkg/sshutil/sshutil_test.go index 2461f2a4..6d20ff07 100644 --- a/third_party/lima/pkg/sshutil/sshutil_test.go +++ b/third_party/lima/pkg/sshutil/sshutil_test.go @@ -13,7 +13,7 @@ import ( "github.com/coreos/go-semver/semver" "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) func TestDefaultPubKeys(t *testing.T) { diff --git a/third_party/lima/pkg/store/disk.go b/third_party/lima/pkg/store/disk.go index a2826f74..28c18c72 100644 --- a/third_party/lima/pkg/store/disk.go +++ b/third_party/lima/pkg/store/disk.go @@ -12,7 +12,7 @@ import ( "github.com/lima-vm/go-qcow2reader" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) type Disk struct { diff --git a/third_party/lima/pkg/store/disk_test.go b/third_party/lima/pkg/store/disk_test.go index ab3e6df4..9e603448 100644 --- a/third_party/lima/pkg/store/disk_test.go +++ b/third_party/lima/pkg/store/disk_test.go @@ -10,7 +10,7 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) func TestDiskLockForInstance(t *testing.T) { diff --git a/third_party/lima/pkg/store/fuzz_test.go b/third_party/lima/pkg/store/fuzz_test.go index 7f1083ee..aad7d162 100644 --- a/third_party/lima/pkg/store/fuzz_test.go +++ b/third_party/lima/pkg/store/fuzz_test.go @@ -10,7 +10,7 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" ) func FuzzLoadYAMLByFilePath(f *testing.F) { diff --git a/third_party/lima/pkg/store/instance.go b/third_party/lima/pkg/store/instance.go index e62f745f..5ee2a00a 100644 --- a/third_party/lima/pkg/store/instance.go +++ b/third_party/lima/pkg/store/instance.go @@ -21,14 +21,14 @@ import ( "github.com/docker/go-units" "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/driverutil" - hostagentclient "github.com/lima-vm/lima/v2/pkg/hostagent/api/client" - "github.com/lima-vm/lima/v2/pkg/instance/hostname" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/textutil" - "github.com/lima-vm/lima/v2/pkg/version/versionutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + hostagentclient "github.com/sylvinus/agent-vm/third_party/lima/pkg/hostagent/api/client" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/instance/hostname" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/textutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/version/versionutil" ) // Inspect returns err only when the instance does not exist (os.ErrNotExist). diff --git a/third_party/lima/pkg/store/instance_test.go b/third_party/lima/pkg/store/instance_test.go index 6f338083..da8b7506 100644 --- a/third_party/lima/pkg/store/instance_test.go +++ b/third_party/lima/pkg/store/instance_test.go @@ -13,7 +13,7 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) const separator = string(filepath.Separator) diff --git a/third_party/lima/pkg/store/instances_dir_test.go b/third_party/lima/pkg/store/instances_dir_test.go new file mode 100644 index 00000000..2559c8e5 --- /dev/null +++ b/third_party/lima/pkg/store/instances_dir_test.go @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package store + +import ( + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" + + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" +) + +func TestInstancesDir(t *testing.T) { + home := t.TempDir() + instances := t.TempDir() + t.Setenv("LIMA_HOME", home) + t.Setenv(dirnames.InstancesEnv, instances) + realInstances, err := filepath.EvalSymlinks(instances) + assert.NilError(t, err) + + dir, err := dirnames.InstanceDir("vm1") + assert.NilError(t, err) + assert.Equal(t, dir, filepath.Join(realInstances, "vm1", dirnames.InstanceSubdir)) + assert.Equal(t, dirnames.InstanceName(dir), "vm1") + + // A folder of the caller's without an instance folder is not an instance. + assert.NilError(t, os.MkdirAll(filepath.Join(instances, "unfinished"), 0o755)) + assert.NilError(t, os.MkdirAll(filepath.Join(instances, "vm1", dirnames.InstanceSubdir), 0o755)) + assert.NilError(t, os.MkdirAll(filepath.Join(instances, "vm2", dirnames.InstanceSubdir), 0o755)) + // An instance in LIMA_HOME is not seen. + assert.NilError(t, os.MkdirAll(filepath.Join(home, "vm3"), 0o755)) + + names, err := Instances() + assert.NilError(t, err) + assert.DeepEqual(t, names, []string{"vm1", "vm2"}) + + // Validate looks for lima.yaml in the instance folder. + assert.ErrorContains(t, Validate(), filenames.LimaYAML) + for _, n := range names { + assert.NilError(t, os.WriteFile(filepath.Join(instances, n, dirnames.InstanceSubdir, filenames.LimaYAML), nil, 0o644)) + } + assert.NilError(t, Validate()) + + // LIMA_HOME keeps the rest. + cfg, err := dirnames.LimaConfigDir() + assert.NilError(t, err) + assert.Equal(t, filepath.Dir(cfg), func() string { r, _ := filepath.EvalSymlinks(home); return r }()) +} + +func TestInstancesDirRelative(t *testing.T) { + t.Setenv(dirnames.InstancesEnv, "relative/path") + _, err := dirnames.InstanceDir("vm1") + assert.ErrorContains(t, err, "absolute") +} + +func TestInstancesDirUnset(t *testing.T) { + home := t.TempDir() + t.Setenv("LIMA_HOME", home) + t.Setenv(dirnames.InstancesEnv, "") + realHome, err := filepath.EvalSymlinks(home) + assert.NilError(t, err) + + dir, err := dirnames.InstanceDir("vm1") + assert.NilError(t, err) + assert.Equal(t, dir, filepath.Join(realHome, "vm1")) + assert.Equal(t, dirnames.InstanceName(dir), "vm1") +} diff --git a/third_party/lima/pkg/store/pidfile.go b/third_party/lima/pkg/store/pidfile.go index 781b8d4e..a56f084c 100644 --- a/third_party/lima/pkg/store/pidfile.go +++ b/third_party/lima/pkg/store/pidfile.go @@ -12,8 +12,8 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) // pidFileSuffix is the suffix of the PID files of an instance. diff --git a/third_party/lima/pkg/store/pidfile_test.go b/third_party/lima/pkg/store/pidfile_test.go index b8c9a491..42b86677 100644 --- a/third_party/lima/pkg/store/pidfile_test.go +++ b/third_party/lima/pkg/store/pidfile_test.go @@ -12,9 +12,9 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/osutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/osutil" ) // fakeBootSessionID replaces the source of the boot session ID for the duration of the test. diff --git a/third_party/lima/pkg/store/store.go b/third_party/lima/pkg/store/store.go index 10cf7701..3d027055 100644 --- a/third_party/lima/pkg/store/store.go +++ b/third_party/lima/pkg/store/store.go @@ -10,12 +10,12 @@ import ( "path/filepath" "strings" - "github.com/lima-vm/lima/v2/pkg/driverutil" - "github.com/lima-vm/lima/v2/pkg/identifiers" - "github.com/lima-vm/lima/v2/pkg/limatype" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/limayaml" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/driverutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/identifiers" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/filenames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limayaml" ) // Directory returns the LimaDir. @@ -29,17 +29,16 @@ func Directory() string { // Validate checks the LimaDir. func Validate() error { - limaDir, err := dirnames.LimaDir() - if err != nil { - return err - } names, err := Instances() if err != nil { return err } for _, name := range names { // Each instance directory needs to have limayaml - instDir := filepath.Join(limaDir, name) + instDir, err := dirnames.InstanceDir(name) + if err != nil { + return err + } yamlPath := filepath.Join(instDir, filenames.LimaYAML) if _, err := os.Stat(yamlPath); err != nil { return err @@ -48,12 +47,20 @@ func Validate() error { return nil } -// Instances returns the names of the instances under LimaDir. +// Instances returns the names of the instances under LimaDir, or under +// $LIMA_INSTANCES_DIR when it is set (see dirnames.InstancesEnv): there, a +// folder without an instance folder in it is not one. func Instances() ([]string, error) { - limaDir, err := dirnames.LimaDir() + instancesDir, err := dirnames.InstancesDir() if err != nil { return nil, err } + limaDir := instancesDir + if limaDir == "" { + if limaDir, err = dirnames.LimaDir(); err != nil { + return nil, err + } + } limaDirList, err := os.ReadDir(limaDir) if err != nil { if errors.Is(err, os.ErrNotExist) { @@ -69,6 +76,11 @@ func Instances() ([]string, error) { if !f.IsDir() { continue } + if instancesDir != "" { + if st, err := os.Stat(filepath.Join(limaDir, f.Name(), dirnames.InstanceSubdir)); err != nil || !st.IsDir() { + continue + } + } names = append(names, f.Name()) } return names, nil diff --git a/third_party/lima/pkg/store/store_test.go b/third_party/lima/pkg/store/store_test.go index 9879bf25..f11ba081 100644 --- a/third_party/lima/pkg/store/store_test.go +++ b/third_party/lima/pkg/store/store_test.go @@ -8,7 +8,7 @@ import ( "gotest.tools/v3/assert" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" ) func TestValidateInstName(t *testing.T) { diff --git a/third_party/lima/pkg/templatestore/templatestore.go b/third_party/lima/pkg/templatestore/templatestore.go index d2e8ccba..7ab1a245 100644 --- a/third_party/lima/pkg/templatestore/templatestore.go +++ b/third_party/lima/pkg/templatestore/templatestore.go @@ -14,8 +14,8 @@ import ( "strings" "unicode" - "github.com/lima-vm/lima/v2/pkg/limatype/dirnames" - "github.com/lima-vm/lima/v2/pkg/usrlocal" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype/dirnames" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/usrlocal" ) type Template struct { diff --git a/third_party/lima/pkg/usrlocal/usrlocal.go b/third_party/lima/pkg/usrlocal/usrlocal.go index dff2063e..07420172 100644 --- a/third_party/lima/pkg/usrlocal/usrlocal.go +++ b/third_party/lima/pkg/usrlocal/usrlocal.go @@ -15,8 +15,8 @@ import ( "github.com/sirupsen/logrus" - "github.com/lima-vm/lima/v2/pkg/debugutil" - "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/debugutil" + "github.com/sylvinus/agent-vm/third_party/lima/pkg/limatype" ) // ExecutableViaArgs0 returns the absolute path to the executable used to start this process. diff --git a/third_party/lima/pkg/version/agentvm_patches.go b/third_party/lima/pkg/version/agentvm_patches.go new file mode 100644 index 00000000..e53807dd --- /dev/null +++ b/third_party/lima/pkg/version/agentvm_patches.go @@ -0,0 +1,9 @@ +// Code generated by scripts/third-party-sync. DO NOT EDIT. + +package version + +// AgentVMPatches is the last of agent-vm's patches/lima in this tree. +const AgentVMPatches = "lima-7" + +// AgentVMUpstream is the upstream version this tree is made from. +const AgentVMUpstream = "v2.3.0-beta.0-98-g928babab" diff --git a/third_party/sftp/agentvm_patches.go b/third_party/sftp/agentvm_patches.go new file mode 100644 index 00000000..167caab5 --- /dev/null +++ b/third_party/sftp/agentvm_patches.go @@ -0,0 +1,9 @@ +// Code generated by scripts/third-party-sync. DO NOT EDIT. + +package sftp + +// AgentVMPatches is the last of agent-vm's patches/sftp in this tree. +const AgentVMPatches = "sftp-5" + +// AgentVMUpstream is the upstream version this tree is made from. +const AgentVMUpstream = "v1.13.11" diff --git a/third_party/sftp/client.go b/third_party/sftp/client.go index c04f1afd..3b07df87 100644 --- a/third_party/sftp/client.go +++ b/third_party/sftp/client.go @@ -18,7 +18,7 @@ import ( "github.com/kr/fs" "golang.org/x/crypto/ssh" - "github.com/pkg/sftp/internal/encoding/ssh/filexfer/openssh" + "github.com/sylvinus/agent-vm/third_party/sftp/internal/encoding/ssh/filexfer/openssh" ) var ( diff --git a/third_party/sftp/example_test.go b/third_party/sftp/example_test.go index 0093eb0c..2eb44cec 100644 --- a/third_party/sftp/example_test.go +++ b/third_party/sftp/example_test.go @@ -10,7 +10,7 @@ import ( "path" "strings" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" "golang.org/x/crypto/ssh" ) diff --git a/third_party/sftp/examples/buffered-read-benchmark/main.go b/third_party/sftp/examples/buffered-read-benchmark/main.go index 7f2adc4a..514eed1c 100644 --- a/third_party/sftp/examples/buffered-read-benchmark/main.go +++ b/third_party/sftp/examples/buffered-read-benchmark/main.go @@ -14,7 +14,7 @@ import ( "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" ) var ( diff --git a/third_party/sftp/examples/buffered-write-benchmark/main.go b/third_party/sftp/examples/buffered-write-benchmark/main.go index 7a0594cf..799118b8 100644 --- a/third_party/sftp/examples/buffered-write-benchmark/main.go +++ b/third_party/sftp/examples/buffered-write-benchmark/main.go @@ -14,7 +14,7 @@ import ( "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" ) var ( diff --git a/third_party/sftp/examples/go-sftp-server/main.go b/third_party/sftp/examples/go-sftp-server/main.go index aef436cb..a07538bf 100644 --- a/third_party/sftp/examples/go-sftp-server/main.go +++ b/third_party/sftp/examples/go-sftp-server/main.go @@ -11,7 +11,7 @@ import ( "net" "os" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" "golang.org/x/crypto/ssh" ) diff --git a/third_party/sftp/examples/request-server/main.go b/third_party/sftp/examples/request-server/main.go index 37a3a55f..76e08c12 100644 --- a/third_party/sftp/examples/request-server/main.go +++ b/third_party/sftp/examples/request-server/main.go @@ -11,7 +11,7 @@ import ( "net" "os" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" "golang.org/x/crypto/ssh" ) diff --git a/third_party/sftp/examples/streaming-read-benchmark/main.go b/third_party/sftp/examples/streaming-read-benchmark/main.go index 3f0f4893..ca217c85 100644 --- a/third_party/sftp/examples/streaming-read-benchmark/main.go +++ b/third_party/sftp/examples/streaming-read-benchmark/main.go @@ -15,7 +15,7 @@ import ( "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" ) var ( diff --git a/third_party/sftp/examples/streaming-write-benchmark/main.go b/third_party/sftp/examples/streaming-write-benchmark/main.go index 2139d97a..c082c44a 100644 --- a/third_party/sftp/examples/streaming-write-benchmark/main.go +++ b/third_party/sftp/examples/streaming-write-benchmark/main.go @@ -15,7 +15,7 @@ import ( "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/agent" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" ) var ( diff --git a/third_party/sftp/go.mod b/third_party/sftp/go.mod deleted file mode 100644 index bdaf6153..00000000 --- a/third_party/sftp/go.mod +++ /dev/null @@ -1,16 +0,0 @@ -module github.com/pkg/sftp - -go 1.25.0 - -require ( - github.com/kr/fs v0.1.0 - github.com/stretchr/testify v1.11.1 - golang.org/x/crypto v0.54.0 - golang.org/x/sys v0.47.0 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/third_party/sftp/go.sum b/third_party/sftp/go.sum deleted file mode 100644 index 8e211ec6..00000000 --- a/third_party/sftp/go.sum +++ /dev/null @@ -1,25 +0,0 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/fsync.go b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/fsync.go index 708a4ba7..45a043de 100644 --- a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/fsync.go +++ b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/fsync.go @@ -1,7 +1,7 @@ package openssh import ( - sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" + sshfx "github.com/sylvinus/agent-vm/third_party/sftp/internal/encoding/ssh/filexfer" ) const extensionFSync = "fsync@openssh.com" diff --git a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/fsync_test.go b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/fsync_test.go index f9e878fb..4eb4b51f 100644 --- a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/fsync_test.go +++ b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/fsync_test.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" + sshfx "github.com/sylvinus/agent-vm/third_party/sftp/internal/encoding/ssh/filexfer" ) var _ sshfx.PacketMarshaller = &FSyncExtendedPacket{} diff --git a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/hardlink.go b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/hardlink.go index f48d25a2..14b5bfe9 100644 --- a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/hardlink.go +++ b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/hardlink.go @@ -1,7 +1,7 @@ package openssh import ( - sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" + sshfx "github.com/sylvinus/agent-vm/third_party/sftp/internal/encoding/ssh/filexfer" ) const extensionHardlink = "hardlink@openssh.com" diff --git a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/hardlink_test.go b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/hardlink_test.go index 5d3be06b..ce7ecbae 100644 --- a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/hardlink_test.go +++ b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/hardlink_test.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" + sshfx "github.com/sylvinus/agent-vm/third_party/sftp/internal/encoding/ssh/filexfer" ) var _ sshfx.PacketMarshaller = &HardlinkExtendedPacket{} diff --git a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/posix-rename.go b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/posix-rename.go index 5166489c..aedd68da 100644 --- a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/posix-rename.go +++ b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/posix-rename.go @@ -1,7 +1,7 @@ package openssh import ( - sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" + sshfx "github.com/sylvinus/agent-vm/third_party/sftp/internal/encoding/ssh/filexfer" ) const extensionPOSIXRename = "posix-rename@openssh.com" diff --git a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go index 58e4fffb..a62f48d9 100644 --- a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go +++ b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/posix-rename_test.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" + sshfx "github.com/sylvinus/agent-vm/third_party/sftp/internal/encoding/ssh/filexfer" ) var _ sshfx.PacketMarshaller = &POSIXRenameExtendedPacket{} diff --git a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/statvfs.go b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/statvfs.go index 51029ca0..79baa759 100644 --- a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/statvfs.go +++ b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/statvfs.go @@ -1,7 +1,7 @@ package openssh import ( - sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" + sshfx "github.com/sylvinus/agent-vm/third_party/sftp/internal/encoding/ssh/filexfer" ) const extensionStatVFS = "statvfs@openssh.com" diff --git a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/statvfs_test.go b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/statvfs_test.go index 014aa637..c502ca06 100644 --- a/third_party/sftp/internal/encoding/ssh/filexfer/openssh/statvfs_test.go +++ b/third_party/sftp/internal/encoding/ssh/filexfer/openssh/statvfs_test.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" + sshfx "github.com/sylvinus/agent-vm/third_party/sftp/internal/encoding/ssh/filexfer" ) var _ sshfx.PacketMarshaller = &StatVFSExtendedPacket{} diff --git a/third_party/sftp/ls_formatting.go b/third_party/sftp/ls_formatting.go index 19271ad7..f9bdc7db 100644 --- a/third_party/sftp/ls_formatting.go +++ b/third_party/sftp/ls_formatting.go @@ -8,7 +8,7 @@ import ( "strconv" "time" - sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" + sshfx "github.com/sylvinus/agent-vm/third_party/sftp/internal/encoding/ssh/filexfer" ) func lsFormatID(id uint32) string { diff --git a/third_party/sftp/server_standalone/main.go b/third_party/sftp/server_standalone/main.go index 0b8e102a..605ce375 100644 --- a/third_party/sftp/server_standalone/main.go +++ b/third_party/sftp/server_standalone/main.go @@ -10,7 +10,7 @@ import ( "io/ioutil" "os" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" ) func main() { diff --git a/third_party/sftp/stat.go b/third_party/sftp/stat.go index 2bb2c137..39b61951 100644 --- a/third_party/sftp/stat.go +++ b/third_party/sftp/stat.go @@ -3,7 +3,7 @@ package sftp import ( "os" - sshfx "github.com/pkg/sftp/internal/encoding/ssh/filexfer" + sshfx "github.com/sylvinus/agent-vm/third_party/sftp/internal/encoding/ssh/filexfer" ) // isRegular returns true if the mode describes a regular file. diff --git a/third_party/sshocker/cmd/sshocker/cliutil.go b/third_party/sshocker/cmd/sshocker/cliutil.go deleted file mode 100644 index a3252ca1..00000000 --- a/third_party/sshocker/cmd/sshocker/cliutil.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/urfave/cli/v2" -) - -// setHidden derived from https://github.com/aquasecurity/trivy/blob/v0.11.0/internal/app.go#L273-L298 (Apache License 2.0) -func setHidden(flags []cli.Flag, hidden bool) []cli.Flag { - var newFlags []cli.Flag - for _, flag := range flags { - var f cli.Flag - switch pf := flag.(type) { - case *cli.StringFlag: - stringFlag := *pf - stringFlag.Hidden = hidden - f = &stringFlag - case *cli.StringSliceFlag: - stringSliceFlag := *pf - stringSliceFlag.Hidden = hidden - f = &stringSliceFlag - case *cli.BoolFlag: - boolFlag := *pf - boolFlag.Hidden = hidden - f = &boolFlag - case *cli.IntFlag: - intFlag := *pf - intFlag.Hidden = hidden - f = &intFlag - case *cli.DurationFlag: - durationFlag := *pf - durationFlag.Hidden = hidden - f = &durationFlag - default: - panic(fmt.Errorf("unknown flag type: %+v", pf)) - } - newFlags = append(newFlags, f) - } - return newFlags -} diff --git a/third_party/sshocker/cmd/sshocker/main.go b/third_party/sshocker/cmd/sshocker/main.go deleted file mode 100644 index 17f43ea5..00000000 --- a/third_party/sshocker/cmd/sshocker/main.go +++ /dev/null @@ -1,52 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/lima-vm/sshocker/pkg/version" - "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" -) - -func newApp() *cli.App { - debug := false - app := cli.NewApp() - app.Name = "sshocker" - app.Usage = "ssh + reverse sshfs + port forwarder, in Docker-like CLI" - app.UsageText = "sshocker run -p LOCALIP:LOCALPORT:REMOTEPORT -v LOCALDIR:REMOTEDIR USER@HOST" - // we can't set app.Version because it conflicts with our `-v` - - app.Flags = []cli.Flag{ - &cli.BoolFlag{ - Name: "debug", - Usage: "debug mode", - Destination: &debug, - }, - &cli.BoolFlag{ - Name: "version", - Usage: "print the version", - }, - } - app.Flags = append(app.Flags, setHidden(runFlags, true)...) - app.Before = func(context *cli.Context) error { - if context.Bool("version") { - fmt.Printf("sshocker version %s\n", version.Version) - os.Exit(0) - return nil - } - if debug { - logrus.SetLevel(logrus.DebugLevel) - } - return nil - } - app.Commands = []*cli.Command{runCommand} - app.Action = runAction - return app -} - -func main() { - if err := newApp().Run(os.Args); err != nil { - logrus.Fatal(err) - } -} diff --git a/third_party/sshocker/cmd/sshocker/run.go b/third_party/sshocker/cmd/sshocker/run.go deleted file mode 100644 index 31a5058b..00000000 --- a/third_party/sshocker/cmd/sshocker/run.go +++ /dev/null @@ -1,180 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "net" - "os/user" - "path/filepath" - "strconv" - "strings" - - "github.com/lima-vm/sshocker/pkg/mount" - "github.com/lima-vm/sshocker/pkg/ssh" - "github.com/lima-vm/sshocker/pkg/sshocker" - "github.com/urfave/cli/v2" -) - -var ( - runFlags = []cli.Flag{ - &cli.StringFlag{ - Name: "ssh-config", - Aliases: []string{"F"}, - Usage: "ssh config file", - }, - &cli.BoolFlag{ - Name: "ssh-persist", - Usage: "enable ControlPersist", - Value: true, - }, - &cli.StringSliceFlag{ - Name: "v", - Usage: "Mount a reverse SSHFS, " + - "e.g. `.:/mnt/ssh` to mount the current directory on the client onto /mnt/ssh on the server, " + - "append `:ro` for read-only mount", - }, - &cli.StringSliceFlag{ - Name: "p", - Usage: "Expose a port, e.g. `8080:80` to forward the port 8080 the client onto the port 80 on the server", - }, - &cli.BoolFlag{ - Name: "sshfs-nonempty", - Usage: "enable sshfs nonempty", - Value: false, - }, - &cli.StringSliceFlag{ - Name: "sshfs-option", - Usage: "Set sshfs mount options.", - }, - &cli.StringFlag{ - Name: "driver", - Usage: "SFTP server driver. \"builtin\" (legacy) or \"openssh-sftp-server\" (robust and secure, recommended), automatically chosen by default", - Value: "auto", - }, - &cli.StringFlag{ - Name: "openssh-sftp-server", - Usage: "OpenSSH SFTP Server binary, automatically chosen by default", - Value: "", - }, - } - runCommand = &cli.Command{ - Name: "run", - Usage: "Akin to `docker run` (The default subcommand)", - Action: runAction, - Flags: runFlags, - } -) - -func parseHost(s string) (string, int, error) { - if !strings.Contains(s, ":") { - // FIXME: this check is not valid for IPv6! - return s, 0, nil - } - host, portStr, err := net.SplitHostPort(s) - if err != nil { - return "", 0, err - } - port, err := strconv.Atoi(portStr) - if err != nil { - return "", 0, err - } - // host may contain "user@" prefix. - return host, port, nil -} - -func runAction(clicontext *cli.Context) error { - if clicontext.NArg() < 1 { - return errors.New("no host specified") - } - sshConfig := &ssh.SSHConfig{ - ConfigFile: clicontext.String("ssh-config"), - Persist: clicontext.Bool("ssh-persist"), - } - host, port, err := parseHost(clicontext.Args().First()) - if err != nil { - return err - } - var sshfsAdditionalArgs []string - if clicontext.Bool("sshfs-nonempty") { - sshfsAdditionalArgs = append(sshfsAdditionalArgs, "-o", "nonempty") - } - for _, o := range clicontext.StringSlice("sshfs-option") { - sshfsAdditionalArgs = append(sshfsAdditionalArgs, "-o", o) - } - - x := &sshocker.Sshocker{ - SSHConfig: sshConfig, - Host: host, - Port: port, - Command: clicontext.Args().Tail(), - SSHFSAdditionalArgs: sshfsAdditionalArgs, - Driver: clicontext.String("driver"), - OpensshSftpServerBinary: clicontext.String("openssh-sftp-server"), - } - if len(x.Command) > 0 && x.Command[0] == "--" { - x.Command = x.Command[1:] - } - for _, v := range clicontext.StringSlice("v") { - m, err := parseFlagV(v) - if err != nil { - return err - } - x.Mounts = append(x.Mounts, m) - } - for _, p := range clicontext.StringSlice("p") { - lforward, err := parseFlagP(p) - if err != nil { - return err - } - x.LForwards = append(x.LForwards, lforward) - } - return x.Run() -} - -func expandLocalPath(localPath string) (string, error) { - s := localPath - if s == "" { - return "", errors.New("got empty local path") - } - if strings.HasPrefix(s, "~/") { - u, err := user.Current() - if err != nil { - return "", err - } - if u.HomeDir == "" { - return "", errors.New("cannot determine the local home directory") - } - s = strings.Replace(s, "~", u.HomeDir, 1) - } - return filepath.Abs(s) -} - -// parseFlagV parses -v flag, akin to `docker run -v` flags. -func parseFlagV(s string) (mount.Mount, error) { - m := mount.Mount{ - Type: mount.MountTypeReverseSSHFS, - } - // TODO: support Windows. How does `docker run -v` work with Windows drive letters? - split := strings.Split(s, ":") - switch len(split) { - case 2: - m.Source = split[0] - m.Destination = split[1] - case 3: - m.Source = split[0] - m.Destination = split[1] - if split[2] == "ro" { - m.Readonly = true - } else { - return m, fmt.Errorf("cannot parse %q: unknown option %q", s, split[2]) - } - default: - return m, fmt.Errorf("cannot parse %q", s) - } - var err error - m.Source, err = expandLocalPath(m.Source) - if err != nil { - return m, fmt.Errorf("cannot use %q: %w", s, err) - } - return m, nil -} diff --git a/third_party/sshocker/cmd/sshocker/run_p.go b/third_party/sshocker/cmd/sshocker/run_p.go deleted file mode 100644 index 548c30ce..00000000 --- a/third_party/sshocker/cmd/sshocker/run_p.go +++ /dev/null @@ -1,43 +0,0 @@ -package main - -import ( - "fmt" - "strconv" - "strings" -) - -// parseFlagP parses -p flag, akin to `docker run -p` flags. -// The returned value conforms to the `ssh -L` syntax -func parseFlagP(s string) (string, error) { - split := strings.Split(s, ":") - switch len(split) { - case 1: - port, err := strconv.Atoi(split[0]) - if err != nil { - return "", fmt.Errorf("invalid port %q", split[0]) - } - return fmt.Sprintf("0.0.0.0:%d:localhost:%d", port, port), nil - case 2: - localPort, err := strconv.Atoi(split[0]) - if err != nil { - return "", fmt.Errorf("invalid port %q", split[0]) - } - remotePort, err := strconv.Atoi(split[1]) - if err != nil { - return "", fmt.Errorf("invalid port %q", split[1]) - } - return fmt.Sprintf("0.0.0.0:%d:localhost:%d", localPort, remotePort), nil - case 3: - localIP := split[0] - localPort, err := strconv.Atoi(split[1]) - if err != nil { - return "", fmt.Errorf("invalid port %q", split[1]) - } - remotePort, err := strconv.Atoi(split[2]) - if err != nil { - return "", fmt.Errorf("invalid port %q", split[2]) - } - return fmt.Sprintf("%s:%d:localhost:%d", localIP, localPort, remotePort), nil - } - return "", fmt.Errorf("cannot parse %q, should be [[LOCALIP:]LOCALPORT:]REMOTEPORT", s) -} diff --git a/third_party/sshocker/cmd/sshocker/run_p_test.go b/third_party/sshocker/cmd/sshocker/run_p_test.go deleted file mode 100644 index ba10a7d8..00000000 --- a/third_party/sshocker/cmd/sshocker/run_p_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import "testing" - -func TestParseFlagP(t *testing.T) { - testCases := map[string]string{ - "80": "0.0.0.0:80:localhost:80", - "8080:80": "0.0.0.0:8080:localhost:80", - "127.0.0.1:8080:80": "127.0.0.1:8080:localhost:80", - "127.0.0.1:8080:127.0.0.1:80": "", - } - for k, v := range testCases { - got, err := parseFlagP(k) - if v == "" { - if err == nil { - t.Errorf("error is expected for %q", k) - } - continue - } - if err != nil { - t.Errorf("failed to parse %q: %v", k, err) - continue - } - if got != v { - t.Errorf("expected %q, got %q for %q", v, got, k) - } - } -} diff --git a/third_party/sshocker/go.mod b/third_party/sshocker/go.mod deleted file mode 100644 index 1226b151..00000000 --- a/third_party/sshocker/go.mod +++ /dev/null @@ -1,19 +0,0 @@ -module github.com/lima-vm/sshocker - -go 1.25.0 - -require ( - github.com/pkg/sftp v1.13.11 - github.com/sirupsen/logrus v1.9.4 - github.com/urfave/cli/v2 v2.27.7 - golang.org/x/sys v0.47.0 - golang.org/x/text v0.42.0 -) - -require ( - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/kr/fs v0.1.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - golang.org/x/crypto v0.54.0 // indirect -) diff --git a/third_party/sshocker/go.sum b/third_party/sshocker/go.sum deleted file mode 100644 index 021360f3..00000000 --- a/third_party/sshocker/go.sum +++ /dev/null @@ -1,28 +0,0 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= -github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE= -github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= -github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/third_party/sshocker/pkg/reversesshfs/agentvm_patches.go b/third_party/sshocker/pkg/reversesshfs/agentvm_patches.go new file mode 100644 index 00000000..3f0de10c --- /dev/null +++ b/third_party/sshocker/pkg/reversesshfs/agentvm_patches.go @@ -0,0 +1,9 @@ +// Code generated by scripts/third-party-sync. DO NOT EDIT. + +package reversesshfs + +// AgentVMPatches is the last of agent-vm's patches/sshocker in this tree. +const AgentVMPatches = "sshocker-9" + +// AgentVMUpstream is the upstream version this tree is made from. +const AgentVMUpstream = "v0.3.11" diff --git a/third_party/sshocker/pkg/reversesshfs/reversesshfs.go b/third_party/sshocker/pkg/reversesshfs/reversesshfs.go index 4aae8ea3..4f40cf22 100644 --- a/third_party/sshocker/pkg/reversesshfs/reversesshfs.go +++ b/third_party/sshocker/pkg/reversesshfs/reversesshfs.go @@ -15,8 +15,8 @@ import ( "strings" "sync/atomic" - "github.com/lima-vm/sshocker/pkg/ssh" - "github.com/lima-vm/sshocker/pkg/util" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/ssh" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/util" "github.com/sirupsen/logrus" ) diff --git a/third_party/sshocker/pkg/reversesshfs/rooted.go b/third_party/sshocker/pkg/reversesshfs/rooted.go index 84fb0b20..5dc43a66 100644 --- a/third_party/sshocker/pkg/reversesshfs/rooted.go +++ b/third_party/sshocker/pkg/reversesshfs/rooted.go @@ -14,7 +14,7 @@ import ( "sync" "time" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" ) // rootedHandlers serves only the files under rootPath. diff --git a/third_party/sshocker/pkg/reversesshfs/rooted_darwin.go b/third_party/sshocker/pkg/reversesshfs/rooted_darwin.go index 589c2152..4acdd98d 100644 --- a/third_party/sshocker/pkg/reversesshfs/rooted_darwin.go +++ b/third_party/sshocker/pkg/reversesshfs/rooted_darwin.go @@ -1,7 +1,7 @@ package reversesshfs import ( - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" "golang.org/x/sys/unix" ) diff --git a/third_party/sshocker/pkg/reversesshfs/rooted_e2e_test.go b/third_party/sshocker/pkg/reversesshfs/rooted_e2e_test.go index 5f6d8a62..b5ad468e 100644 --- a/third_party/sshocker/pkg/reversesshfs/rooted_e2e_test.go +++ b/third_party/sshocker/pkg/reversesshfs/rooted_e2e_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/lima-vm/sshocker/pkg/util" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/util" "golang.org/x/sys/unix" ) diff --git a/third_party/sshocker/pkg/reversesshfs/rooted_linux.go b/third_party/sshocker/pkg/reversesshfs/rooted_linux.go index 2ae2e83d..823134af 100644 --- a/third_party/sshocker/pkg/reversesshfs/rooted_linux.go +++ b/third_party/sshocker/pkg/reversesshfs/rooted_linux.go @@ -4,7 +4,7 @@ import ( "errors" "strconv" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" "golang.org/x/sys/unix" ) diff --git a/third_party/sshocker/pkg/reversesshfs/rooted_others.go b/third_party/sshocker/pkg/reversesshfs/rooted_others.go index 1fc49721..eb1fb91f 100644 --- a/third_party/sshocker/pkg/reversesshfs/rooted_others.go +++ b/third_party/sshocker/pkg/reversesshfs/rooted_others.go @@ -6,7 +6,7 @@ import ( "errors" "io" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" ) type rootedHandlers struct{} diff --git a/third_party/sshocker/pkg/reversesshfs/rooted_robust_test.go b/third_party/sshocker/pkg/reversesshfs/rooted_robust_test.go index a5d6e3f2..a9c742be 100644 --- a/third_party/sshocker/pkg/reversesshfs/rooted_robust_test.go +++ b/third_party/sshocker/pkg/reversesshfs/rooted_robust_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" ) // An opened directory renamed, with a symlink to a folder outside the root put in its place, diff --git a/third_party/sshocker/pkg/reversesshfs/rooted_test.go b/third_party/sshocker/pkg/reversesshfs/rooted_test.go index 2b8a60c4..fad006e6 100644 --- a/third_party/sshocker/pkg/reversesshfs/rooted_test.go +++ b/third_party/sshocker/pkg/reversesshfs/rooted_test.go @@ -12,8 +12,8 @@ import ( "testing" "time" - "github.com/lima-vm/sshocker/pkg/util" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/util" + "github.com/sylvinus/agent-vm/third_party/sftp" ) var ( diff --git a/third_party/sshocker/pkg/reversesshfs/rooted_unix.go b/third_party/sshocker/pkg/reversesshfs/rooted_unix.go index 6515874b..91158697 100644 --- a/third_party/sshocker/pkg/reversesshfs/rooted_unix.go +++ b/third_party/sshocker/pkg/reversesshfs/rooted_unix.go @@ -9,7 +9,7 @@ import ( "path/filepath" "strings" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" "golang.org/x/sys/unix" ) diff --git a/third_party/sshocker/pkg/reversesshfs/rooted_windows.go b/third_party/sshocker/pkg/reversesshfs/rooted_windows.go index e4c87c17..55185f05 100644 --- a/third_party/sshocker/pkg/reversesshfs/rooted_windows.go +++ b/third_party/sshocker/pkg/reversesshfs/rooted_windows.go @@ -9,7 +9,7 @@ import ( "syscall" "unsafe" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" "golang.org/x/sys/windows" ) diff --git a/third_party/sshocker/pkg/reversesshfs/rooted_windows_test.go b/third_party/sshocker/pkg/reversesshfs/rooted_windows_test.go index 78824d0c..b69c36ef 100644 --- a/third_party/sshocker/pkg/reversesshfs/rooted_windows_test.go +++ b/third_party/sshocker/pkg/reversesshfs/rooted_windows_test.go @@ -8,7 +8,7 @@ import ( "testing" "unsafe" - "github.com/pkg/sftp" + "github.com/sylvinus/agent-vm/third_party/sftp" "golang.org/x/sys/windows" ) diff --git a/third_party/sshocker/pkg/sshocker/sshocker.go b/third_party/sshocker/pkg/sshocker/sshocker.go index a90db0d5..e037da75 100644 --- a/third_party/sshocker/pkg/sshocker/sshocker.go +++ b/third_party/sshocker/pkg/sshocker/sshocker.go @@ -7,9 +7,9 @@ import ( "os/exec" "strconv" - "github.com/lima-vm/sshocker/pkg/mount" - "github.com/lima-vm/sshocker/pkg/reversesshfs" - "github.com/lima-vm/sshocker/pkg/ssh" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/mount" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/reversesshfs" + "github.com/sylvinus/agent-vm/third_party/sshocker/pkg/ssh" "github.com/sirupsen/logrus" ) diff --git a/www/src/i18n/en.ts b/www/src/i18n/en.ts index cd7f2dc7..214ce613 100644 --- a/www/src/i18n/en.ts +++ b/www/src/i18n/en.ts @@ -273,13 +273,15 @@ export const en = { ['--unsafe-disable-security-prompts', 'Go on where a start would stop on a security question (see [Protecting .git](#git)). The warnings are still printed. Same as `AGENT_VM_UNSAFE_DISABLE_SECURITY_PROMPTS=1`.', 'off'], ['--rm', 'Destroy the VM once the command exits.', 'off'], ['--scratch', 'A new VM with nothing of yours mounted, deleted when the command ends (asked first on a terminal). `~/.agent-vm/env` and `runtime.sh` go in, the project’s files do not. Several can run at once.', 'off'], + ['--name NAME', 'The VM called `NAME`, not the folder’s own, so that a folder can have several (`agent-vm --name agent2 claude`). It belongs to the folder it is made in and runs from there only: from another it stops, naming the folder. `--reset` makes it again, the only way to change that. Lowercase letters, digits and dashes, at most 40 characters; not `base` nor `base-…`, nor ending in a dash and 8 hex digits. `stop NAME` and `rm NAME` reach it.', 'the folder’s VM'], + ['--no-shares', 'With `--name`, when the VM is made: it shares nothing, like `--scratch`, but is kept and runs from any folder (`agent-vm --name sandbox --no-shares claude`). It never gets shares. The folder’s path is made in the VM as an empty working folder. `~/.agent-vm/env` and `runtime.sh` go in, the project’s files do not.', 'off'], ], envTitle: 'Environment variables', envNote: 'Read from your shell, never from a file of the project.', envHeaders: ['Variable', 'What it does', 'Default'], env: [ ['AGENT_VM_STATE_DIR', 'Moves `~/.agent-vm`, VMs included, for tests, CI or a second install, without moving `HOME`.', '~/.agent-vm'], - ['AGENT_VM_LIMA_HOME', 'Where the VMs live. Not your own Lima’s home: agent-vm keeps its VMs apart.', '~/.agent-vm/lima'], + ['AGENT_VM_LIMA_HOME', 'Where Lima keeps what the VMs share (SSH key, networks). Not your own Lima’s home: agent-vm keeps its own apart. The VMs are in `~/.agent-vm/vms//`.', '~/.agent-vm/lima'], ['AGENT_VM_PROJECT_ENV', 'The project’s env file, relative to the project or absolute.', '.agent-vm.env'], ['AGENT_VM_PROJECT_RUNTIME', 'The project’s runtime script, relative to the project or absolute. Outside the project, the host reads it.', '.agent-vm.runtime.sh'], ['AGENT_VM_HOST_SHARE', 'CPU and memory per VM are capped at the host’s divided by this. `1`: the whole host.', '2'], @@ -352,7 +354,7 @@ export const en = { { title: 'From 0.2', paras: [ - 'Lima is built in: Homebrew’s Lima, or the Lima build 0.2 installed, can go, unless you use it for VMs of your own. agent-vm keeps its VMs in `~/.agent-vm/lima`, apart from yours, and names them without the `agent-vm-` prefix: `your-project-1a2b3c4d`, and `base` for the template.', + 'Lima is built in: Homebrew’s Lima, or the Lima build 0.2 installed, can go, unless you use it for VMs of your own. agent-vm keeps each VM in a folder of its own, `~/.agent-vm/vms//`, apart from yours, and names them without the `agent-vm-` prefix: `your-project-1a2b3c4d`, and `base` for the template. The folder holds Lima’s files for the VM and what agent-vm knows of it: removing the VM removes the folder.', 'The first command that needs the VMs offers to move 0.2’s from `~/.lima` (or `$LIMA_HOME`), stopping the running ones first: they keep their disks, shares and settings. Declined, it asks again next time; with no terminal to ask on, it moves them. `doctor` and `info` only look, and say when some are still to move.', 'To update, run the curl installer again, or `git pull && ./agent-vm.sh install` in a clone (Go needed): the link on your `PATH` then leads to the binary. Until then, 0.2’s link to `agent-vm.sh` keeps working (in a clone, after a `git pull`, it builds agent-vm first), and so does a shell rc line sourcing it, which `install` says can go. A VM from a base 0.1.0 built no longer starts: run `agent-vm setup`, then `--reset`.', ], @@ -422,7 +424,7 @@ export const en = { title: 'Extra mounts, in detail', paras: [ 'A destination is used as written, with no `~`: the VM home is `/home/.guest` (`.linux` before Lima 2.1). A missing source is skipped with a warning.', - 'A single file is hardlinked into `~/.agent-vm/file-mounts//` and bind-mounted. Across filesystems it is copied, and host edits wait for the next start.', + 'A single file is hardlinked into `~/.agent-vm/vms//file-mounts/` and bind-mounted. Across filesystems it is copied, and host edits wait for the next start.', ], list: [], code: '# ~/.agent-vm/volumes: your Claude instructions and skills,\n# not the whole ~/.claude, which holds your login on Linux\n~/.claude/CLAUDE.md:/home/you.guest/.claude/CLAUDE.md\n~/.claude/skills:/home/you.guest/.claude/skills', @@ -547,7 +549,7 @@ export const en = { 'For scripts, `scp` or `rsync`, `agent-vm info` prints the SSH alias as `ssh_host`. Put these lines at the top of `~/.ssh/config`: a `ForwardAgent yes` found before them would hand the VM your SSH keys. `lima-*` also matches the VMs of your own Lima, if you have one. `--ssh-port` pins the port for tools that save it.', ], list: [], - code: '# top of ~/.ssh/config\nInclude ~/.agent-vm/lima/*/ssh.config\nHost lima-*\n ForwardAgent no\n ForwardX11 no\n\nagent-vm info | grep ^ssh_host # the alias to use\nagent-vm --ssh-port 2222 shell # a fixed port', + code: '# top of ~/.ssh/config\nInclude ~/.agent-vm/vms/*/lima/ssh.config\nHost lima-*\n ForwardAgent no\n ForwardX11 no\n\nagent-vm info | grep ^ssh_host # the alias to use\nagent-vm --ssh-port 2222 shell # a fixed port', }, { title: 'Shell, commands, hooks', diff --git a/www/src/i18n/fr.ts b/www/src/i18n/fr.ts index 3c46f794..b3756187 100644 --- a/www/src/i18n/fr.ts +++ b/www/src/i18n/fr.ts @@ -275,13 +275,15 @@ export const fr: Dictionary = { ['--unsafe-disable-security-prompts', 'Continue là où un démarrage s\'arrêterait sur une question de sécurité (voir [Protéger .git](#git)). Les avertissements restent affichés. Équivaut à `AGENT_VM_UNSAFE_DISABLE_SECURITY_PROMPTS=1`.', 'inactif'], ['--rm', 'Détruit la VM dès que la commande se termine.', 'inactif'], ['--scratch', 'Une nouvelle VM où rien de ce qui est à toi n\'est monté, supprimée à la fin de la commande (après confirmation sur un terminal). `~/.agent-vm/env` et `runtime.sh` y entrent, les fichiers du projet non. Plusieurs peuvent tourner en même temps.', 'inactif'], + ['--name NOM', 'La VM appelée `NOM`, pas celle du dossier, pour qu\'un dossier puisse en avoir plusieurs (`agent-vm --name agent2 claude`). Elle appartient au dossier où elle est créée et ne tourne que depuis lui : depuis un autre, elle s\'arrête en nommant le dossier. `--reset` la refait, seul moyen de changer cela. Lettres minuscules, chiffres et tirets, 40 caractères au plus ; ni `base` ni `base-…`, ni un nom qui finit par un tiret et 8 chiffres hexadécimaux. `stop NOM` et `rm NOM` l\'atteignent.', 'la VM du dossier'], + ['--no-shares', 'Avec `--name`, à la création de la VM : elle ne partage rien, comme `--scratch`, mais elle est conservée et tourne depuis n\'importe quel dossier (`agent-vm --name sandbox --no-shares claude`). Elle ne reçoit jamais de partages. Le chemin du dossier est créé dans la VM comme dossier de travail vide. `~/.agent-vm/env` et `runtime.sh` y entrent, les fichiers du projet non.', 'inactif'], ], envTitle: 'Variables d\'environnement', envNote: 'Lues dans ton shell, jamais dans un fichier du projet.', envHeaders: ['Variable', 'Rôle', 'Défaut'], env: [ ['AGENT_VM_STATE_DIR', 'Déplace `~/.agent-vm`, VM comprises, pour des tests, la CI ou une seconde installation, sans déplacer `HOME`.', '~/.agent-vm'], - ['AGENT_VM_LIMA_HOME', 'Où vivent les VM. Pas le dossier de ton propre Lima : agent-vm garde ses VM à part.', '~/.agent-vm/lima'], + ['AGENT_VM_LIMA_HOME', 'Où Lima garde ce que les VM partagent (clé SSH, réseaux). Pas le dossier de ton propre Lima : agent-vm garde le sien à part. Les VM sont dans `~/.agent-vm/vms//`.', '~/.agent-vm/lima'], ['AGENT_VM_PROJECT_ENV', 'Le fichier d\'env du projet, relatif au projet ou absolu.', '.agent-vm.env'], ['AGENT_VM_PROJECT_RUNTIME', 'Le script de runtime du projet, relatif au projet ou absolu. Hors du projet, c\'est l\'hôte qui le lit.', '.agent-vm.runtime.sh'], ['AGENT_VM_HOST_SHARE', 'CPU et mémoire par VM sont plafonnés à ceux de l\'hôte divisés par ce nombre. `1` : tout l\'hôte.', '2'], @@ -354,7 +356,7 @@ export const fr: Dictionary = { { title: 'Depuis la 0.2', paras: [ - 'Lima est intégré : le Lima de Homebrew, ou la version de Lima installée par la 0.2, peut partir, sauf si tu t\'en sers pour tes propres VM. agent-vm garde ses VM dans `~/.agent-vm/lima`, à l\'écart des tiennes, et les nomme sans le préfixe `agent-vm-` : `ton-projet-1a2b3c4d`, et `base` pour l\'image.', + 'Lima est intégré : le Lima de Homebrew, ou la version de Lima installée par la 0.2, peut partir, sauf si tu t\'en sers pour tes propres VM. agent-vm garde chaque VM dans un dossier à elle, `~/.agent-vm/vms//`, à l\'écart des tiennes, et les nomme sans le préfixe `agent-vm-` : `ton-projet-1a2b3c4d`, et `base` pour l\'image. Le dossier contient les fichiers de Lima pour la VM et ce qu\'agent-vm en sait : supprimer la VM supprime le dossier.', 'La première commande qui a besoin des VM propose de déplacer celles de la 0.2 depuis `~/.lima` (ou `$LIMA_HOME`), en arrêtant d\'abord celles qui tournent : elles gardent leurs disques, partages et réglages. Si tu refuses, agent-vm redemande la fois suivante ; sans terminal pour demander, il les déplace. `doctor` et `info` se contentent de regarder, et disent s\'il en reste à déplacer.', 'Pour mettre à jour, relance l\'installeur curl, ou `git pull && ./agent-vm.sh install` dans un clone (Go nécessaire) : le lien de ton `PATH` mène alors au binaire. D\'ici là, le lien de la 0.2 vers `agent-vm.sh` continue de marcher (dans un clone, après un `git pull`, il compile d\'abord agent-vm), tout comme une ligne de ton fichier rc qui le source, dont `install` signale qu\'elle peut partir. Une VM issue d\'une base construite par la 0.1.0 ne démarre plus : lance `agent-vm setup`, puis `--reset`.', ], @@ -424,7 +426,7 @@ export const fr: Dictionary = { title: 'Montages supplémentaires, en détail', paras: [ 'Une destination est prise telle quelle, sans `~` : le dossier personnel dans la VM est `/home/.guest` (`.linux` avant Lima 2.1). Une source absente est ignorée, avec un avertissement.', - 'Un fichier seul est lié en dur dans `~/.agent-vm/file-mounts//` puis monté. D\'un système de fichiers à l\'autre, il est copié, et les modifications de l\'hôte attendent le démarrage suivant.', + 'Un fichier seul est lié en dur dans `~/.agent-vm/vms//file-mounts/` puis monté. D\'un système de fichiers à l\'autre, il est copié, et les modifications de l\'hôte attendent le démarrage suivant.', ], list: [], code: '# ~/.agent-vm/volumes : tes instructions et skills Claude,\n# pas tout ~/.claude, qui contient ta connexion sous Linux\n~/.claude/CLAUDE.md:/home/toi.guest/.claude/CLAUDE.md\n~/.claude/skills:/home/toi.guest/.claude/skills', @@ -549,7 +551,7 @@ export const fr: Dictionary = { 'Pour des scripts, `scp` ou `rsync`, `agent-vm info` affiche l\'alias SSH comme `ssh_host`. Mets ces lignes en haut de `~/.ssh/config` : un `ForwardAgent yes` trouvé avant elles donnerait tes clés SSH à la VM. `lima-*` couvre aussi les VM de ton propre Lima, si tu en as un. `--ssh-port` fixe le port pour les outils qui l\'enregistrent.', ], list: [], - code: '# en haut de ~/.ssh/config\nInclude ~/.agent-vm/lima/*/ssh.config\nHost lima-*\n ForwardAgent no\n ForwardX11 no\n\nagent-vm info | grep ^ssh_host # l\'alias à utiliser\nagent-vm --ssh-port 2222 shell # un port fixe', + code: '# en haut de ~/.ssh/config\nInclude ~/.agent-vm/vms/*/lima/ssh.config\nHost lima-*\n ForwardAgent no\n ForwardX11 no\n\nagent-vm info | grep ^ssh_host # l\'alias à utiliser\nagent-vm --ssh-port 2222 shell # un port fixe', }, { title: 'Shell, commandes, hooks',