diff --git a/cmd/image/commands.go b/cmd/image/commands.go index cbdd859..65fc9dd 100644 --- a/cmd/image/commands.go +++ b/cmd/image/commands.go @@ -7,12 +7,7 @@ import ( ) func Command() *cobra.Command { - imageCmd := &cobra.Command{ - Use: "image", - Short: "Manage macOS disk images (reuses cocoon's cloudimg store)", - Args: cobra.NoArgs, - RunE: cliutil.ShowHelp, - } + imageCmd := cliutil.GroupCommand("image", "Manage macOS disk images (reuses cocoon's cloudimg store)") pull := &cobra.Command{ Use: "pull REF", diff --git a/cmd/image/oci.go b/cmd/image/oci.go index 0f253cc..9ac4d83 100644 --- a/cmd/image/oci.go +++ b/cmd/image/oci.go @@ -37,7 +37,7 @@ func pullOCIBlob(ctx context.Context, ref, dest string) error { } blobURL := fmt.Sprintf("https://%s/v2/%s/blobs/%s", repo.Reference.Host(), repo.Reference.Repository, layer.Digest) - f, err := os.Create(dest) //nolint:gosec // dest is an internal temp path (os.CreateTemp), not user input + f, err := os.Create(dest) if err != nil { return err } diff --git a/cmd/vm/commands.go b/cmd/vm/commands.go index 3af6aa6..5a415ac 100644 --- a/cmd/vm/commands.go +++ b/cmd/vm/commands.go @@ -7,12 +7,7 @@ import ( ) func Command() *cobra.Command { - vmCmd := &cobra.Command{ - Use: "vm", - Short: "Manage macOS VMs", - Args: cobra.NoArgs, - RunE: cliutil.ShowHelp, - } + vmCmd := cliutil.GroupCommand("vm", "Manage macOS VMs") createCmd := &cobra.Command{ Use: "create [flags] IMAGE", diff --git a/cmd/vm/gc.go b/cmd/vm/gc.go index d50f941..921dd8e 100644 --- a/cmd/vm/gc.go +++ b/cmd/vm/gc.go @@ -50,7 +50,7 @@ func GC(cmd *cobra.Command, _ []string) error { return sweep(cliutil.CommandContext(cmd), cmd, registerNetGC) } -// sweep takes the provisioning lock exclusively and every VM's lock, so a create, clone or rm in flight is never read as residue; a busy lock refuses the run. The cm-family collectors are host-global, so they run only for a root that has owned host devices: a VM dir seen under the lock, or the marker every path that provisions or removes a VM dir persists first. A root without either still sweeps its own temp files. +// sweep holds the provisioning lock exclusively and every VM lock, so an in-flight create, clone or rm is never read as residue. func sweep(ctx context.Context, cmd *cobra.Command, registerNet func(*gc.Orchestrator, *cobra.Command, network.VMInUse) error) error { if _, err := os.Stat(home.Dir(cmd)); err != nil { return fmt.Errorf("no state root at %s: check --state-dir or $COCOON_MACOS_HOME: %w", home.Dir(cmd), err) @@ -80,13 +80,13 @@ func sweep(ctx context.Context, cmd *cobra.Command, registerNet func(*gc.Orchest gc.Register(o, vmGCModule(home.Dir(cmd), dirs)) if len(dirs) == 0 && !utils.FileExists(filepath.Join(home.VMsDir(cmd), ".locks", provisionedMarker)) { log.WithFunc("cmd.vm.sweep").Warnf(ctx, "no VM dir and no provisioning marker under %s: leaving the host's netns and TAPs alone (check --state-dir or $COCOON_MACOS_HOME; a root from before this build regains them at its next create)", home.Dir(cmd)) - } else if err := registerNet(o, cmd, vmInUse(dirs)); err != nil { + } else if err := registerNet(o, cmd, func(context.Context, string) (bool, error) { return false, nil }); err != nil { return err } return o.Run(ctx) } -// withProvisionLock shares the gc lock for the span in which a create or clone owns host devices no record names yet; the file is persistent so a waiter never ends up on an unlinked inode. +// withProvisionLock shares the gc lock for the span in which a create or clone owns host devices no record names yet. func withProvisionLock(ctx context.Context, cmd *cobra.Command, fn func() error) error { gl, err := openGCLock(cmd) if err != nil { @@ -99,13 +99,13 @@ func withProvisionLock(ctx context.Context, cmd *cobra.Command, fn func() error) return fn() } -// markProvisioned records that the state root owning vmsDir has held a VM: applyNet writes it right before the first host device, rm and the incomplete-dir reset before they remove a VM dir, so the evidence outlives the dir while a create that failed before provisioning under a mistyped root leaves nothing behind. +// markProvisioned records that the state root owning vmsDir has held a VM; the marker outlives the last VM dir. func markProvisioned(vmsDir string) error { path := filepath.Join(vmsDir, ".locks", provisionedMarker) if err := utils.EnsureDirs(filepath.Dir(path)); err != nil { return fmt.Errorf("create vm lock dir: %w", err) } - marker, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o600) //nolint:gosec // under the state root's own lock dir + marker, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o600) if err != nil { return fmt.Errorf("mark state root provisioned: %w", err) } @@ -165,24 +165,6 @@ func tryLockVMDirs(ctx context.Context, dirs []string) (func(), error) { return unlock, nil } -func vmInUse(dirs []string) network.VMInUse { - return func(_ context.Context, vmID string) (bool, error) { - for _, dir := range dirs { - r, err := loadRec(dir) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - continue - } - return false, fmt.Errorf("read vm %s for gc: %w", filepath.Base(dir), err) - } - if r.VMID == vmID { - return isRunning(r), nil - } - } - return false, nil - } -} - func vmGCModule(stateDir string, dirs []string) gc.Module[vmGCSnapshot] { return gc.Module[vmGCSnapshot]{ Name: "vm", diff --git a/cmd/vm/gc_test.go b/cmd/vm/gc_test.go index 1b91fc2..06f703e 100644 --- a/cmd/vm/gc_test.go +++ b/cmd/vm/gc_test.go @@ -169,7 +169,7 @@ func TestGCRefusesABusyVM(t *testing.T) { if err := held.Lock(t.Context()); err != nil { t.Fatal(err) } - defer held.Unlock(t.Context()) //nolint:errcheck + defer held.Unlock(t.Context()) err := sweep(t.Context(), cmd, noNetGC) if err == nil || !strings.Contains(err.Error(), "busy") { diff --git a/cmd/vm/handler.go b/cmd/vm/handler.go index ad8f524..9e3ec48 100644 --- a/cmd/vm/handler.go +++ b/cmd/vm/handler.go @@ -33,7 +33,7 @@ type record struct { NetMode string `json:"net_mode,omitempty"` Hugepages bool `json:"hugepages,omitempty"` ExitOnReboot bool `json:"exit_on_reboot,omitempty"` - DataDisks []string `json:"data_disks,omitempty"` // created data-disk qcow2 paths, attached on AHCI ports 0,1,3,5 + DataDisks []string `json:"data_disks,omitempty"` Disk string `json:"disk"` OpenCore string `json:"opencore"` diff --git a/cmd/vm/lifecycle.go b/cmd/vm/lifecycle.go index 5b0316c..568aea4 100644 --- a/cmd/vm/lifecycle.go +++ b/cmd/vm/lifecycle.go @@ -39,7 +39,7 @@ func Start(cmd *cobra.Command, args []string) error { if err != nil { return err } - // an op that held the lock (export, a racing run) may have restarted qemu; adopt it and only repair a dead vnc proxy + // an op that held the lock (a racing run) may have restarted qemu; adopt it and only repair a dead vnc proxy running, err := reconcileRunningQEMU(dir, r) if err != nil { return err diff --git a/cmd/vm/net_linux.go b/cmd/vm/net_linux.go index 9bcfd68..fdb1809 100644 --- a/cmd/vm/net_linux.go +++ b/cmd/vm/net_linux.go @@ -5,7 +5,6 @@ package vm import ( "cmp" "context" - "errors" "fmt" "os/exec" "path/filepath" @@ -77,9 +76,6 @@ func provisionNet(cmd *cobra.Command, r *record) (tap, netns, mac string, err er return "", "", "", fmt.Errorf("prepare network: %w", err) } cfgs, err := provider.Add(ctx, r.VMID, vmCfg, network.AddRange(0, 1)...) - if err == nil && len(cfgs) == 0 { - err = errors.New("network add returned no NIC") - } if err != nil { // Add rolls back only what it created itself; the netns from Prepare is ours to drop rctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), vmCleanupTimeout) diff --git a/cmd/vm/utils.go b/cmd/vm/utils.go index bc287fe..3026252 100644 --- a/cmd/vm/utils.go +++ b/cmd/vm/utils.go @@ -352,7 +352,7 @@ func powerDown(ctx context.Context, monSock string, r *record, grace time.Durati return false } if err := utils.WaitFor(ctx, grace, 200*time.Millisecond, func() (bool, error) { return !isRunning(r), nil }); err != nil { - logger.Infof(ctx, "guest %s did not halt within %s; signaling qemu", r.Name, grace) + logger.Warnf(ctx, "guest %s did not halt within %s; signaling qemu", r.Name, grace) } return true } diff --git a/cmd/vm/utils_test.go b/cmd/vm/utils_test.go index aa5f159..fcfcc95 100644 --- a/cmd/vm/utils_test.go +++ b/cmd/vm/utils_test.go @@ -164,17 +164,17 @@ func TestStopInstanceAsksTheGuestToPowerDownFirst(t *testing.T) { if err != nil { t.Fatal(err) } - defer ln.Close() //nolint:errcheck + defer ln.Close() got := make(chan string, 1) go func() { conn, err := ln.Accept() if err != nil { return } - defer conn.Close() //nolint:errcheck - fmt.Fprint(conn, hmpPrompt) //nolint:errcheck + defer conn.Close() + fmt.Fprint(conn, hmpPrompt) line, _ := bufio.NewReader(conn).ReadString('\n') - fmt.Fprint(conn, " "+line+hmpPrompt) //nolint:errcheck + fmt.Fprint(conn, " "+line+hmpPrompt) got <- strings.TrimSpace(line) }() disk := filepath.Join(dir, "disk.qcow2") diff --git a/cmd/vm/vnc.go b/cmd/vm/vnc.go index 870e7af..cfbd654 100644 --- a/cmd/vm/vnc.go +++ b/cmd/vm/vnc.go @@ -20,7 +20,6 @@ import ( "github.com/cocoonstack/cocoon/utils" ) -// The proxy fronts a CNI VM's netns-local VNC unix socket on a host TCP port (see qemu.Spec.VNCSock). const ( vncSockName = "vnc.sock" vncProxyPID = "vnc-proxy.pid" @@ -53,8 +52,8 @@ func validateVNCPassword(pw string) error { if len(pw) > 8 { return fmt.Errorf("--vnc-password must be at most 8 bytes, got %d", len(pw)) } - if strings.ContainsFunc(pw, func(r rune) bool { return unicode.IsControl(r) || unicode.IsSpace(r) }) { - return errors.New("--vnc-password must not contain whitespace or control characters") + if strings.ContainsFunc(pw, func(r rune) bool { return unicode.IsControl(r) || unicode.IsSpace(r) || r == '"' }) { + return errors.New("--vnc-password must not contain whitespace, control characters or double quotes") } return nil } diff --git a/cmd/vm/vnc_test.go b/cmd/vm/vnc_test.go index 87ab8aa..d87bbb0 100644 --- a/cmd/vm/vnc_test.go +++ b/cmd/vm/vnc_test.go @@ -50,6 +50,7 @@ func TestValidateVNCPassword(t *testing.T) { {"newline injection rejected", "x\nquit", true}, {"carriage return rejected", "x\rset", true}, {"tab rejected", "a\tb", true}, + {"leading quote rejected", "\"abcdef\"", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/docs/cli.md b/docs/cli.md index 741c18f..cb37b0b 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -91,9 +91,9 @@ snapshot and prevents new orphan collection; a read error is not evidence that t ## What `vm run` does -1. `qemu-img create -b overlay.qcow2` — instant copy-on-write clone of the golden image. +1. `qemu-img create -b disk.qcow2` — instant copy-on-write clone of the golden image. 2. Copy a per-VM `OVMF_VARS`. -3. With `--random-smbios`, copy OpenCore per-VM and inject a generated identity into its +3. With `--random-smbios`, bake a per-VM OpenCore overlay and inject a generated identity into its `config.plist` `PlatformInfo/Generic` via a `qemu-nbd` mount — model stays `iMac19,1` (proven to boot Tahoe), only serial/MLB/UUID/ROM are randomized. The identity is recorded and shown by `vm inspect`. diff --git a/docs/networking.md b/docs/networking.md index 083086c..b160446 100644 --- a/docs/networking.md +++ b/docs/networking.md @@ -55,6 +55,9 @@ stops storming softirqs, and `vm start` (or the relaunch) brings it back up. `vm `create`) deletes the TAP and the netns; a netns or TAP left behind by a create or clone that was killed before its record was written is reclaimed by `cocoon-macos gc`. +`vm start` (and a forced restore's relaunch) rebuilds an auto-created TAP (under `cni` also its netns +and TC redirect) with the recorded MAC when the host lost it, e.g. after a reboot. + ## VNC exposure VNC exposure depends on the net mode: @@ -83,9 +86,9 @@ retargeted, so the CLI prints that the supplied VNC settings were ignored and ch QEMU's default `None` auth **hangs macOS Screen Sharing**. Pass `--vnc-password <≤8 bytes>` (applied via the QEMU monitor post-launch) so Screen Sharing prompts and connects. The launch is **rejected** -(not truncated) if the password exceeds 8 bytes or contains whitespace or control characters — QEMU's VNC DES -auth only supports 8-byte passwords. Plain VNC clients (RealVNC/TigerVNC) work without a password on -the loopback modes. +(not truncated) if the password exceeds 8 bytes or contains whitespace, control characters or double +quotes — QEMU's VNC DES auth only supports 8-byte passwords. Plain VNC clients (RealVNC/TigerVNC) work +without a password on the loopback modes. > In-guest macOS Screen Sharing (to the guest's own IP) is **not** enabled headlessly — macOS > requires the Screen Recording TCC grant from the GUI or MDM. Use QEMU's built-in VNC above instead. diff --git a/go.mod b/go.mod index 7d8a932..41f0a70 100644 --- a/go.mod +++ b/go.mod @@ -3,33 +3,34 @@ module github.com/cocoonstack/cocoon-macos go 1.27.0 require ( - github.com/cocoonstack/cocoon v0.6.9-0.20260923161722-22b7392e16b0 - github.com/gofrs/flock v0.13.0 + github.com/cocoonstack/cocoon v0.6.9-0.20260924025202-dcec1fa3f056 + github.com/gofrs/flock v0.13.1 github.com/opencontainers/image-spec v1.1.1 - github.com/projecteru2/core v0.1.3 + github.com/projecteru2/core v0.1.5 github.com/spf13/cobra v1.10.2 howett.net/plist v1.0.1 - oras.land/oras-go/v2 v2.6.1 + oras.land/oras-go/v2 v2.6.2 ) require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/errors v1.14.0 // indirect github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect github.com/cockroachdb/redact v1.1.8 // indirect - github.com/containernetworking/cni v1.3.0 // indirect + github.com/containernetworking/cni v1.3.1 // indirect github.com/containernetworking/plugins v1.9.1 // indirect github.com/creack/pty v1.1.24 // indirect - github.com/docker/cli v29.6.2+incompatible // indirect + github.com/docker/cli v29.8.0+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.8 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.10.1 // indirect - github.com/getsentry/sentry-go v0.48.0 // indirect + github.com/getsentry/sentry-go v0.49.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/go-containerregistry v0.21.7 // indirect + github.com/google/go-containerregistry v0.22.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.19.2 // indirect + github.com/klauspost/compress v1.20.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.15 // indirect @@ -46,15 +47,17 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netns v0.0.5 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect + go.opentelemetry.io/otel v1.45.0 // indirect + go.opentelemetry.io/otel/trace v1.45.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect golang.org/x/text v0.41.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260918162117-cecb64721679 // indirect google.golang.org/grpc v1.83.2 // indirect google.golang.org/protobuf v1.36.12 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect - modernc.org/libc v1.74.1 // indirect + modernc.org/libc v1.75.7 // indirect modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.54.0 // indirect + modernc.org/memory v1.12.1 // indirect + modernc.org/sqlite v1.59.0 // indirect ) diff --git a/go.sum b/go.sum index 2015474..a53b3ba 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,25 @@ 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cockroachdb/errors v1.14.0 h1:EfdVEJpN3z8rPMo43Yit59LxoiIa470fSXpZXuEs+ZI= github.com/cockroachdb/errors v1.14.0/go.mod h1:xRa70jZ9sNBQmISt5KmJmAD++E4dQHm89oCRiZGEdq0= github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k= github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo= github.com/cockroachdb/redact v1.1.8 h1:8eVLLj6juKxiKrAEw2b8cJvNqWq++U8WOfQFuL7KTaA= github.com/cockroachdb/redact v1.1.8/go.mod h1:GceHHpJ0rMDpYARL5In88Alq/xMBUtVlz7Qxix6ZVkw= -github.com/cocoonstack/cocoon v0.6.9-0.20260923161722-22b7392e16b0 h1:RJe3v2i/sP1BymsYlJx8+0DqnTRxExvYyi1rslV7tvg= -github.com/cocoonstack/cocoon v0.6.9-0.20260923161722-22b7392e16b0/go.mod h1:B02b6pewrk1DbrRn+iBtDc8LNQWxwot2qF1j4YX5/jg= -github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= -github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= +github.com/cocoonstack/cocoon v0.6.9-0.20260924025202-dcec1fa3f056 h1:WlBQ1pI36wKAQbD31NMLJEyd22kLmwdORuZMmhGhTm4= +github.com/cocoonstack/cocoon v0.6.9-0.20260924025202-dcec1fa3f056/go.mod h1:hnIOJMDDAZq4qsXlT3uhTAib1G+LN9TTiA4zYySnugw= +github.com/containernetworking/cni v1.3.1 h1:gnHlU/YC1bJcS+CSh+pYQKeuu4O4gsMGW1NMaLhkzW4= +github.com/containernetworking/cni v1.3.1/go.mod h1:OtVXL0yXMfQb+p93OC/vRuhjpxsO1gAV0smjt28UQUQ= github.com/containernetworking/plugins v1.9.1 h1:8oU6WsIsU3bpnNZuvHp74a6cE1MJwbj2P7s4/yTUNlA= github.com/containernetworking/plugins v1.9.1/go.mod h1:fj7kS55qg3o/RgS+WGsF3+ZxwIImMPusQZKzBpcSr4c= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn6LwTVOcqw= -github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.8.0+incompatible h1:ih0c2jq/nN7QfES8zIfwSzIhRScjs4ehER+kZ60aeSk= +github.com/docker/cli v29.8.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q= github.com/docker/docker-credential-helpers v0.9.8/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -26,24 +28,26 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= -github.com/getsentry/sentry-go v0.48.0 h1:FRZNr7Uk1C86ev1bSJmYlUkL9oyivQA6YOcdYfaaMmY= -github.com/getsentry/sentry-go v0.48.0/go.mod h1:E5UkA5wp1qR2+MDydNYlVeUiNN2xEdjYMidkgf0Qoss= +github.com/getsentry/sentry-go v0.49.0 h1:Ehejknu1l023Ub7QoRBVLAI7g3Jnhqku4oWx4B4Sh5s= +github.com/getsentry/sentry-go v0.49.0/go.mod h1:nuMJAoCfe1u0Bts2ocyNI+TW8HT84vRMqwA5Qq/SKUI= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/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-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/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= -github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo= +github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 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/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= -github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= -github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= -github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/go-containerregistry v0.22.1 h1:RZuuSYhTvlDvtsK+NkutoCZ//C0X2ebLK8X8l3ULs84= +github.com/google/go-containerregistry v0.22.1/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= 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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -53,8 +57,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= -github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= 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= @@ -78,8 +82,8 @@ github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTw 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/projecteru2/core v0.1.3 h1:0odzNdtDKRvbMir3xKpqCPJaThHdzfzvou/vGFWquZY= -github.com/projecteru2/core v0.1.3/go.mod h1:4WGV1OlU8TY0hra+N8Ua2s8hbX1AeJmcjnj3PcztnI4= +github.com/projecteru2/core v0.1.5 h1:cS3xfqa6I1bMXtGdbu2A2UzhjiHaP2mogjQWFTkSEF0= +github.com/projecteru2/core v0.1.5/go.mod h1:MDMJmmlJ3ZVvFQl5BnRcKV1/AVCHyiWoI4BI+sCnuYM= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= @@ -105,6 +109,14 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= @@ -117,8 +129,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= -golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= 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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -128,15 +140,15 @@ golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +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/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.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= @@ -151,8 +163,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260918162117-cecb64721679 h1:KmqdJU4vrNcxy/6qdg3JduZtalEXrJLspVltnR1cE+8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260918162117-cecb64721679/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc= 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 v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= @@ -165,33 +177,33 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc= -modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= -modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= -modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= +modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8= +modernc.org/cc/v4 v4.29.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.35.0 h1:F+TUsmw09QxLzmi3aeYYGxjAXarmZaKgj3mKQHNaA8w= +modernc.org/ccgo/v4 v4.35.0/go.mod h1:qrVGs9S3Sr2Ztcg9ve+kTAYMp5a3YvWjo+SoN06kJ5I= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= -modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc= +modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ= -modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os= +modernc.org/libc v1.75.7 h1:o3DTP9/0p9pKmY2WCKQaySW6wIiZhNM7wc2lUoyhfew= +modernc.org/libc v1.75.7/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g= +modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog= -modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= +modernc.org/sqlite v1.59.0 h1:X1es1GpqBlS/5T+vbM4HLUdaa8OtQx468DF2vrx+38A= +modernc.org/sqlite v1.59.0/go.mod h1:+paeT2A3iPRHkQDwG7oA6Tk0zQd5woMEI8q7orfry8k= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -oras.land/oras-go/v2 v2.6.1 h1:bonOEkjLfp8tt6qXWRRWP6p1F+9octchOf2EqnWB4Zs= -oras.land/oras-go/v2 v2.6.1/go.mod h1:dhtFrFOuZuDtAVeZ9FUnaa5zfzplG3ZnFX9/uH1J/Yk= +oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo= +oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4= diff --git a/qemu/inject.go b/qemu/inject.go index cda3374..4de2495 100644 --- a/qemu/inject.go +++ b/qemu/inject.go @@ -121,10 +121,8 @@ func withNBDLease(ctx context.Context, path string, fn func() error) error { // waitForPart blocks until the kernel's async partition scan exposes nbdXp1 for the mount. func waitForPart(ctx context.Context, nbd string) error { if err := utils.WaitFor(ctx, 5*time.Second, 100*time.Millisecond, func() (bool, error) { - if _, err := os.Stat(nbd + "p1"); err == nil { - return true, nil - } - return false, nil + _, err := os.Stat(nbd + "p1") + return err == nil, nil }); err != nil { return fmt.Errorf("wait for partition on %s: %w", nbd, err) } diff --git a/qemu/launch.go b/qemu/launch.go index 334d2e9..c734ffe 100644 --- a/qemu/launch.go +++ b/qemu/launch.go @@ -22,14 +22,14 @@ const ( // Spec is the per-VM input for launching a macOS guest from a golden qcow2. type Spec struct { CPUs int - Memory string // MiB, e.g. "8192" - VNCDisp int // n => host 127.0.0.1:590n; <0 disables - VNCSock string // when set, bind VNC to this unix socket instead of 127.0.0.1 (CNI: fronted by vncProxy) - VNCPass string // set via the monitor post-launch (macOS Screen Sharing needs password auth) - SSHPort int // host port forwarded to guest :22; 0 disables - Hugepages bool // needs host hugepages reserved; off => default RAM - ExitOnReboot bool // exit QEMU on a guest reboot so an external owner can relaunch it cold - DataDisks []string // extra qcow2 data disks; attached on the AHCI ports MacHDD/OpenCore leave free + Memory string // MiB, e.g. "8192" + VNCDisp int // n => host 127.0.0.1:590n; <0 disables + VNCSock string // when set, bind VNC to this unix socket instead of 127.0.0.1 (CNI: fronted by vncProxy) + VNCPass string // set via the monitor post-launch (macOS Screen Sharing needs password auth) + SSHPort int // host port forwarded to guest :22; 0 disables + Hugepages bool // needs host hugepages reserved; off => default RAM + ExitOnReboot bool // exit QEMU on a guest reboot so an external owner can relaunch it cold + DataDisks []string Disk string OpenCore string diff --git a/qemu/smbios.go b/qemu/smbios.go index 8e8812d..4caff5a 100644 --- a/qemu/smbios.go +++ b/qemu/smbios.go @@ -4,6 +4,8 @@ import ( "crypto/rand" "encoding/hex" "fmt" + "strings" + "uuid" ) const ( @@ -30,15 +32,11 @@ func RandomSMBIOS() (SMBIOS, error) { if err != nil { return SMBIOS{}, err } - uuid, err := randUUID() - if err != nil { - return SMBIOS{}, err - } rom, err := randROM() if err != nil { return SMBIOS{}, err } - return SMBIOS{Model: smbiosModel, Serial: serial, MLB: mlb, UUID: uuid, ROM: rom}, nil + return SMBIOS{Model: smbiosModel, Serial: serial, MLB: mlb, UUID: strings.ToUpper(uuid.NewV4().String()), ROM: rom}, nil } // MAC returns the ROM formatted as the default guest NIC MAC; CNI supplies its own runtime MAC. @@ -61,16 +59,6 @@ func randString(n int) (string, error) { return string(b), nil } -func randUUID() (string, error) { - b, err := randBytes(16) - if err != nil { - return "", err - } - b[6] = (b[6] & 0x0f) | 0x40 // version 4 - b[8] = (b[8] & 0x3f) | 0x80 // variant - return fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil -} - func randROM() (string, error) { b, err := randBytes(6) if err != nil { diff --git a/qemu/snapshot.go b/qemu/snapshot.go index 5db64cd..9a5bbd2 100644 --- a/qemu/snapshot.go +++ b/qemu/snapshot.go @@ -29,7 +29,7 @@ type snapshotEntry struct { // SnapExists reports whether img carries the snapshot tag. func SnapExists(ctx context.Context, img, tag string) (bool, error) { - out, err := exec.CommandContext(ctx, "qemu-img", "info", "--output=json", img).Output() //nolint:gosec // img is an internal path + out, err := exec.CommandContext(ctx, "qemu-img", "info", "--output=json", img).Output() if err != nil { return false, fmt.Errorf("qemu-img info %s: %w", img, err) } diff --git a/scripts/e2e.sh b/scripts/e2e.sh index f3024db..7dcb513 100755 --- a/scripts/e2e.sh +++ b/scripts/e2e.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# cm-smoke.sh — cocoon-macos E2E regression, modeled on cocoonv2's issue28-repro.sh. +# scripts/e2e.sh — cocoon-macos E2E regression. # # Two tiers: # [DUMMY] file-level lifecycle on a tiny throwaway qcow2 — NO macOS boot. Fast, runs on any @@ -10,10 +10,10 @@ # Idempotent: cleans state-dir + test bridge + leftover cm* TAPs/netns at start AND end. # # Usage: -# sudo ./cm-smoke.sh # [DUMMY] tier only -# sudo ./cm-smoke.sh --real # [DUMMY] then [REAL] boot of tahoe:26 -# sudo ./cm-smoke.sh --no-net # skip the root/bridge --net auto-create rows -# sudo ./cm-smoke.sh cleanup # tear down and exit +# sudo ./scripts/e2e.sh # [DUMMY] tier only +# sudo ./scripts/e2e.sh --real # [DUMMY] then [REAL] boot of tahoe:26 +# sudo ./scripts/e2e.sh --no-net # skip the root/bridge --net auto-create rows +# sudo ./scripts/e2e.sh cleanup # tear down and exit # # Env overrides: # CM_BIN path to the cocoon-macos binary (default: ./cocoon-macos) diff --git a/scripts/qmp-input.py b/scripts/qmp-input.py index b56839f..c71dcd2 100644 --- a/scripts/qmp-input.py +++ b/scripts/qmp-input.py @@ -5,11 +5,16 @@ the 0..32767 range onto the framebuffer) plus key presses and screendumps. Usage: - qmp-input.py SOCK click PX PY # single left click at pixel (PX,PY) - qmp-input.py SOCK dclick PX PY # double click - qmp-input.py SOCK move PX PY # move pointer only - qmp-input.py SOCK key K [K...] # send qcode key(s), e.g. ret down spc - qmp-input.py SOCK screendump FILE # write framebuffer ppm + qmp-input.py SOCK click PX PY # single left click at pixel (PX,PY) + qmp-input.py SOCK dclick PX PY # double click + qmp-input.py SOCK move PX PY # move pointer only + qmp-input.py SOCK key K [K...] # send qcode key(s) one by one, e.g. ret down spc + qmp-input.py SOCK chord K [K...] # press qcode keys together, e.g. meta_l q + qmp-input.py SOCK type TEXT # type ASCII text, adding shift where needed + qmp-input.py SOCK ocrclick WORD [YMIN YMAX] # OCR-locate WORD and click it; exit 3 if not found + qmp-input.py SOCK ocrtext [YMIN [YMAX]] # print the OCR words between YMIN and YMAX + qmp-input.py SOCK agreebtn # click the active SLA Agree button; exit 3 if not found + qmp-input.py SOCK screendump FILE # write framebuffer ppm Screen size defaults to 1280x800; override with QMP_W / QMP_H env vars. """ from __future__ import annotations @@ -53,7 +58,7 @@ def __init__(self, path: str) -> None: self.f = self.s.makefile("rw") self.f.readline() # greeting self.cmd("qmp_capabilities") - except OSError: + except (OSError, RuntimeError): self.close() raise @@ -71,7 +76,7 @@ def close(self) -> None: self.f = None self.s.close() - def cmd(self, execute: str, **args: object) -> dict | None: + def cmd(self, execute: str, **args: object) -> dict: """Send a QMP command and return its response, skipping async events.""" msg: dict[str, object] = {"execute": execute} if args: @@ -81,9 +86,11 @@ def cmd(self, execute: str, **args: object) -> dict | None: while True: line = self.f.readline() if not line: - return None + raise RuntimeError(f"qmp closed before answering {execute}") obj = json.loads(line) - if "return" in obj or "error" in obj: + if "error" in obj: + raise RuntimeError(f"qmp {execute} failed: {obj['error'].get('desc', obj['error'])}") + if "return" in obj: return obj def send_events(self, events: list[dict]) -> None: @@ -154,35 +161,15 @@ def ocr_text(self, ymin: int = 0, ymax: int = 10 ** 9) -> str: def ocrclick(self, word: str, ymin: int = 0, ymax: int = 10 ** 9) -> bool: hits = self.ocr_find(word, ymin, ymax) if not hits: - print("NOTFOUND %s" % word) + print(f"NOTFOUND {word}") return False cx, cy, conf = hits[0] self.click(cx, cy) - print("CLICK %s (%d,%d) conf=%.0f" % (word, cx, cy, conf)) - return True - - def ocrdclick(self, word: str, yoff: int = 0) -> bool: - """OCR-locate word and double-click at its center (+yoff to hit an icon above its label). - - Boots an OpenCore/OpenCanopy picker entry by name — its arrow keys are - unreliable but the picker takes mouse input. - """ - hits = self.ocr_find(word) - if not hits: - print("NOTFOUND %s" % word) - return False - cx, cy, conf = hits[0] - self.click(cx, cy + yoff, n=2) - print("DCLICK %s (%d,%d) conf=%.0f" % (word, cx, cy + yoff, conf)) + print(f"CLICK {word} ({cx},{cy}) conf={conf:.0f}") return True def agree_button(self) -> tuple[int, int] | None: - """Locate the macOS SLA "Agree" button: the one immediately right of a same-row "Disagree". - - This excludes the body-text "...read and agree to the terms..."; when a - confirm sheet overlays the license pane, the topmost such pair is the - active modal button (background license buttons sit lower, greyed out). - """ + """Locate the SLA "Agree" button: the topmost one right of a same-row "Disagree", else the lowest "Agree".""" ag = self.ocr_find("Agree") dis = self.ocr_find("Disagree") pairs = [] @@ -192,7 +179,7 @@ def agree_button(self) -> tuple[int, int] | None: pairs.append((ay, ax)) break if pairs: - pairs.sort() # smallest y first = topmost active button row + pairs.sort() return pairs[0][1], pairs[0][0] if ag: # no Disagree paired: take the lowest Agree (plain license button at the bottom) lo = max(ag, key=lambda t: t[1]) @@ -246,14 +233,11 @@ def main() -> None: ymin = int(args[1]) if len(args) > 1 else 0 ymax = int(args[2]) if len(args) > 2 else 10 ** 9 sys.exit(0 if q.ocrclick(args[0], ymin, ymax) else 3) - elif op == "ocrdclick": - yoff = int(args[1]) if len(args) > 1 else 0 - sys.exit(0 if q.ocrdclick(args[0], yoff) else 3) elif op == "agreebtn": btn = q.agree_button() if btn: q.click(*btn) - print("AGREE (%d,%d)" % btn) + print(f"AGREE ({btn[0]},{btn[1]})") else: print("NO-AGREE-BUTTON") sys.exit(3)